feat: add and remove products to/from collection in bulk endpoints (#1032)
* adds bulk add/remove products to/from collection. Adds endpoint updateProducts on collections that uses these bulk operations * fix integration tests and test description * undo change to swap * made requested changes * added removeProducts endpoint * made requested changes * fix: set collection_id null * updated collection_id to type string | undefined
This commit is contained in:
1 parent
22d3d4a2f6
commit
66294038f0
14 files changed
+580
-73
No files matched your search
@@ -1,5 +1,102 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`/admin/collections /admin/collections adds products to collection 1`] = `
|
||||||
|
Object {
|
||||||
|
"collection": Object {
|
||||||
|
"created_at": Any<String>,
|
||||||
|
"deleted_at": null,
|
||||||
|
"handle": "test-collection",
|
||||||
|
"id": "test-collection",
|
||||||
|
"metadata": null,
|
||||||
|
"products": Array [
|
||||||
|
Object {
|
||||||
|
"collection_id": "test-collection",
|
||||||
|
"created_at": Any<String>,
|
||||||
|
"deleted_at": null,
|
||||||
|
"description": "test-product-description",
|
||||||
|
"discountable": true,
|
||||||
|
"external_id": null,
|
||||||
|
"handle": "test-product",
|
||||||
|
"height": null,
|
||||||
|
"hs_code": null,
|
||||||
|
"id": "test-product",
|
||||||
|
"is_giftcard": false,
|
||||||
|
"length": null,
|
||||||
|
"material": null,
|
||||||
|
"metadata": null,
|
||||||
|
"mid_code": null,
|
||||||
|
"origin_country": null,
|
||||||
|
"profile_id": StringMatching /\\^sp_\\*/,
|
||||||
|
"status": "draft",
|
||||||
|
"subtitle": null,
|
||||||
|
"thumbnail": null,
|
||||||
|
"title": "Test product",
|
||||||
|
"type_id": "test-type",
|
||||||
|
"updated_at": Any<String>,
|
||||||
|
"weight": null,
|
||||||
|
"width": null,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"collection_id": "test-collection",
|
||||||
|
"created_at": Any<String>,
|
||||||
|
"deleted_at": null,
|
||||||
|
"description": "test-product-description1",
|
||||||
|
"discountable": true,
|
||||||
|
"external_id": null,
|
||||||
|
"handle": "test-product1",
|
||||||
|
"height": null,
|
||||||
|
"hs_code": null,
|
||||||
|
"id": "test-product1",
|
||||||
|
"is_giftcard": false,
|
||||||
|
"length": null,
|
||||||
|
"material": null,
|
||||||
|
"metadata": null,
|
||||||
|
"mid_code": null,
|
||||||
|
"origin_country": null,
|
||||||
|
"profile_id": StringMatching /\\^sp_\\*/,
|
||||||
|
"status": "draft",
|
||||||
|
"subtitle": null,
|
||||||
|
"thumbnail": null,
|
||||||
|
"title": "Test product1",
|
||||||
|
"type_id": "test-type",
|
||||||
|
"updated_at": Any<String>,
|
||||||
|
"weight": null,
|
||||||
|
"width": null,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"collection_id": "test-collection",
|
||||||
|
"created_at": Any<String>,
|
||||||
|
"deleted_at": null,
|
||||||
|
"description": "test-product-description",
|
||||||
|
"discountable": true,
|
||||||
|
"external_id": null,
|
||||||
|
"handle": "test-product_filtering_1",
|
||||||
|
"height": null,
|
||||||
|
"hs_code": null,
|
||||||
|
"id": "test-product_filtering_1",
|
||||||
|
"is_giftcard": false,
|
||||||
|
"length": null,
|
||||||
|
"material": null,
|
||||||
|
"metadata": null,
|
||||||
|
"mid_code": null,
|
||||||
|
"origin_country": null,
|
||||||
|
"profile_id": StringMatching /\\^sp_\\*/,
|
||||||
|
"status": "proposed",
|
||||||
|
"subtitle": null,
|
||||||
|
"thumbnail": null,
|
||||||
|
"title": "Test product filtering 1",
|
||||||
|
"type_id": "test-type",
|
||||||
|
"updated_at": Any<String>,
|
||||||
|
"weight": null,
|
||||||
|
"width": null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Test collection",
|
||||||
|
"updated_at": Any<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`/admin/collections /admin/collections creates a collection 1`] = `
|
exports[`/admin/collections /admin/collections creates a collection 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"collection": Object {
|
"collection": Object {
|
||||||
@@ -259,6 +356,16 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`/admin/collections /admin/collections removes products from collection 1`] = `
|
||||||
|
Object {
|
||||||
|
"id": "test-collection",
|
||||||
|
"object": "product-collection",
|
||||||
|
"removed_products": Array [
|
||||||
|
"test-product",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`/admin/collections /admin/collections/:id gets collection 1`] = `
|
exports[`/admin/collections /admin/collections/:id gets collection 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"collection": Object {
|
"collection": Object {
|
||||||
|
|||||||
@@ -108,19 +108,19 @@ describe("/admin/collections", () => {
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
products: [
|
products: [
|
||||||
{
|
{
|
||||||
collection_id: "test-collection",
|
collection_id: "test-collection",
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
profile_id: expect.stringMatching(/^sp_*/),
|
profile_id: expect.stringMatching(/^sp_*/),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
collection_id: "test-collection",
|
collection_id: "test-collection",
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
profile_id: expect.stringMatching(/^sp_*/),
|
profile_id: expect.stringMatching(/^sp_*/),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -237,6 +237,75 @@ describe("/admin/collections", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("adds products to collection", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
// adds product test-product-filterid-1
|
||||||
|
const response = await api
|
||||||
|
.post(
|
||||||
|
"/admin/collections/test-collection/products/batch",
|
||||||
|
{
|
||||||
|
product_ids: ["test-product_filtering_1"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { Authorization: "Bearer test_token" },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((err) => console.warn(err))
|
||||||
|
|
||||||
|
expect(response.data).toMatchSnapshot({
|
||||||
|
collection: {
|
||||||
|
id: "test-collection",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
collection_id: "test-collection",
|
||||||
|
id: "test-product",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
profile_id: expect.stringMatching(/^sp_*/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collection_id: "test-collection",
|
||||||
|
id: "test-product1",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
profile_id: expect.stringMatching(/^sp_*/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collection_id: "test-collection",
|
||||||
|
id: "test-product_filtering_1",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
profile_id: expect.stringMatching(/^sp_*/),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("removes products from collection", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.delete("/admin/collections/test-collection/products/batch", {
|
||||||
|
headers: { Authorization: "Bearer test_token" },
|
||||||
|
data: { product_ids: ["test-product"] },
|
||||||
|
})
|
||||||
|
.catch((err) => console.warn(err))
|
||||||
|
|
||||||
|
expect(response.data).toMatchSnapshot({
|
||||||
|
id: "test-collection",
|
||||||
|
object: "product-collection",
|
||||||
|
removed_products: ["test-product"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
it("filters collections by title", async () => {
|
it("filters collections by title", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,15 @@
|
|||||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/medusa": "1.1.62-dev-1643977380526",
|
"@medusajs/medusa": "1.1.64-dev-1644230658795",
|
||||||
"medusa-interfaces": "1.1.32-dev-1643977380526",
|
"medusa-interfaces": "1.1.34-dev-1644230658795",
|
||||||
"typeorm": "^0.2.31"
|
"typeorm": "^0.2.31"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.12.10",
|
"@babel/cli": "^7.12.10",
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/node": "^7.12.10",
|
"@babel/node": "^7.12.10",
|
||||||
"babel-preset-medusa-package": "1.1.19-dev-1643977380526",
|
"babel-preset-medusa-package": "1.1.19-dev-1644230658795",
|
||||||
"jest": "^26.6.3"
|
"jest": "^26.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1256,10 +1256,10 @@
|
|||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|
||||||
"@medusajs/medusa-cli@1.1.27-dev-1643977380526":
|
"@medusajs/medusa-cli@1.1.27-dev-1644230658795":
|
||||||
version "1.1.27-dev-1643977380526"
|
version "1.1.27-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.27-dev-1643977380526.tgz#a74b506cef603ac1630eedb0329cfe0243c51de2"
|
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.27-dev-1644230658795.tgz#dc1fed2e68d4f3fa134786d07c3a252aa2f07354"
|
||||||
integrity sha512-n3clGbZYiWmr0BSyvejd0insnrjcx0MVir9LCkfXPkgnghkdC+C9lIoRAduceSnha9fxoz2XjfapV3VHeAvYoQ==
|
integrity sha512-m+DqNNdpGO0wubizrPwQoBad0LrGpyut9tdI24U6a0202Dbr5DL+ekW5Lgm8VEwNd/cIiHkI5Trym/hXsnun+w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.8.7"
|
"@babel/polyfill" "^7.8.7"
|
||||||
"@babel/runtime" "^7.9.6"
|
"@babel/runtime" "^7.9.6"
|
||||||
@@ -1277,8 +1277,8 @@
|
|||||||
is-valid-path "^0.1.1"
|
is-valid-path "^0.1.1"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
meant "^1.0.1"
|
meant "^1.0.1"
|
||||||
medusa-core-utils "1.1.31-dev-1643977380526"
|
medusa-core-utils "1.1.31-dev-1644230658795"
|
||||||
medusa-telemetry "0.0.11-dev-1643977380526"
|
medusa-telemetry "0.0.11-dev-1644230658795"
|
||||||
netrc-parser "^3.1.6"
|
netrc-parser "^3.1.6"
|
||||||
open "^8.0.6"
|
open "^8.0.6"
|
||||||
ora "^5.4.1"
|
ora "^5.4.1"
|
||||||
@@ -1292,13 +1292,13 @@
|
|||||||
winston "^3.3.3"
|
winston "^3.3.3"
|
||||||
yargs "^15.3.1"
|
yargs "^15.3.1"
|
||||||
|
|
||||||
"@medusajs/medusa@1.1.62-dev-1643977380526":
|
"@medusajs/medusa@1.1.64-dev-1644230658795":
|
||||||
version "1.1.62-dev-1643977380526"
|
version "1.1.64-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.62-dev-1643977380526.tgz#db32cba8212ba20694b81bb7a64ec40f5846abc0"
|
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.64-dev-1644230658795.tgz#10f957947ce9521bc8fe109737765647b98d3d7e"
|
||||||
integrity sha512-Dy/PWcTlMk7sW+Y3dgt8ypQZnuOqB9mtCDz+1yDkgLkVbUWWHbe/8kGIkAmwvetKTJJt6FoaEgCHlEaD72zulQ==
|
integrity sha512-gIuCzyEGT/lXG0yBGOdQ5H9pUg2WTYz/Q5nH8d2sRGDhW2A47cLOJb8VYsqrx6EcSNirW4bygoZgo24gyL5PFg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/joi" "^16.1.8"
|
"@hapi/joi" "^16.1.8"
|
||||||
"@medusajs/medusa-cli" "1.1.27-dev-1643977380526"
|
"@medusajs/medusa-cli" "1.1.27-dev-1644230658795"
|
||||||
"@types/lodash" "^4.14.168"
|
"@types/lodash" "^4.14.168"
|
||||||
awilix "^4.2.3"
|
awilix "^4.2.3"
|
||||||
body-parser "^1.19.0"
|
body-parser "^1.19.0"
|
||||||
@@ -1322,8 +1322,8 @@
|
|||||||
joi "^17.3.0"
|
joi "^17.3.0"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
jsonwebtoken "^8.5.1"
|
jsonwebtoken "^8.5.1"
|
||||||
medusa-core-utils "1.1.31-dev-1643977380526"
|
medusa-core-utils "1.1.31-dev-1644230658795"
|
||||||
medusa-test-utils "1.1.37-dev-1643977380526"
|
medusa-test-utils "1.1.37-dev-1644230658795"
|
||||||
morgan "^1.9.1"
|
morgan "^1.9.1"
|
||||||
multer "^1.4.2"
|
multer "^1.4.2"
|
||||||
passport "^0.4.0"
|
passport "^0.4.0"
|
||||||
@@ -1947,10 +1947,10 @@ babel-preset-jest@^26.6.2:
|
|||||||
babel-plugin-jest-hoist "^26.6.2"
|
babel-plugin-jest-hoist "^26.6.2"
|
||||||
babel-preset-current-node-syntax "^1.0.0"
|
babel-preset-current-node-syntax "^1.0.0"
|
||||||
|
|
||||||
babel-preset-medusa-package@1.1.19-dev-1643977380526:
|
babel-preset-medusa-package@1.1.19-dev-1644230658795:
|
||||||
version "1.1.19-dev-1643977380526"
|
version "1.1.19-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1643977380526.tgz#5db5e61d37a81d47e37f14cf25066fac36cc3743"
|
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1644230658795.tgz#11b437ba399ed335c2ca8ecd0ced8e2c94557e68"
|
||||||
integrity sha512-WmowYmKs5I6MMqiCPWgLCzD14LFhnAbqJ+BiJnR+XgXZdIs90y/9QIrDYjvmm+hhuit0GYRfsVvqgB+swAlzyQ==
|
integrity sha512-NSvAyqCQgnkGGAN5/Vs6QHT/KoG8AmPnuvMSooT2QBRB2h/vULVI4UuP7CQb3mfnfjX/gm9Tbch/zHhQJKPRUw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
||||||
"@babel/plugin-proposal-decorators" "^7.12.1"
|
"@babel/plugin-proposal-decorators" "^7.12.1"
|
||||||
@@ -5135,25 +5135,25 @@ media-typer@0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
medusa-core-utils@1.1.31-dev-1643977380526:
|
medusa-core-utils@1.1.31-dev-1644230658795:
|
||||||
version "1.1.31-dev-1643977380526"
|
version "1.1.31-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1643977380526.tgz#486812e5df4c5fb276591d3448a466d7df1a9c70"
|
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1644230658795.tgz#fdeb0df1976c0331c3ce975f368429adcc4732f8"
|
||||||
integrity sha512-EilnAOhs8Z8F6BaO4GpxdlZl2UTaOCDA0AUoYHr3FwA4VIx1ZrHWFaJKcZH3ZPhtkPiAfj0hJYIrIlWZiYL2AQ==
|
integrity sha512-DdaTYsepwqpJGg2Hk6fa5ODVZZr8vKOwTKuAdlE45+LcildU625/2fdHYI8fUdBuOqKMnZRwWqI06I77xlpHgw==
|
||||||
dependencies:
|
dependencies:
|
||||||
joi "^17.3.0"
|
joi "^17.3.0"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
|
|
||||||
medusa-interfaces@1.1.32-dev-1643977380526:
|
medusa-interfaces@1.1.34-dev-1644230658795:
|
||||||
version "1.1.32-dev-1643977380526"
|
version "1.1.34-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.32-dev-1643977380526.tgz#fa4e9d1fc78d49bb6acb6f00c9523ae7155018a2"
|
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.34-dev-1644230658795.tgz#b719329abe31a337ad4350161e8b45f3dd8af1e7"
|
||||||
integrity sha512-Sy25gHF66/ZBzYNKo8TE7sVu5Gz9H4TKGBW2FpIlTJJ7rJKcaKutX71Qc2JT7KEgJ/Jx5Bu9OJaZlYaqbtmiFQ==
|
integrity sha512-lKd5QeZi/kEU9yte5HwUafRLWO0F3L2ArgrNkNoGOgA49tOKE5qS+BYqH/hImmGBSducT3ig8Bd2bCn96iEbDA==
|
||||||
dependencies:
|
dependencies:
|
||||||
medusa-core-utils "1.1.31-dev-1643977380526"
|
medusa-core-utils "1.1.31-dev-1644230658795"
|
||||||
|
|
||||||
medusa-telemetry@0.0.11-dev-1643977380526:
|
medusa-telemetry@0.0.11-dev-1644230658795:
|
||||||
version "0.0.11-dev-1643977380526"
|
version "0.0.11-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1643977380526.tgz#becabd4b43192025ae35923f82cba55b2a4ec7e6"
|
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1644230658795.tgz#2599eefe6440795e73e71a90ca41e63e61101405"
|
||||||
integrity sha512-WhM5e3VjnpKAh2xBk/L0ZzkbV7B5sJHsl8ftHfZdncz1lMnVVBpScatXVBVIVVcwZPvyMD8wj6iMLBhDIYflMg==
|
integrity sha512-yAi6W7NXqVnCvseow5eLhP8mRh8sV79PGN/otPSmhuT3v82W9aXhIdhdOQVIbxsc7N1P0+3iunh7Qu7uR9cElw==
|
||||||
dependencies:
|
dependencies:
|
||||||
axios "^0.21.1"
|
axios "^0.21.1"
|
||||||
axios-retry "^3.1.9"
|
axios-retry "^3.1.9"
|
||||||
@@ -5165,13 +5165,13 @@ medusa-telemetry@0.0.11-dev-1643977380526:
|
|||||||
remove-trailing-slash "^0.1.1"
|
remove-trailing-slash "^0.1.1"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
medusa-test-utils@1.1.37-dev-1643977380526:
|
medusa-test-utils@1.1.37-dev-1644230658795:
|
||||||
version "1.1.37-dev-1643977380526"
|
version "1.1.37-dev-1644230658795"
|
||||||
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1643977380526.tgz#e211bf227b344d079595a091cd4079107b04c98b"
|
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1644230658795.tgz#803fcd6b6e7e831a14449af8545edfbe2302af3f"
|
||||||
integrity sha512-1BM1GVFcFUVfRq6D5kwXMlpdeXaXRB9Zjl6xppCgFrgooxvoVoT4cKWPMAgyo/Q7JEvOaIaZDT1LKrtFo/QFOg==
|
integrity sha512-VTTuHRngkoGCTLNzTE1Z14BfesRRlpmceS5qnicVkPLidY+l20WhJDAWLpE7RmRNQxuOXirJEdHT99cNQrA0yA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/plugin-transform-classes" "^7.9.5"
|
"@babel/plugin-transform-classes" "^7.9.5"
|
||||||
medusa-core-utils "1.1.31-dev-1643977380526"
|
medusa-core-utils "1.1.31-dev-1644230658795"
|
||||||
randomatic "^3.1.1"
|
randomatic "^3.1.1"
|
||||||
|
|
||||||
merge-descriptors@1.0.1:
|
merge-descriptors@1.0.1:
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("POST /admin/collections/:id/products/batch", () => {
|
||||||
|
describe("successfully adds products to collection", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"POST",
|
||||||
|
`/admin/collections/${IdMap.getId("col")}/products/batch`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
product_ids: ["prod_1", "prod_2"],
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns updated collection with new products", () => {
|
||||||
|
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("product collection service update", () => {
|
||||||
|
expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledTimes(1)
|
||||||
|
expect(
|
||||||
|
ProductCollectionServiceMock.addProducts
|
||||||
|
).toHaveBeenCalledWith(IdMap.getId("col"), ["prod_1", "prod_2"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("error on non-existing collection", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"POST",
|
||||||
|
`/admin/collections/null/products/batch`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
product_ids: ["prod_1", "prod_2"],
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws error", () => {
|
||||||
|
expect(subject.body.message).toBe("Product collection not found")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("error invalid request", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"POST",
|
||||||
|
`/admin/collections/${IdMap.getId("col")}/products/batch`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
product_ids: [],
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 400", () => {
|
||||||
|
expect(subject.status).toEqual(400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("DELETE /admin/collections/:id/products/batch", () => {
|
||||||
|
describe("successfully removes products from collection", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"DELETE",
|
||||||
|
`/admin/collections/${IdMap.getId("col")}/products/batch`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
product_ids: ["prod_1", "prod_2"],
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("product collection service remove products", () => {
|
||||||
|
expect(ProductCollectionServiceMock.removeProducts).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
ProductCollectionServiceMock.removeProducts
|
||||||
|
).toHaveBeenCalledWith(IdMap.getId("col"), ["prod_1", "prod_2"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("error on invalid request", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"DELETE",
|
||||||
|
`/admin/collections/${IdMap.getId("col")}/products/batch`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
product_ids: [],
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 400", () => {
|
||||||
|
expect(subject.status).toEqual(400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ArrayNotEmpty, IsString } from "class-validator"
|
||||||
|
import ProductCollectionService from "../../../../services/product-collection"
|
||||||
|
import { validator } from "../../../../utils/validator"
|
||||||
|
/**
|
||||||
|
* @oas [post] /collections/{id}/products/batch
|
||||||
|
* operationId: "PostProductsToCollection"
|
||||||
|
* summary: "Updates products associated with a Product Collection"
|
||||||
|
* description: "Updates products associated with a Product Collection"
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The id of the Collection.
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* properties:
|
||||||
|
* product_ids:
|
||||||
|
* description: "An array of Product IDs to add to the Product Collection."
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* description: "The ID of a Product to add to the Product Collection."
|
||||||
|
* type: string
|
||||||
|
* tags:
|
||||||
|
* - Collection
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const validated = await validator(AdminPostProductsToCollectionReq, req.body)
|
||||||
|
|
||||||
|
const productCollectionService: ProductCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const collection = await productCollectionService.addProducts(
|
||||||
|
id,
|
||||||
|
validated.product_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ collection })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminPostProductsToCollectionReq {
|
||||||
|
@ArrayNotEmpty()
|
||||||
|
@IsString({ each: true })
|
||||||
|
product_ids: string[]
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ export default (app) => {
|
|||||||
route.get("/:id", middlewares.wrap(require("./get-collection").default))
|
route.get("/:id", middlewares.wrap(require("./get-collection").default))
|
||||||
route.get("/", middlewares.wrap(require("./list-collections").default))
|
route.get("/", middlewares.wrap(require("./list-collections").default))
|
||||||
|
|
||||||
|
route.post("/:id/products/batch", middlewares.wrap(require("./add-products").default))
|
||||||
|
route.delete("/:id/products/batch", middlewares.wrap(require("./remove-products").default))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { ArrayNotEmpty, IsString } from "class-validator"
|
||||||
|
import ProductCollectionService from "../../../../services/product-collection"
|
||||||
|
import { validator } from "../../../../utils/validator"
|
||||||
|
/**
|
||||||
|
* @oas [delete] /collections/{id}/products/batch
|
||||||
|
* operationId: "DeleteProductsFromCollection"
|
||||||
|
* summary: "Removes products associated with a Product Collection"
|
||||||
|
* description: "Removes products associated with a Product Collection"
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The id of the Collection.
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* properties:
|
||||||
|
* product_ids:
|
||||||
|
* description: "An array of Product IDs to remove from the Product Collection."
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* description: "The ID of a Product to remove from the Product Collection."
|
||||||
|
* type: string
|
||||||
|
* tags:
|
||||||
|
* - Collection
|
||||||
|
* responses:
|
||||||
|
* "200":
|
||||||
|
* description: OK
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const validated = await validator(
|
||||||
|
AdminDeleteProductsFromCollectionReq,
|
||||||
|
req.body
|
||||||
|
)
|
||||||
|
|
||||||
|
const productCollectionService: ProductCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
|
||||||
|
await productCollectionService.removeProducts(id, validated.product_ids)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id,
|
||||||
|
object: "product-collection",
|
||||||
|
removed_products: validated.product_ids,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminDeleteProductsFromCollectionReq {
|
||||||
|
@ArrayNotEmpty()
|
||||||
|
@IsString({ each: true })
|
||||||
|
product_ids: string[]
|
||||||
|
}
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
|
import _ from "lodash"
|
||||||
import {
|
import {
|
||||||
Entity,
|
|
||||||
Index,
|
|
||||||
BeforeInsert,
|
BeforeInsert,
|
||||||
Column,
|
Column,
|
||||||
DeleteDateColumn,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
DeleteDateColumn,
|
||||||
PrimaryColumn,
|
Entity,
|
||||||
OneToOne,
|
Index,
|
||||||
OneToMany,
|
|
||||||
ManyToOne,
|
|
||||||
ManyToMany,
|
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { ulid } from "ulid"
|
import { ulid } from "ulid"
|
||||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||||
|
|
||||||
import { Image } from "./image"
|
import { Image } from "./image"
|
||||||
import { ProductCollection } from "./product-collection"
|
import { ProductCollection } from "./product-collection"
|
||||||
import { ProductOption } from "./product-option"
|
import { ProductOption } from "./product-option"
|
||||||
@@ -24,7 +23,6 @@ import { ProductTag } from "./product-tag"
|
|||||||
import { ProductType } from "./product-type"
|
import { ProductType } from "./product-type"
|
||||||
import { ProductVariant } from "./product-variant"
|
import { ProductVariant } from "./product-variant"
|
||||||
import { ShippingProfile } from "./shipping-profile"
|
import { ShippingProfile } from "./shipping-profile"
|
||||||
import _ from "lodash"
|
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
DRAFT = "draft",
|
DRAFT = "draft",
|
||||||
@@ -76,13 +74,13 @@ export class Product {
|
|||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => ProductOption,
|
() => ProductOption,
|
||||||
productOption => productOption.product
|
(productOption) => productOption.product
|
||||||
)
|
)
|
||||||
options: ProductOption[]
|
options: ProductOption[]
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => ProductVariant,
|
() => ProductVariant,
|
||||||
variant => variant.product,
|
(variant) => variant.product,
|
||||||
{ cascade: true }
|
{ cascade: true }
|
||||||
)
|
)
|
||||||
variants: ProductVariant[]
|
variants: ProductVariant[]
|
||||||
@@ -120,7 +118,7 @@ export class Product {
|
|||||||
material: string
|
material: string
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
collection_id: string
|
collection_id: string | null
|
||||||
|
|
||||||
@ManyToOne(() => ProductCollection)
|
@ManyToOne(() => ProductCollection)
|
||||||
@JoinColumn({ name: "collection_id" })
|
@JoinColumn({ name: "collection_id" })
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
EntityRepository,
|
EntityRepository,
|
||||||
FindManyOptions,
|
FindManyOptions,
|
||||||
FindOperator,
|
FindOperator,
|
||||||
|
In,
|
||||||
OrderByCondition,
|
OrderByCondition,
|
||||||
Repository,
|
Repository,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
@@ -76,7 +77,9 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
return [entities, count]
|
return [entities, count]
|
||||||
}
|
}
|
||||||
|
|
||||||
private getGroupedRelations(relations: Array<keyof Product>): {
|
private getGroupedRelations(
|
||||||
|
relations: Array<keyof Product>
|
||||||
|
): {
|
||||||
[toplevel: string]: string[]
|
[toplevel: string]: string[]
|
||||||
} {
|
} {
|
||||||
const groupedRelations: { [toplevel: string]: string[] } = {}
|
const groupedRelations: { [toplevel: string]: string[] } = {}
|
||||||
@@ -194,8 +197,9 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||||
const entitiesToReturn =
|
const entitiesToReturn = this.mergeEntitiesWithRelations(
|
||||||
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
entitiesAndRelations
|
||||||
|
)
|
||||||
|
|
||||||
return [entitiesToReturn, count]
|
return [entitiesToReturn, count]
|
||||||
}
|
}
|
||||||
@@ -236,8 +240,9 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||||
const entitiesToReturn =
|
const entitiesToReturn = this.mergeEntitiesWithRelations(
|
||||||
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
entitiesAndRelations
|
||||||
|
)
|
||||||
|
|
||||||
return entitiesToReturn
|
return entitiesToReturn
|
||||||
}
|
}
|
||||||
@@ -255,4 +260,30 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
)
|
)
|
||||||
return result[0]
|
return result[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async bulkAddToCollection(
|
||||||
|
productIds: string[],
|
||||||
|
collectionId: string
|
||||||
|
): Promise<Product[]> {
|
||||||
|
await this.createQueryBuilder()
|
||||||
|
.update(Product)
|
||||||
|
.set({ collection_id: collectionId })
|
||||||
|
.where({ id: In(productIds) })
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
return this.findByIds(productIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async bulkRemoveFromCollection(
|
||||||
|
productIds: string[],
|
||||||
|
collectionId: string
|
||||||
|
): Promise<Product[]> {
|
||||||
|
await this.createQueryBuilder()
|
||||||
|
.update(Product)
|
||||||
|
.set({ collection_id: null })
|
||||||
|
.where({ id: In(productIds), collection_id: collectionId })
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
return this.findByIds(productIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
export const ProductCollectionServiceMock = {
|
export const ProductCollectionServiceMock = {
|
||||||
withTransaction: function () {
|
withTransaction: function() {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
create: jest.fn().mockImplementation((data) => {
|
create: jest.fn().mockImplementation((data) => {
|
||||||
@@ -16,6 +16,16 @@ export const ProductCollectionServiceMock = {
|
|||||||
update: jest.fn().mockImplementation((id, value) => {
|
update: jest.fn().mockImplementation((id, value) => {
|
||||||
return Promise.resolve({ id, title: value })
|
return Promise.resolve({ id, title: value })
|
||||||
}),
|
}),
|
||||||
|
addProducts: jest.fn().mockImplementation((id, product_ids) => {
|
||||||
|
if (id === IdMap.getId("col")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
id,
|
||||||
|
products: product_ids.map((i) => ({ id: i })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw new Error("Product collection not found")
|
||||||
|
}),
|
||||||
|
removeProducts: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
list: jest.fn().mockImplementation((data) => {
|
list: jest.fn().mockImplementation((data) => {
|
||||||
return Promise.resolve([{ id: IdMap.getId("col"), title: "Suits" }])
|
return Promise.resolve([{ id: IdMap.getId("col"), title: "Suits" }])
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -162,6 +162,32 @@ class ProductCollectionService extends BaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addProducts(collectionId, productIds) {
|
||||||
|
return this.atomicPhase_(async (manager) => {
|
||||||
|
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||||
|
|
||||||
|
const { id } = await this.retrieve(collectionId, { select: ["id"] })
|
||||||
|
|
||||||
|
await productRepo.bulkAddToCollection(productIds, id)
|
||||||
|
|
||||||
|
return await this.retrieve(id, {
|
||||||
|
relations: ["products"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeProducts(collectionId, productIds) {
|
||||||
|
return this.atomicPhase_(async (manager) => {
|
||||||
|
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||||
|
|
||||||
|
const { id } = await this.retrieve(collectionId, { select: ["id"] })
|
||||||
|
|
||||||
|
await productRepo.bulkRemoveFromCollection(productIds, id)
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists product collections
|
* Lists product collections
|
||||||
* @param {Object} selector - the query object for find
|
* @param {Object} selector - the query object for find
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseService } from "medusa-interfaces"
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles swaps
|
* Handles swaps
|
||||||
|
|||||||
Reference in new issue
Block a user