diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index 4cd2e9683e..8fc7342918 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -49,6 +49,7 @@ Array [ ], "origin_country": null, "profile_id": StringMatching /\\^sp_\\*/, + "status": "draft", "subtitle": null, "tags": Array [ Object { @@ -252,6 +253,7 @@ Array [ "options": Array [], "origin_country": null, "profile_id": StringMatching /\\^sp_\\*/, + "status": "draft", "subtitle": null, "tags": Array [ Object { diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 9c17d99cf1..1e398b65c5 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -42,6 +42,73 @@ describe("/admin/products", () => { await db.teardown() }) + it("returns a list of products with all statuses when no status or invalid status is provided", async () => { + const api = useApi() + + const res = await api + .get("/admin/products?status%5B%5D=null", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(res.status).toEqual(200) + expect(res.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product", + status: "draft", + }), + expect.objectContaining({ + id: "test-product1", + status: "draft", + }), + ]) + ) + }) + + it("returns a list of products where status is proposed", async () => { + const api = useApi() + + const payload = { + status: "proposed", + } + + //update test-product status to proposed + await api + .post("/admin/products/test-product", payload, { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + const response = await api + .get("/admin/products?status%5B%5D=proposed", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product", + status: "proposed", + }), + ]) + ) + }) + it("returns a list of products with child entities", async () => { const api = useApi() @@ -297,6 +364,7 @@ describe("/admin/products", () => { discountable: true, is_giftcard: false, handle: "test", + status: "draft", images: expect.arrayContaining([ expect.objectContaining({ url: "test-image.png", @@ -455,7 +523,7 @@ describe("/admin/products", () => { ) }) - it("updates a product (update prices, tags, delete collection, delete type, replaces images)", async () => { + it("updates a product (update prices, tags, update status, delete collection, delete type, replaces images)", async () => { const api = useApi() const payload = { @@ -476,6 +544,7 @@ describe("/admin/products", () => { tags: [{ value: "123" }], images: ["test-image-2.png"], type: { value: "test-type-2" }, + status: "published", } const response = await api @@ -514,6 +583,7 @@ describe("/admin/products", () => { }), ], type: null, + status: "published", collection: null, type: expect.objectContaining({ value: "test-type-2", @@ -522,6 +592,25 @@ describe("/admin/products", () => { ) }) + it("fails to update product with invalid status", async () => { + const api = useApi() + + const payload = { + status: null, + } + + try { + await api.post("/admin/products/test-product", payload, { + headers: { + Authorization: "Bearer test_token", + }, + }) + } catch (e) { + expect(e.response.status).toEqual(400) + expect(e.response.data.type).toEqual("invalid_data") + } + }) + it("updates a product (variant ordering)", async () => { const api = useApi() diff --git a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap index 436c7e60cc..781d540f96 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap @@ -101,6 +101,7 @@ Object { ], "origin_country": null, "profile_id": StringMatching /\\^sp_\\*/, + "status": "draft", "subtitle": null, "tags": Array [ Object { diff --git a/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap b/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap index 5f3522721f..fee312ff37 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/swaps.js.snap @@ -58,6 +58,7 @@ Object { "mid_code": null, "origin_country": null, "profile_id": StringMatching /\\^sp_\\*/, + "status": "draft", "subtitle": null, "thumbnail": null, "title": "test product", @@ -228,6 +229,7 @@ Object { "mid_code": null, "origin_country": null, "profile_id": StringMatching /\\^sp_\\*/, + "status": "draft", "subtitle": null, "thumbnail": null, "title": "test product", diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index b3225c289c..22f35079d4 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -5,6 +5,7 @@ const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") const productSeeder = require("../../helpers/product-seeder") +const adminSeeder = require("../../helpers/admin-seeder") jest.setTimeout(30000) describe("/store/products", () => { let medusaProcess @@ -26,6 +27,7 @@ describe("/store/products", () => { beforeEach(async () => { try { await productSeeder(dbConnection) + await adminSeeder(dbConnection) } catch (err) { console.log(err) throw err @@ -261,5 +263,38 @@ describe("/store/products", () => { expect(product.variants.some((variant) => variant.options)).toEqual(false) }) + + it("lists all published products", async () => { + const api = useApi() + + //update test-product status to published + await api + .post( + "/admin/products/test-product", + { + status: "published", + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) + + const response = await api.get("/store/products") + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product", + status: "published", + }), + ]) + ) + }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js index 25a9012395..f7d9722bd5 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js @@ -110,6 +110,7 @@ describe("POST /admin/products", () => { description: "Test Description", tags: [{ id: "test", value: "test" }], handle: "test-product", + status: "draft", is_giftcard: false, options: [{ title: "Denominations" }], profile_id: IdMap.getId("default_shipping_profile"), @@ -170,6 +171,7 @@ describe("POST /admin/products", () => { options: [{ title: "Denominations" }], handle: "test-gift-card", is_giftcard: true, + status: "draft", profile_id: IdMap.getId("giftCardProfile"), }) }) diff --git a/packages/medusa/src/api/routes/admin/products/create-product.js b/packages/medusa/src/api/routes/admin/products/create-product.js index 95c5f4b3b7..d02ac8fb7d 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/create-product.js @@ -193,6 +193,9 @@ export default async (req, res) => { .optional(), thumbnail: Validator.string().optional(), handle: Validator.string().optional(), + status: Validator.string() + .valid("proposed", "draft", "published", "rejected") + .default("draft"), type: Validator.object() .keys({ id: Validator.string().optional(), diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js index 8c0ab5207d..bd4873a82d 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -1,4 +1,5 @@ import _ from "lodash" +import { MedusaError, Validator } from "medusa-core-utils" import { defaultFields, defaultRelations } from "./" /** @@ -56,6 +57,20 @@ export default async (req, res) => { selector.is_giftcard = req.query.is_giftcard === "true" } + if ("status" in req.query) { + const schema = Validator.array() + .items( + Validator.string().valid("proposed", "draft", "published", "rejected") + ) + .single() + + const { value, error } = schema.validate(req.query.status) + + if (value && !error) { + selector.status = value + } + } + const listConfig = { select: includeFields.length ? includeFields : defaultFields, relations: expandFields.length ? expandFields : defaultRelations, diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js index 71a2fe2cc2..85b8f72559 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.js +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -193,6 +193,12 @@ export default async (req, res) => { .allow(null, ""), description: Validator.string().optional(), discountable: Validator.boolean().optional(), + status: Validator.string().valid( + "proposed", + "draft", + "published", + "rejected" + ), type: Validator.object() .keys({ id: Validator.string().optional(), diff --git a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js index 647e29d281..8e044bd193 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js @@ -18,7 +18,7 @@ describe("GET /store/products", () => { it("calls get product from productSerice", () => { expect(ProductServiceMock.list).toHaveBeenCalledTimes(1) expect(ProductServiceMock.list).toHaveBeenCalledWith( - {}, + { status: ["published"] }, { relations: defaultRelations, skip: 0, take: 100 } ) }) @@ -43,7 +43,7 @@ describe("GET /store/products", () => { it("calls list from productSerice", () => { expect(ProductServiceMock.list).toHaveBeenCalledTimes(1) expect(ProductServiceMock.list).toHaveBeenCalledWith( - { is_giftcard: true }, + { is_giftcard: true, status: ["published"] }, { relations: defaultRelations, skip: 0, take: 100 } ) }) diff --git a/packages/medusa/src/api/routes/store/products/list-products.js b/packages/medusa/src/api/routes/store/products/list-products.js index 44b024fe71..fadbce82de 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.js +++ b/packages/medusa/src/api/routes/store/products/list-products.js @@ -1,3 +1,4 @@ +import { MedusaError, Validator } from "medusa-core-utils" import { defaultRelations } from "." /** @@ -41,6 +42,8 @@ export default async (req, res) => { selector.is_giftcard = req.query.is_giftcard === "true" } + selector.status = ["published"] + const listConfig = { relations: defaultRelations, skip: offset, diff --git a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js index cebdee0f60..890ab5e078 100644 --- a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js +++ b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js @@ -28,10 +28,7 @@ describe("Get variant by id", () => { describe("get variant with prices", () => { let subject beforeAll(async () => { - subject = await request( - "GET", - `/store/variants/${IdMap.getId("variantWithPrices")}` - ) + subject = await request("GET", `/store/variants/variant_with_prices`) }) it("successfully retrieves variants with prices", async () => { expect(subject.status).toEqual(200) diff --git a/packages/medusa/src/migrations/1631864388026-status_on_product.ts b/packages/medusa/src/migrations/1631864388026-status_on_product.ts new file mode 100644 index 0000000000..1a54b5eb77 --- /dev/null +++ b/packages/medusa/src/migrations/1631864388026-status_on_product.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class statusOnProduct1631864388026 implements MigrationInterface { + name = "statusOnProduct1631864388026" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "product_status_enum" AS ENUM('draft', 'proposed', 'published', 'rejected')` + ) + await queryRunner.query( + `ALTER TABLE "product" ADD "status" "product_status_enum" ` + ) + await queryRunner.query( + `UPDATE "product" SET "status" = 'published' WHERE "status" IS NULL` + ) + await queryRunner.query( + `ALTER TABLE "product" ALTER COLUMN "status" SET NOT NULL` + ) + await queryRunner.query( + `ALTER TABLE "product" ALTER COLUMN "status" SET DEFAULT 'draft'` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "status"`) + await queryRunner.query(`DROP TYPE "product_status_enum"`) + } +} diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index c1ff8d713f..062912b2a5 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -26,6 +26,13 @@ import { ProductVariant } from "./product-variant" import { ShippingProfile } from "./shipping-profile" import _ from "lodash" +export enum Status { + DRAFT = "draft", + PROPOSED = "proposed", + PUBLISHED = "published", + REJECTED = "rejected", +} + @Entity() export class Product { @PrimaryColumn() @@ -47,6 +54,9 @@ export class Product { @Column({ default: false }) is_giftcard: boolean + @DbAwareColumn({ type: "enum", enum: Status, default: "draft" }) + status: Status + @ManyToMany(() => Image, { cascade: ["insert"] }) @JoinTable({ name: "product_images", diff --git a/packages/medusa/src/services/__mocks__/product-variant.js b/packages/medusa/src/services/__mocks__/product-variant.js index 2b84bdbb34..7576cbae13 100644 --- a/packages/medusa/src/services/__mocks__/product-variant.js +++ b/packages/medusa/src/services/__mocks__/product-variant.js @@ -223,7 +223,7 @@ export const ProductVariantServiceMock = { if (variantId === "4") { return Promise.resolve(variant4) } - if (variantId === IdMap.getId("variantWithPrices")) { + if (variantId === "variant_with_prices") { return Promise.resolve(variantWithPrices) } if (variantId === IdMap.getId("validId")) { diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index a8fe0463d4..af1c0ae7cb 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -199,6 +199,12 @@ describe("ProductService", () => { variants: [{ id: IdMap.getId("green"), title: "Green" }], }) } + if (query.where.id === "prod_status") { + return Promise.resolve({ + id: "prod_status", + status: "draft", + }) + } if (query.where.id === "123") { return undefined } @@ -290,6 +296,18 @@ describe("ProductService", () => { expect(productRepository.save).toHaveBeenCalledTimes(1) }) + it("successfully updates product status", async () => { + await productService.update(IdMap.getId("ironman"), { + status: "published", + }) + + expect(productRepository.save).toHaveBeenCalledTimes(1) + expect(productRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("ironman"), + status: "published", + }) + }) + it("successfully updates product", async () => { await productService.update(IdMap.getId("ironman"), { title: "Full suit", diff --git a/packages/medusa/yarn.lock b/packages/medusa/yarn.lock index 35c3432b2e..df967251e9 100644 --- a/packages/medusa/yarn.lock +++ b/packages/medusa/yarn.lock @@ -1407,10 +1407,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@medusajs/medusa-cli@1.1.16-dev-1631019393655": - version "1.1.16-dev-1631019393655" - resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.16-dev-1631019393655.tgz#68fc30f7053df428cd7a4e5a443326371818a20d" - integrity sha512-5wUVfTJahuHBbY4U5iuwSKbE/Dn3S/fC8MWVpP5R/JFNK484zR3On1qBL0I9tXtsUd9CmB+jKI59w6D49wJ29A== +"@medusajs/medusa-cli@^1.1.17": + version "1.1.18" + resolved "https://registry.yarnpkg.com/@medusajs/medusa-cli/-/medusa-cli-1.1.18.tgz#a2b34575a81a7df239d6d06cf0d0b192e2b8c8db" + integrity sha512-JEvQVjebaGuOF5BsqjZYnewmU4TPbrnhODKVyadPKPb/cxPcCMODg21d5QyoaVlcXood08LgTFe8CfdWoyubVw== dependencies: "@babel/polyfill" "^7.8.7" "@babel/runtime" "^7.9.6" @@ -1428,8 +1428,8 @@ is-valid-path "^0.1.1" joi-objectid "^3.0.1" meant "^1.0.1" - medusa-core-utils "1.1.20-dev-1631019393655" - medusa-telemetry "0.0.3-dev-1631019393655" + medusa-core-utils "^0.1.27" + medusa-telemetry "^0.0.5" netrc-parser "^3.1.6" open "^8.0.6" ora "^5.4.1" @@ -2046,10 +2046,10 @@ babel-preset-jest@^25.5.0: babel-plugin-jest-hoist "^25.5.0" babel-preset-current-node-syntax "^0.1.2" -babel-preset-medusa-package@1.1.13-dev-1631019393655: - version "1.1.13-dev-1631019393655" - resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.13-dev-1631019393655.tgz#95b20e00ae34b6b1d5a3be2dbd17c4bf45c1895e" - integrity sha512-cpaVSi2+M8LFlZjfcazmp1GHB+lEgDNZNlX8J9RdB5/LWZwWRrqFGVyv//8hXp7AKiFFTtayJmpgBAEFfLhdcA== +babel-preset-medusa-package@^1.1.14: + version "1.1.15" + resolved "https://registry.yarnpkg.com/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.15.tgz#6917cadd8abe9a1f64c71b5c43ab507df193effc" + integrity sha512-toA8mFdvLeKbbRJ7KvQvpL6VJnzkKURZv7Yd97cXMMNpdjrhp+SZppcNOL2tk6ywgBAs4NC2LCVjtZInMMBS6Q== dependencies: "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-decorators" "^7.12.1" @@ -5454,25 +5454,33 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@1.1.20-dev-1631019393655: - version "1.1.20-dev-1631019393655" - resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.20-dev-1631019393655.tgz#edd5e25518677799647eef32ccc8ddb4f244026e" - integrity sha512-Ko+gjpe4pHwQMI6Gv4MxMlzWbQbCL9bnN8MX5eHwLJoGcPzt57y5/RWujJo99nUuNthUDiJ14EFXQHVQu45bmQ== +medusa-core-utils@^0.1.27: + version "0.1.39" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df" + integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ== + dependencies: + "@hapi/joi" "^16.1.8" + joi-objectid "^3.0.1" + +medusa-core-utils@^1.1.21, medusa-core-utils@^1.1.22: + version "1.1.22" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.22.tgz#84ce0af0a7c672191d758ea462056e30a39d08b1" + integrity sha512-kMuRkWOuNG4Bw6epg/AYu95UJuE+rjHTeTWRLbEPrYGjWREV82tLWVDI21/QcccmaHmMU98Rkw2z9JwyFZIiyw== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" -medusa-interfaces@1.1.21-dev-1631019393655: - version "1.1.21-dev-1631019393655" - resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.21-dev-1631019393655.tgz#c2a72b6751a802438ed0f9010ed0cfecff1d1a2d" - integrity sha512-I8BEdvmVKorakTU5myw8IyTLp9OcO40OkNdZmZj29Q4oRU4scwP3i+5bXd6xW0ML5t5hbMUT9TKU9n7oZNIvug== +medusa-interfaces@^1.1.22: + version "1.1.23" + resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.23.tgz#b552a8c1d0eaddeff30472ab238652b9e1a56e73" + integrity sha512-dHCOnsyYQvjrtRd3p0ZqQZ4M/zmo4M/BAgVfRrYSyGrMdQ86TK9Z1DQDCHEzM1216AxEfXz2JYUD7ilTfG2iHQ== dependencies: - medusa-core-utils "1.1.20-dev-1631019393655" + medusa-core-utils "^1.1.22" -medusa-telemetry@0.0.3-dev-1631019393655: - version "0.0.3-dev-1631019393655" - resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.3-dev-1631019393655.tgz#dc6abef74c631520d4d8c81a4175ac795d72fbda" - integrity sha512-I8a8iTTmL0u5/SYnTw02xn3JiFa2PDclRyDL5pAl8gFgCzVCWD+fm+ik51kMMhbNhK7GWobs4h5xxA4W6eCcAQ== +medusa-telemetry@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/medusa-telemetry/-/medusa-telemetry-0.0.5.tgz#d7d08fca5cbecc0e853b4e0406194a92c5206caa" + integrity sha512-h7hP5Lc33OkFhMcvfrPcwINzMOuPoG8Vn8O6niKGFxF9RmmQnJgaAG1J43/Eq9ZWBrWi0n42XBttibKwCMViHw== dependencies: axios "^0.21.1" axios-retry "^3.1.9" @@ -5484,13 +5492,13 @@ medusa-telemetry@0.0.3-dev-1631019393655: remove-trailing-slash "^0.1.1" uuid "^8.3.2" -medusa-test-utils@1.1.23-dev-1631019393655: - version "1.1.23-dev-1631019393655" - resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.23-dev-1631019393655.tgz#1daf3913cfe8a9586863e4b80dabbb7344ebba48" - integrity sha512-5qsKVr2whQi03lGPmy2+/tAD1KuFsqrAcaaEp+u42Jjg6acDV1rrcTgFLj+lplRBOC2qFzMEU0cBFuGKNmYhpg== +medusa-test-utils@^1.1.24: + version "1.1.25" + resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.25.tgz#7c4aa8a70ec8a95875304258ffbe7493a1e5a7fc" + integrity sha512-4xy20KsZBR1XcuzckGRq9A+GJwh+CFHzVw3dajaO4iiNpL/a9K3Yj2N4f/8BgRcQyw5PnkKGJ0pzv+OR8+5GVw== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "1.1.20-dev-1631019393655" + medusa-core-utils "^1.1.22" randomatic "^3.1.1" merge-descriptors@1.0.1: