diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index c7ec540ae1..7dbf3ac288 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -32,6 +32,50 @@ let { jest.setTimeout(50000) +const getProductFixture = () => ({ + title: "Test fixture", + description: "test-product-description", + images: breaking( + () => ["test-image.png", "test-image-2.png"], + () => [{ url: "test-image.png" }, { url: "test-image-2.png" }] + ), + tags: [{ value: "123" }, { value: "456" }], + options: breaking( + () => [{ title: "size" }, { title: "color" }], + () => [ + { title: "size", values: ["large"] }, + { title: "color", values: ["green"] }, + ] + ), + variants: [ + { + title: "Test variant", + inventory_quantity: 10, + prices: [ + { + currency_code: "usd", + amount: 100, + }, + { + currency_code: "eur", + amount: 45, + }, + { + currency_code: "dkk", + amount: 30, + }, + ], + options: breaking( + () => [{ value: "large" }, { value: "green" }], + () => ({ + size: "large", + color: "green", + }) + ), + }, + ], +}) + medusaIntegrationTestRunner({ env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }, testSuite: ({ dbConnection, getContainer, api }) => { @@ -41,7 +85,6 @@ medusaIntegrationTestRunner({ let scService let remoteLink let container - let productFixture beforeAll(() => { // Note: We have to lazily load everything because there are weird ordering issues when doing `require` of `@medusajs/medusa` @@ -69,54 +112,12 @@ medusaIntegrationTestRunner({ container = getContainer() await createAdminUser(dbConnection, adminHeaders, container) - productFixture = { - title: "Test fixture", - description: "test-product-description", - type: { value: "test-type" }, - images: ["test-image.png", "test-image-2.png"], - tags: [{ value: "123" }, { value: "456" }], - options: breaking( - () => [{ title: "size" }, { title: "color" }], - () => [ - { title: "size", values: ["large"] }, - { title: "color", values: ["green"] }, - ] - ), - variants: [ - { - title: "Test variant", - inventory_quantity: 10, - prices: [ - { - currency_code: "usd", - amount: 100, - }, - { - currency_code: "eur", - amount: 45, - }, - { - currency_code: "dkk", - amount: 30, - }, - ], - options: breaking( - () => [{ value: "large" }, { value: "green" }], - () => ({ - size: "large", - color: "green", - }) - ), - }, - ], - } - // We want to seed another product for v2 that has pricing correctly wired up for all pricing-related tests. v2Product = ( await breaking( async () => ({}), async () => - await api.post("/admin/products", productFixture, adminHeaders) + await api.post("/admin/products", getProductFixture(), adminHeaders) ) )?.data?.product @@ -475,13 +476,13 @@ medusaIntegrationTestRunner({ expect.objectContaining({ id: breaking( () => "test-price_4", - () => expect.stringMatching(/^ma_*/) + () => expect.stringMatching(/^price_*/) ), }), expect.objectContaining({ id: breaking( () => "test-price_3", - () => expect.stringMatching(/^ma_*/) + () => expect.stringMatching(/^price_*/) ), }), ]) @@ -1398,7 +1399,11 @@ medusaIntegrationTestRunner({ .post( "/admin/products", { - ...productFixture, + ...getProductFixture(), + ...breaking( + () => ({ type: { value: "test-type" } }), + () => ({ type_id: "test-type" }) + ), title: "Test create", collection_id: "test-collection", }, @@ -1408,6 +1413,11 @@ medusaIntegrationTestRunner({ console.log(err) }) + const priceIdSelector = breaking( + () => /^ma_*/, + () => /^price_*/ + ) + // TODO: It seems we end up with this recursive nested population (product -> variant -> product) that we need to get rid of expect(response.status).toEqual(200) expect(response.data.product).toEqual( @@ -1502,7 +1512,7 @@ medusaIntegrationTestRunner({ title: "Test variant", prices: expect.arrayContaining([ expect.objectContaining({ - id: expect.stringMatching(/^ma_*/), + id: expect.stringMatching(priceIdSelector), currency_code: "usd", amount: 100, created_at: expect.any(String), @@ -1510,7 +1520,7 @@ medusaIntegrationTestRunner({ variant_id: expect.stringMatching(/^variant_*/), }), expect.objectContaining({ - id: expect.stringMatching(/^ma_*/), + id: expect.stringMatching(priceIdSelector), currency_code: "eur", amount: 45, created_at: expect.any(String), @@ -1518,7 +1528,7 @@ medusaIntegrationTestRunner({ variant_id: expect.stringMatching(/^variant_*/), }), expect.objectContaining({ - id: expect.stringMatching(/^ma_*/), + id: expect.stringMatching(priceIdSelector), currency_code: "dkk", amount: 30, created_at: expect.any(String), @@ -1579,8 +1589,10 @@ medusaIntegrationTestRunner({ title: "Test", discountable: false, description: "test-product-description", - type: { value: "test-type" }, - images: ["test-image.png", "test-image-2.png"], + images: breaking( + () => ["test-image.png", "test-image-2.png"], + () => [{ url: "test-image.png" }, { url: "test-image-2.png" }] + ), collection_id: "test-collection", tags: [{ value: "123" }, { value: "456" }], variants: [ @@ -1610,8 +1622,10 @@ medusaIntegrationTestRunner({ const payload = { title: "Test product - 1", description: "test-product-description 1", - type: { value: "test-type 1" }, - images: ["test-image.png", "test-image-2.png"], + images: breaking( + () => ["test-image.png", "test-image-2.png"], + () => [{ url: "test-image.png" }, { url: "test-image-2.png" }] + ), collection_id: "test-collection", tags: [{ value: "123" }, { value: "456" }], variants: [ @@ -1706,8 +1720,10 @@ medusaIntegrationTestRunner({ }, ], tags: [{ value: "123" }], - images: ["test-image-2.png"], - type: { value: "test-type-2" }, + images: breaking( + () => ["test-image-2.png"], + () => [{ url: "test-image-2.png" }] + ), status: "published", } @@ -1771,8 +1787,7 @@ medusaIntegrationTestRunner({ updated_at: expect.any(String), value: "test-type-2", }), - // TODO: For some reason this is `test-type`, but the ID is correct in the `type` property. - // type_id: expect.stringMatching(/^ptyp_*/), + type_id: expect.stringMatching(/^ptyp_*/), updated_at: expect.any(String), variants: expect.arrayContaining([ expect.objectContaining({ @@ -2929,8 +2944,10 @@ medusaIntegrationTestRunner({ title: "Test product", handle: "test-product", description: "test-product-description", - type: { value: "test-type" }, - images: ["test-image.png", "test-image-2.png"], + images: breaking( + () => ["test-image.png", "test-image-2.png"], + () => [{ url: "test-image.png" }, { url: "test-image-2.png" }] + ), collection_id: "test-collection", tags: [{ value: "123" }, { value: "456" }], variants: [ @@ -2964,8 +2981,10 @@ medusaIntegrationTestRunner({ title: "Test product", handle: "test-product", description: "test-product-description", - type: { value: "test-type" }, - images: ["test-image.png", "test-image-2.png"], + images: breaking( + () => ["test-image.png", "test-image-2.png"], + () => [{ url: "test-image.png" }, { url: "test-image-2.png" }] + ), collection_id: "test-collection", tags: [{ value: "123" }, { value: "456" }], variants: [ diff --git a/integration-tests/factories/simple-product-variant-factory.ts b/integration-tests/factories/simple-product-variant-factory.ts index 6cb444a3bb..bf4f5e8ba6 100644 --- a/integration-tests/factories/simple-product-variant-factory.ts +++ b/integration-tests/factories/simple-product-variant-factory.ts @@ -7,6 +7,7 @@ import { import { DataSource } from "typeorm" import faker from "faker" +import { breaking } from "../helpers/breaking" export type ProductVariantFactoryData = { product_id: string @@ -62,22 +63,24 @@ export const simpleProductVariantFactory = async ( }) } - const prices = data.prices || [{ currency: "usd", amount: 100 }] - for (const p of prices) { - const ma_id = `${p.currency}-${p.amount}-${Math.random()}` - await manager.insert(MoneyAmount, { - id: ma_id, - currency_code: p.currency, - amount: p.amount, - region_id: p.region_id, - }) + await breaking(async () => { + const prices = data.prices || [{ currency: "usd", amount: 100 }] + for (const p of prices) { + const ma_id = `${p.currency}-${p.amount}-${Math.random()}` + await manager.insert(MoneyAmount, { + id: ma_id, + currency_code: p.currency, + amount: p.amount, + region_id: p.region_id, + }) - await manager.insert(ProductVariantMoneyAmount, { - id: `${ma_id}-${id}-${Math.random()}`, - money_amount_id: ma_id, - variant_id: id, - }) - } + await manager.insert(ProductVariantMoneyAmount, { + id: `${ma_id}-${id}-${Math.random()}`, + money_amount_id: ma_id, + variant_id: id, + }) + } + }) return variant } diff --git a/integration-tests/helpers/product-seeder.js b/integration-tests/helpers/product-seeder.js index 0a1ace9085..02bc96a0f0 100644 --- a/integration-tests/helpers/product-seeder.js +++ b/integration-tests/helpers/product-seeder.js @@ -12,6 +12,7 @@ const { MoneyAmount, ProductVariantMoneyAmount, } = require("@medusajs/medusa") +const { breaking } = require("./breaking") module.exports = async (dataSource, data = {}) => { const manager = dataSource.manager @@ -141,17 +142,22 @@ module.exports = async (dataSource, data = {}) => { await manager.save(variant1) - const ma = await manager.insert(MoneyAmount, { - id: "test-price", - currency_code: "usd", - amount: 100, - }) + await breaking( + async () => { + const ma = await manager.insert(MoneyAmount, { + id: "test-price", + currency_code: "usd", + amount: 100, + }) - await manager.insert(ProductVariantMoneyAmount, { - id: "pvma0", - money_amount_id: "test-price", - variant_id: "test-variant", - }) + await manager.insert(ProductVariantMoneyAmount, { + id: "pvma0", + money_amount_id: "test-price", + variant_id: "test-variant", + }) + }, + () => {} + ) const sale = manager.create(ProductVariant, { id: "test-variant-sale", @@ -174,17 +180,22 @@ module.exports = async (dataSource, data = {}) => { await manager.save(sale) - const ma_sale = await manager.insert(MoneyAmount, { - id: "test-price-sale", - currency_code: "usd", - amount: 1000, - }) + await breaking( + async () => { + const ma_sale = await manager.insert(MoneyAmount, { + id: "test-price-sale", + currency_code: "usd", + amount: 1000, + }) - await manager.insert(ProductVariantMoneyAmount, { - id: "pvma1", - money_amount_id: "test-price-sale", - variant_id: "test-variant-sale", - }) + await manager.insert(ProductVariantMoneyAmount, { + id: "pvma1", + money_amount_id: "test-price-sale", + variant_id: "test-variant-sale", + }) + }, + () => {} + ) const variant2 = manager.create(ProductVariant, { id: "test-variant_1", @@ -207,17 +218,22 @@ module.exports = async (dataSource, data = {}) => { await manager.save(variant2) - const ma_1 = await manager.insert(MoneyAmount, { - id: "test-price_1", - currency_code: "usd", - amount: 1000, - }) + await breaking( + async () => { + const ma_1 = await manager.insert(MoneyAmount, { + id: "test-price_1", + currency_code: "usd", + amount: 1000, + }) - await manager.insert(ProductVariantMoneyAmount, { - id: "pvma2", - money_amount_id: "test-price_1", - variant_id: "test-variant_1", - }) + await manager.insert(ProductVariantMoneyAmount, { + id: "pvma2", + money_amount_id: "test-price_1", + variant_id: "test-variant_1", + }) + }, + () => {} + ) const variant3 = manager.create(ProductVariant, { id: "test-variant_2", @@ -239,17 +255,22 @@ module.exports = async (dataSource, data = {}) => { await manager.save(variant3) - const ma_2 = await manager.insert(MoneyAmount, { - id: "test-price_2", - currency_code: "usd", - amount: 100, - }) + await breaking( + async () => { + const ma_2 = await manager.insert(MoneyAmount, { + id: "test-price_2", + currency_code: "usd", + amount: 100, + }) - await manager.insert(ProductVariantMoneyAmount, { - id: "pvma3", - money_amount_id: "test-price_2", - variant_id: "test-variant_2", - }) + await manager.insert(ProductVariantMoneyAmount, { + id: "pvma3", + money_amount_id: "test-price_2", + variant_id: "test-variant_2", + }) + }, + () => {} + ) const p1 = manager.create(Product, { id: "test-product1", @@ -288,18 +309,23 @@ module.exports = async (dataSource, data = {}) => { await manager.save(variant4) - const ma_3 = await manager.insert(MoneyAmount, { - id: "test-price_3", - currency_code: "usd", - amount: 100, - region_id: "test-region", - }) + await breaking( + async () => { + const ma_3 = await manager.insert(MoneyAmount, { + id: "test-price_3", + currency_code: "usd", + amount: 100, + region_id: "test-region", + }) - await manager.insert(ProductVariantMoneyAmount, { - id: "pvma4", - money_amount_id: "test-price_3", - variant_id: "test-variant_3", - }) + await manager.insert(ProductVariantMoneyAmount, { + id: "pvma4", + money_amount_id: "test-price_3", + variant_id: "test-variant_3", + }) + }, + () => {} + ) const variant5 = manager.create(ProductVariant, { id: "test-variant_4", @@ -321,17 +347,22 @@ module.exports = async (dataSource, data = {}) => { await manager.save(variant5) - const ma_4 = await manager.insert(MoneyAmount, { - id: "test-price_4", - currency_code: "usd", - amount: 100, - }) + await breaking( + async () => { + const ma_4 = await manager.insert(MoneyAmount, { + id: "test-price_4", + currency_code: "usd", + amount: 100, + }) - await manager.insert(ProductVariantMoneyAmount, { - id: "pvma5", - money_amount_id: "test-price_4", - variant_id: "test-variant_4", - }) + await manager.insert(ProductVariantMoneyAmount, { + id: "pvma5", + money_amount_id: "test-price_4", + variant_id: "test-variant_4", + }) + }, + () => {} + ) const product1 = manager.create(Product, { id: "test-product_filtering_1", diff --git a/packages/core-flows/src/definition/cart/utils/prepare-line-item-data.ts b/packages/core-flows/src/definition/cart/utils/prepare-line-item-data.ts index 33313f0992..8e563e6a41 100644 --- a/packages/core-flows/src/definition/cart/utils/prepare-line-item-data.ts +++ b/packages/core-flows/src/definition/cart/utils/prepare-line-item-data.ts @@ -11,6 +11,10 @@ interface Input { export function prepareLineItemData(data: Input) { const { variant, unitPrice, quantity, metadata, cartId } = data + if (!variant.product) { + throw new Error("Variant does not have a product") + } + const lineItem: any = { quantity, title: variant.title, diff --git a/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts b/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts index 92bb2eca19..c724d9788d 100644 --- a/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts +++ b/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts @@ -75,13 +75,13 @@ export async function updateProductVariantsPrepareData({ } const variantsData: ProductWorkflow.UpdateProductVariantsInputDTO[] = - productVariantsMap.get(variantWithProductID.product_id) || [] + productVariantsMap.get(variantWithProductID.product_id!) || [] if (variantData) { variantsData.push(variantData) } - productVariantsMap.set(variantWithProductID.product_id, variantsData) + productVariantsMap.set(variantWithProductID.product_id!, variantsData) } return { diff --git a/packages/core-flows/src/product/steps/update-product-options.ts b/packages/core-flows/src/product/steps/update-product-options.ts index d78ac578c2..573d49ed11 100644 --- a/packages/core-flows/src/product/steps/update-product-options.ts +++ b/packages/core-flows/src/product/steps/update-product-options.ts @@ -40,6 +40,13 @@ export const updateProductOptionsStep = createStep( ModuleRegistrationName.PRODUCT ) - await service.upsertOptions(prevData) + await service.upsertOptions( + prevData.map((o) => ({ + ...o, + values: o.values?.map((v) => v.value), + product: undefined, + product_id: o.product_id ?? undefined, + })) + ) } ) diff --git a/packages/core-flows/src/product/workflows/create-products.ts b/packages/core-flows/src/product/workflows/create-products.ts index 81e759f054..54f9a765c2 100644 --- a/packages/core-flows/src/product/workflows/create-products.ts +++ b/packages/core-flows/src/product/workflows/create-products.ts @@ -36,7 +36,7 @@ export const createProductsWorkflow = createWorkflow( const createdProducts = createProductsStep(productWithoutPrices) - // Note: We rely on the same order of input and output when creating products here, make sure that assumption holds + // Note: We rely on the same order of input and output when creating products here, ensure this always holds true const variantsWithAssociatedPrices = transform( { input, createdProducts }, (data) => { diff --git a/packages/medusa/src/api-v2/admin/products/helpers.ts b/packages/medusa/src/api-v2/admin/products/helpers.ts index 6a76e75387..81dca40df9 100644 --- a/packages/medusa/src/api-v2/admin/products/helpers.ts +++ b/packages/medusa/src/api-v2/admin/products/helpers.ts @@ -1,4 +1,9 @@ -import { MedusaContainer, ProductDTO, ProductVariantDTO } from "@medusajs/types" +import { + CreateProductDTO, + MedusaContainer, + ProductDTO, + ProductVariantDTO, +} from "@medusajs/types" import { remoteQueryObjectFromString } from "@medusajs/utils" const isPricing = (fieldName: string) => @@ -45,8 +50,14 @@ export const remapVariant = (v: ProductVariantDTO) => { return { ...v, prices: (v as any).price_set?.prices?.map((price) => ({ - ...price, + id: price.id, + amount: price.amount, + currency_code: price.currency_code, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, variant_id: v.id, + created_at: price.created_at, + updated_at: price.updated_at, })), price_set: undefined, } diff --git a/packages/medusa/src/api-v2/admin/products/route.ts b/packages/medusa/src/api-v2/admin/products/route.ts index 4e06b502d8..3a2be9ecb6 100644 --- a/packages/medusa/src/api-v2/admin/products/route.ts +++ b/packages/medusa/src/api-v2/admin/products/route.ts @@ -41,11 +41,7 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const input = [ - { - ...req.validatedBody, - }, - ] + const input = [req.validatedBody] const { result, errors } = await createProductsWorkflow(req.scope).run({ input: { products: input }, diff --git a/packages/medusa/src/api-v2/admin/products/validators.ts b/packages/medusa/src/api-v2/admin/products/validators.ts index 708086fb71..2cb02d650a 100644 --- a/packages/medusa/src/api-v2/admin/products/validators.ts +++ b/packages/medusa/src/api-v2/admin/products/validators.ts @@ -287,9 +287,8 @@ export class AdminPostProductsReq { status?: ProductStatus = ProductStatus.DRAFT @IsOptional() - @Type(() => ProductTypeReq) - @ValidateNested() - type?: ProductTypeReq + @IsString() + type_id?: string @IsOptional() @IsString() @@ -400,9 +399,8 @@ export class AdminPostProductsProductReq { status?: ProductStatus @IsOptional() - @Type(() => ProductTypeReq) - @ValidateNested() - type?: ProductTypeReq + @IsString() + type_id?: string @IsOptional() @IsString() diff --git a/packages/product/integration-tests/__fixtures__/product/data/create-product.ts b/packages/product/integration-tests/__fixtures__/product/data/create-product.ts index 1316297243..7194d4ae70 100644 --- a/packages/product/integration-tests/__fixtures__/product/data/create-product.ts +++ b/packages/product/integration-tests/__fixtures__/product/data/create-product.ts @@ -42,7 +42,7 @@ export const buildProductAndRelationsData = ({ thumbnail, images, status, - type, + type_id, tags, options, variants, @@ -60,7 +60,7 @@ export const buildProductAndRelationsData = ({ thumbnail: thumbnail as string, status: status ?? ProductTypes.ProductStatus.PUBLISHED, images: (images ?? []) as Image[], - type: type ? { value: type } : { value: faker.commerce.productName() }, + type_id, tags: tags ?? [{ value: "tag-1" }], collection_id, options: options ?? [ diff --git a/packages/product/integration-tests/__tests__/services/product-collection/index.ts b/packages/product/integration-tests/__tests__/services/product-collection/index.ts index b316feeee7..a1e62b4b2b 100644 --- a/packages/product/integration-tests/__tests__/services/product-collection/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-collection/index.ts @@ -180,6 +180,7 @@ moduleIntegrationTestRunner({ { id: "test-2", title: "col 2", + handle: "col-2", products: [], }, ]) @@ -253,6 +254,7 @@ moduleIntegrationTestRunner({ expect(serialized).toEqual({ id: collectionData.id, title: collectionData.title, + handle: "collection-1", }) }) @@ -272,6 +274,7 @@ moduleIntegrationTestRunner({ expect(serialized).toEqual({ id: collectionData.id, title: collectionData.title, + handle: "collection-1", products: [], }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts index feeffb6796..8fba59fbd6 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts @@ -354,18 +354,15 @@ moduleIntegrationTestRunner({ let error try { - await service.upsertCollections([ - { - id: "does-not-exist", - title: "New Collection", - }, - ]) + await service.updateCollections("does-not-exist", { + title: "New Collection", + }) } catch (e) { error = e } expect(error.message).toEqual( - 'ProductCollection with id "does-not-exist" not found' + "ProductCollection with id: does-not-exist was not found" ) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts index e1e4f39df9..6722d580e7 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts @@ -103,6 +103,8 @@ moduleIntegrationTestRunner({ product_id: productOne.id, product: { id: productOne.id, + type_id: null, + collection_id: null, }, }, ]) @@ -175,6 +177,8 @@ moduleIntegrationTestRunner({ product_id: productOne.id, product: { id: productOne.id, + type_id: null, + collection_id: null, }, }, ]) @@ -203,8 +207,12 @@ moduleIntegrationTestRunner({ id: optionOne.id, product: { id: "product-1", + handle: "product-1", title: "product 1", + type_id: null, + collection_id: null, }, + product_id: "product-1", }) ) }) @@ -258,17 +266,13 @@ moduleIntegrationTestRunner({ let error try { - await service.upsertOptions([ - { - id: "does-not-exist", - }, - ]) + await service.updateOptions("does-not-exist", {}) } catch (e) { error = e } expect(error.message).toEqual( - `Option with id "does-not-exist" does not exist, but was referenced in the update request` + `ProductOption with id: does-not-exist was not found` ) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts index fc17d052c8..f5e0e4cca2 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts @@ -102,6 +102,8 @@ moduleIntegrationTestRunner({ value: tagOne.value, products: [ { + collection_id: null, + type_id: null, id: productOne.id, }, ], @@ -175,6 +177,8 @@ moduleIntegrationTestRunner({ value: tagOne.value, products: [ { + collection_id: null, + type_id: null, id: productOne.id, }, ], diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts index 7e61bcc2ad..18708d82cf 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts @@ -9,14 +9,17 @@ import { ProductVariant, } from "@models" -import { MockEventBusService } from "medusa-test-utils" +import { + MockEventBusService, + moduleIntegrationTestRunner, + SuiteOptions, +} from "medusa-test-utils" import { createCollections, createTypes } from "../../../__fixtures__/product" import { createProductCategories } from "../../../__fixtures__/product-category" import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product" -import { UpdateProductInput } from "../../../../src/types/services/product" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import { UpdateProductInput } from "@types" -jest.setTimeout(30000) +jest.setTimeout(300000) moduleIntegrationTestRunner({ moduleName: Modules.PRODUCT, @@ -54,7 +57,7 @@ moduleIntegrationTestRunner({ let variantTwo: ProductVariant let productTypeOne: ProductType let productTypeTwo: ProductType - let images = ["image-1"] + let images = [{ url: "image-1" }] const productCategoriesData = [ { @@ -151,7 +154,7 @@ moduleIntegrationTestRunner({ it("should update a product and upsert relations that are not created yet", async () => { const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const variantTitle = data.variants[0].title @@ -173,12 +176,10 @@ moduleIntegrationTestRunner({ ...productBefore.variants!, ...data.variants, ] - productBefore.type = { value: "new-type" } productBefore.options = data.options productBefore.images = data.images productBefore.thumbnail = data.thumbnail productBefore.tags = data.tags - const updatedProducts = await service.upsert([productBefore]) expect(updatedProducts).toHaveLength(1) @@ -211,12 +212,12 @@ moduleIntegrationTestRunner({ subtitle: productBefore.subtitle, is_giftcard: productBefore.is_giftcard, discountable: productBefore.discountable, - thumbnail: images[0], + thumbnail: images[0].url, status: productBefore.status, images: expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), - url: images[0], + url: images[0].url, }), ]), options: expect.arrayContaining([ @@ -237,10 +238,6 @@ moduleIntegrationTestRunner({ value: productBefore.tags?.[0].value, }), ]), - type: expect.objectContaining({ - id: expect.any(String), - value: productBefore.type!.value, - }), variants: expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), @@ -268,7 +265,7 @@ moduleIntegrationTestRunner({ const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const updateData = { @@ -328,10 +325,7 @@ moduleIntegrationTestRunner({ it("should upsert a product type when type object is passed", async () => { let updateData = { id: productTwo.id, - type: { - id: productTypeOne.id, - value: productTypeOne.value, - }, + type_id: productTypeOne.id, } await service.upsert([updateData]) @@ -348,29 +342,6 @@ moduleIntegrationTestRunner({ }), }) ) - - updateData = { - id: productTwo.id, - type: { - id: "new-type-id", - value: "new-type-value", - }, - } - - await service.upsert([updateData]) - - product = await service.retrieve(updateData.id, { - relations: ["type"], - }) - - expect(product).toEqual( - expect.objectContaining({ - id: productTwo.id, - type: expect.objectContaining({ - ...updateData.type, - }), - }) - ) }) it("should replace relationships of a product", async () => { @@ -421,8 +392,7 @@ moduleIntegrationTestRunner({ ) }) - // TODO: Currently the base repository doesn't remove relationships if an empty array is passed, we need to fix that in the base repo. - it.skip("should remove relationships of a product", async () => { + it("should remove relationships of a product", async () => { const updateData = { id: productTwo.id, categories: [], @@ -450,18 +420,13 @@ moduleIntegrationTestRunner({ it("should throw an error when product ID does not exist", async () => { let error - const updateData = { - id: "does-not-exist", - title: "test", - } - try { - await service.upsert([updateData]) + await service.update("does-not-exist", { title: "test" }) } catch (e) { error = e.message } - expect(error).toEqual(`Product with id "does-not-exist" not found`) + expect(error).toEqual(`Product with id: does-not-exist was not found`) }) it("should update, create and delete variants", async () => { @@ -503,15 +468,13 @@ moduleIntegrationTestRunner({ ) }) - it("should throw an error when variant with id does not exist", async () => { - let error - + it("should createa variant with id that was passed if it does not exist", async () => { const updateData = { id: productTwo.id, // Note: VariantThree is already assigned to productTwo, that should be deleted variants: [ { - id: "does-not-exist", + id: "passed-id", title: "updated-variant", }, { @@ -520,28 +483,33 @@ moduleIntegrationTestRunner({ ], } - try { - await service.upsert([updateData]) - } catch (e) { - error = e - } - - await service.retrieve(updateData.id, { + await service.upsert([updateData]) + const retrieved = await service.retrieve(updateData.id, { relations: ["variants"], }) - expect(error.message).toEqual( - `Variant with id "does-not-exist" does not exist, but was referenced in the update request` + expect(retrieved.variants).toHaveLength(2) + expect(retrieved.variants).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "passed-id", + title: "updated-variant", + }), + expect.objectContaining({ + id: expect.any(String), + title: "created-variant", + }), + ]) ) }) }) describe("create", function () { - let images = ["image-1"] + let images = [{ url: "image-1" }] it("should create a product", async () => { const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const productsCreated = await service.create([data]) @@ -557,7 +525,6 @@ moduleIntegrationTestRunner({ "options", "options.values", "tags", - "type", ], } ) @@ -578,12 +545,12 @@ moduleIntegrationTestRunner({ subtitle: data.subtitle, is_giftcard: data.is_giftcard, discountable: data.discountable, - thumbnail: images[0], + thumbnail: images[0].url, status: data.status, images: expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), - url: images[0], + url: images[0].url, }), ]), options: expect.arrayContaining([ @@ -604,10 +571,6 @@ moduleIntegrationTestRunner({ value: data.tags[0].value, }), ]), - type: expect.objectContaining({ - id: expect.any(String), - value: data.type.value, - }), variants: expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), @@ -635,7 +598,7 @@ moduleIntegrationTestRunner({ const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const products = await service.create([data]) @@ -650,11 +613,11 @@ moduleIntegrationTestRunner({ }) describe("softDelete", function () { - let images = ["image-1"] + let images = [{ url: "image-1" }] it("should soft delete a product and its cascaded relations", async () => { const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const products = await service.create([data]) @@ -705,7 +668,7 @@ moduleIntegrationTestRunner({ it("should retrieve soft-deleted products if filtered on deleted_at", async () => { const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const products = await service.create([data]) @@ -723,7 +686,7 @@ moduleIntegrationTestRunner({ const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const products = await service.create([data]) @@ -740,12 +703,12 @@ moduleIntegrationTestRunner({ }) describe("restore", function () { - let images = ["image-1"] + let images = [{ url: "image-1" }] it("should restore a soft deleted product and its cascaded relations", async () => { const data = buildProductAndRelationsData({ images, - thumbnail: images[0], + thumbnail: images[0].url, }) const products = await service.create([data]) @@ -852,7 +815,7 @@ moduleIntegrationTestRunner({ ]) }) - it("should returns empty array when querying for a collection that doesnt exist", async () => { + it("should return empty array when querying for a collection that doesnt exist", async () => { const products = await service.list( { categories: { id: ["collection-doesnt-exist-id"] }, diff --git a/packages/product/integration-tests/__tests__/services/product-option/index.ts b/packages/product/integration-tests/__tests__/services/product-option/index.ts index 9601836f76..4884380c96 100644 --- a/packages/product/integration-tests/__tests__/services/product-option/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-option/index.ts @@ -210,6 +210,7 @@ moduleIntegrationTestRunner({ expect(serialized).toEqual({ id: optionId, title: optionValue, + product_id: null, }) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-tag/index.ts b/packages/product/integration-tests/__tests__/services/product-tag/index.ts index 8d2193d202..ef055797fa 100644 --- a/packages/product/integration-tests/__tests__/services/product-tag/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-tag/index.ts @@ -184,8 +184,11 @@ moduleIntegrationTestRunner({ products: [ { id: "test-1", + collection_id: null, + type_id: null, }, ], + value: "France", }), ]) }) diff --git a/packages/product/integration-tests/__tests__/services/product/index.ts b/packages/product/integration-tests/__tests__/services/product/index.ts index ecbd08f2fa..6878a3b01d 100644 --- a/packages/product/integration-tests/__tests__/services/product/index.ts +++ b/packages/product/integration-tests/__tests__/services/product/index.ts @@ -379,18 +379,21 @@ moduleIntegrationTestRunner({ name: "category 0", handle: "category-0", mpath: "category-0.", + parent_category_id: null, }, { id: "category-1", name: "category 1", handle: "category-1", mpath: "category-0.category-1.", + parent_category_id: null, }, { id: "category-1-a", name: "category 1 a", handle: "category-1-a", mpath: "category-0.category-1.category-1-a.", + parent_category_id: null, }, ]) }) @@ -482,7 +485,11 @@ moduleIntegrationTestRunner({ { id: workingProduct.id, title: workingProduct.title, + handle: "product-1", + collection_id: workingCollection.id, + type_id: null, collection: { + handle: "col-1", id: workingCollection.id, title: workingCollection.title, }, @@ -508,8 +515,11 @@ moduleIntegrationTestRunner({ { id: workingProduct.id, title: workingProduct.title, + handle: "product-1", + type_id: null, collection_id: workingCollection.id, collection: { + handle: "col-1", id: workingCollection.id, title: workingCollection.title, }, @@ -517,8 +527,11 @@ moduleIntegrationTestRunner({ { id: workingProductTwo.id, title: workingProductTwo.title, + handle: "product", + type_id: null, collection_id: workingCollectionTwo.id, collection: { + handle: "col-2", id: workingCollectionTwo.id, title: workingCollectionTwo.title, }, diff --git a/packages/product/src/migrations/.snapshot-medusa-products.json b/packages/product/src/migrations/.snapshot-medusa-products.json index a170b3f9aa..e8ea283c36 100644 --- a/packages/product/src/migrations/.snapshot-medusa-products.json +++ b/packages/product/src/migrations/.snapshot-medusa-products.json @@ -944,7 +944,7 @@ "id" ], "referencedTableName": "public.product", - "deleteRule": "set null", + "deleteRule": "cascade", "updateRule": "cascade" } } @@ -1063,6 +1063,7 @@ "id" ], "referencedTableName": "public.product_option", + "deleteRule": "cascade", "updateRule": "cascade" } } diff --git a/packages/product/src/migrations/InitialSetup20240315083440.ts b/packages/product/src/migrations/InitialSetup20240315083440.ts deleted file mode 100644 index ecc1fc9833..0000000000 --- a/packages/product/src/migrations/InitialSetup20240315083440.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Migration } from "@mikro-orm/migrations" - -export class InitialSetup20240315083440 extends Migration { - async up(): Promise { - // TODO: These migrations that get generated don't even reflect the models, write by hand. - const productTables = await this.execute( - "select * from information_schema.tables where table_name = 'product' and table_schema = 'public'" - ) - - if (productTables.length > 0) { - // This is so we can still run the api tests, remove completely once that is not needed - this.addSql( - `alter table "product_option_value" alter column "variant_id" drop not null;` - ) - } - - this.addSql( - 'create table if not exists "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), constraint "product_category_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_category_path" on "product_category" ("mpath");' - ) - this.addSql( - 'create table if not exists "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");' - ) - this.addSql( - 'alter table if exists "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");' - ) - - this.addSql( - 'create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_image_url" on "image" ("url");' - ) - this.addSql( - 'create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");' - ) - - this.addSql( - 'create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");' - ) - - this.addSql( - 'create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_type_deleted_at" on "product_type" ("deleted_at");' - ) - - this.addSql( - 'create table if not exists "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_type_id" on "product" ("type_id");' - ) - this.addSql( - 'create index if not exists "IDX_product_deleted_at" on "product" ("deleted_at");' - ) - this.addSql( - 'alter table if exists "product" add constraint "IDX_product_handle_unique" unique ("handle");' - ) - - this.addSql( - 'create table if not exists "product_option" ("id" text not null, "title" text not null, "product_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_option_deleted_at" on "product_option" ("deleted_at");' - ) - - this.addSql( - 'create table if not exists "product_option_value" ("id" text not null, "value" text not null, "option_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_option_value_option_id" on "product_option_value" ("option_id");' - ) - this.addSql( - 'create index if not exists "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");' - ) - - this.addSql( - 'create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));' - ) - - this.addSql( - 'create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));' - ) - - this.addSql( - 'create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));' - ) - - this.addSql( - 'create table if not exists "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "product_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_variant_product_id" on "product_variant" ("product_id");' - ) - this.addSql( - 'create index if not exists "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");' - ) - this.addSql( - 'alter table if exists "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");' - ) - this.addSql( - 'alter table if exists "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");' - ) - this.addSql( - 'alter table if exists "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");' - ) - this.addSql( - 'alter table if exists "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");' - ) - - this.addSql( - 'create table if not exists "product_variant_option" ("id" text not null, "option_value_id" text null, "variant_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_option_pkey" primary key ("id"));' - ) - this.addSql( - 'create index if not exists "IDX_product_variant_option_deleted_at" on "product_variant_option" ("deleted_at");' - ) - - this.addSql( - 'alter table if exists "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;' - ) - - this.addSql( - 'alter table if exists "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;' - ) - this.addSql( - 'alter table if exists "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;' - ) - - this.addSql( - 'alter table if exists "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete set null;' - ) - - this.addSql( - 'alter table if exists "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade;' - ) - - this.addSql( - 'alter table if exists "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;' - ) - this.addSql( - 'alter table if exists "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;' - ) - - this.addSql( - 'alter table if exists "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;' - ) - this.addSql( - 'alter table if exists "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;' - ) - - this.addSql( - 'alter table if exists "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;' - ) - this.addSql( - 'alter table if exists "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;' - ) - - this.addSql( - 'alter table if exists "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;' - ) - - this.addSql( - 'alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete set null;' - ) - this.addSql( - 'alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete set null;' - ) - } -} diff --git a/packages/product/src/migrations/InitialSetup20240325200756.ts b/packages/product/src/migrations/InitialSetup20240325200756.ts new file mode 100644 index 0000000000..1a732aee02 --- /dev/null +++ b/packages/product/src/migrations/InitialSetup20240325200756.ts @@ -0,0 +1,92 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class InitialSetup20240315083440 extends Migration { + async up(): Promise { + // TODO: These migrations that get generated don't even reflect the models, write by hand. + const productTables = await this.execute( + "select * from information_schema.tables where table_name = 'product' and table_schema = 'public'" + ) + + if (productTables.length > 0) { + // This is so we can still run the api tests, remove completely once that is not needed + this.addSql( + `alter table "product_option_value" alter column "variant_id" drop not null;` + ) + this.addSql( + `alter table "product_variant" alter column "inventory_quantity" drop not null;` + ) + } + + this.addSql('create table if not exists "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), constraint "product_category_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_category_path" on "product_category" ("mpath");'); + + // TODO: Re-enable when we run the migration from v1 + // this.addSql('alter table if exists "product_category" add constraint "IDX_product_category_handle" unique ("handle");'); + + this.addSql('create table if not exists "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");'); + this.addSql('alter table if exists "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");'); + + this.addSql('create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_image_url" on "image" ("url");'); + this.addSql('create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");'); + + this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");'); + + this.addSql('create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_type_deleted_at" on "product_type" ("deleted_at");'); + + this.addSql('create table if not exists "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_type_id" on "product" ("type_id");'); + this.addSql('create index if not exists "IDX_product_deleted_at" on "product" ("deleted_at");'); + this.addSql('alter table if exists "product" add constraint "IDX_product_handle_unique" unique ("handle");'); + + this.addSql('create table if not exists "product_option" ("id" text not null, "title" text not null, "product_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_option_deleted_at" on "product_option" ("deleted_at");'); + + this.addSql('create table if not exists "product_option_value" ("id" text not null, "value" text not null, "option_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_option_value_option_id" on "product_option_value" ("option_id");'); + this.addSql('create index if not exists "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");'); + + this.addSql('create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));'); + + this.addSql('create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));'); + + this.addSql('create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));'); + + this.addSql('create table if not exists "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "product_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_variant_product_id" on "product_variant" ("product_id");'); + this.addSql('create index if not exists "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");'); + this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");'); + this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");'); + this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");'); + this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");'); + + this.addSql('create table if not exists "product_variant_option" ("id" text not null, "option_value_id" text null, "variant_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_option_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_variant_option_deleted_at" on "product_variant_option" ("deleted_at");'); + + this.addSql('alter table if exists "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;'); + + this.addSql('alter table if exists "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;'); + this.addSql('alter table if exists "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;'); + + this.addSql('alter table if exists "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete set null;'); + this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete set null;'); + } +} diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts index 40013bfc5a..3111be1090 100644 --- a/packages/product/src/models/product-category.ts +++ b/packages/product/src/models/product-category.ts @@ -56,10 +56,15 @@ class ProductCategory { @Property({ columnType: "numeric", nullable: false, default: 0 }) rank?: number - @Property({ columnType: "text", nullable: true }) + @ManyToOne(() => ProductCategory, { + columnType: "text", + fieldName: "parent_category_id", + nullable: true, + mapToPk: true, + }) parent_category_id?: string | null - @ManyToOne(() => ProductCategory, { nullable: true }) + @ManyToOne(() => ProductCategory, { nullable: true, persist: false }) parent_category?: ProductCategory @OneToMany({ @@ -89,11 +94,13 @@ class ProductCategory { @OnInit() async onInit() { this.id = generateEntityId(this.id, "pcat") + this.parent_category_id ??= this.parent_category?.id ?? null } @BeforeCreate() async onCreate(args: EventArgs) { this.id = generateEntityId(this.id, "pcat") + this.parent_category_id ??= this.parent_category?.id ?? null if (!this.handle && this.name) { this.handle = kebabCase(this.name) diff --git a/packages/product/src/models/product-collection.ts b/packages/product/src/models/product-collection.ts index 21dd8bf2bc..5622f6dc46 100644 --- a/packages/product/src/models/product-collection.ts +++ b/packages/product/src/models/product-collection.ts @@ -65,13 +65,17 @@ class ProductCollection { @OnInit() onInit() { this.id = generateEntityId(this.id, "pcol") + + if (!this.handle && this.title) { + this.handle = kebabCase(this.title) + } } @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "pcol") - if (!this.handle) { + if (!this.handle && this.title) { this.handle = kebabCase(this.title) } } diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index 430795758b..a13166bc47 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -47,14 +47,21 @@ class ProductOptionValue { @Property({ columnType: "text" }) value: string - @Property({ columnType: "text", nullable: true }) - option_id!: string + @ManyToOne(() => ProductOption, { + columnType: "text", + fieldName: "option_id", + mapToPk: true, + nullable: true, + index: "IDX_product_option_value_option_id", + onDelete: "cascade", + }) + option_id: string | null @ManyToOne(() => ProductOption, { - index: "IDX_product_option_value_option_id", - fieldName: "option_id", + nullable: true, + persist: false, }) - option: ProductOption + option: ProductOption | null @OneToMany(() => ProductVariantOption, (value) => value.option_value, {}) variant_options = new Collection(this) @@ -84,11 +91,13 @@ class ProductOptionValue { @OnInit() onInit() { this.id = generateEntityId(this.id, "optval") + this.option_id ??= this.option?.id ?? null } @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "optval") + this.option_id ??= this.option?.id ?? null } } diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index a21629f2fa..d3f027dd1e 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -48,14 +48,20 @@ class ProductOption { @Property({ columnType: "text" }) title: string - @Property({ columnType: "text", nullable: true }) - product_id!: string + @ManyToOne(() => Product, { + columnType: "text", + fieldName: "product_id", + mapToPk: true, + nullable: true, + onDelete: "cascade", + }) + product_id: string | null @ManyToOne(() => Product, { - fieldName: "product_id", + persist: false, nullable: true, }) - product!: Product + product: Product | null @OneToMany(() => ProductOptionValue, (value) => value.option, { cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any], @@ -87,11 +93,13 @@ class ProductOption { @OnInit() onInit() { this.id = generateEntityId(this.id, "opt") + this.product_id ??= this.product?.id ?? null } @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "opt") + this.product_id ??= this.product?.id ?? null } } diff --git a/packages/product/src/models/product-variant-option.ts b/packages/product/src/models/product-variant-option.ts index 1d084e1e59..3b6594034e 100644 --- a/packages/product/src/models/product-variant-option.ts +++ b/packages/product/src/models/product-variant-option.ts @@ -31,20 +31,30 @@ class ProductVariantOption { @PrimaryKey({ columnType: "text" }) id!: string - @Property({ columnType: "text", nullable: true }) + @ManyToOne(() => ProductOptionValue, { + columnType: "text", + nullable: true, + fieldName: "option_value_id", + mapToPk: true, + }) option_value_id!: string @ManyToOne(() => ProductOptionValue, { - fieldName: "option_value_id", + persist: false, nullable: true, }) option_value!: ProductOptionValue - @Property({ columnType: "text", nullable: true }) - variant_id!: string + @ManyToOne(() => ProductVariant, { + columnType: "text", + nullable: true, + fieldName: "variant_id", + mapToPk: true, + }) + variant_id: string | null @ManyToOne(() => ProductVariant, { - fieldName: "variant_id", + persist: false, nullable: true, }) variant!: ProductVariant @@ -71,11 +81,15 @@ class ProductVariantOption { @OnInit() onInit() { this.id = generateEntityId(this.id, "varopt") + this.variant_id ??= this.variant?.id ?? null + this.option_value_id ??= this.option_value?.id ?? null } @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "varopt") + this.variant_id ??= this.variant?.id ?? null + this.option_value_id ??= this.option_value?.id ?? null } } diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index c1049b61ca..2fa1d042d1 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -119,16 +119,21 @@ class ProductVariant { }) variant_rank?: number | null - @Property({ columnType: "text", nullable: true }) - product_id!: string + @ManyToOne(() => Product, { + columnType: "text", + nullable: true, + onDelete: "cascade", + fieldName: "product_id", + index: "IDX_product_variant_product_id", + mapToPk: true, + }) + product_id: string | null @ManyToOne(() => Product, { - onDelete: "cascade", - index: "IDX_product_variant_product_id", - fieldName: "product_id", + persist: false, nullable: true, }) - product!: Product + product: Product | null @Property({ onCreate: () => new Date(), @@ -161,11 +166,13 @@ class ProductVariant { @OnInit() onInit() { this.id = generateEntityId(this.id, "variant") + this.product_id ??= this.product?.id ?? null } @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "variant") + this.product_id ??= this.product?.id ?? null } } diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index ebc74cd3f4..5a8caebc62 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -106,31 +106,39 @@ class Product { @Property({ columnType: "text", nullable: true }) material?: string | null - @Property({ columnType: "text", nullable: true }) - collection_id!: string + @ManyToOne(() => ProductCollection, { + columnType: "text", + nullable: true, + fieldName: "collection_id", + mapToPk: true, + }) + collection_id: string | null @ManyToOne(() => ProductCollection, { nullable: true, - fieldName: "collection_id", + persist: false, }) - collection!: ProductCollection | null + collection: ProductCollection | null - @Property({ columnType: "text", nullable: true }) - type_id!: string + @ManyToOne(() => ProductType, { + columnType: "text", + nullable: true, + fieldName: "type_id", + index: "IDX_product_type_id", + mapToPk: true, + }) + type_id: string | null @ManyToOne(() => ProductType, { nullable: true, - index: "IDX_product_type_id", - fieldName: "type_id", + persist: false, }) - type!: ProductType + type: ProductType | null @ManyToMany(() => ProductTag, "products", { owner: true, pivotTable: "product_tags", index: "IDX_product_tag_id", - cascade: ["soft-remove"] as any, - // TODO: Do we really want to remove tags if the product is deleted? }) tags = new Collection(this) @@ -138,7 +146,6 @@ class Product { owner: true, pivotTable: "product_images", index: "IDX_product_image_id", - cascade: ["soft-remove"] as any, joinColumn: "product_id", inverseJoinColumn: "image_id", }) @@ -147,7 +154,6 @@ class Product { @ManyToMany(() => ProductCategory, "products", { owner: true, pivotTable: "product_category_product", - // TODO: rm cascade: ["soft-remove"] as any, }) categories = new Collection(this) @@ -182,12 +188,21 @@ class Product { @OnInit() onInit() { this.id = generateEntityId(this.id, "prod") + this.type_id ??= this.type?.id ?? null + this.collection_id ??= this.collection?.id ?? null + + if (!this.handle && this.title) { + this.handle = kebabCase(this.title) + } } @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "prod") - if (!this.handle) { + this.type_id ??= this.type?.id ?? null + this.collection_id ??= this.collection?.id ?? null + + if (!this.handle && this.title) { this.handle = kebabCase(this.title) } } diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 0be18dc29a..998ff4d689 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -37,22 +37,22 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, + ProductStatus, promiseAll, } from "@medusajs/utils" import { ProductCategoryEventData, ProductCategoryEvents, - UpdateCollectionInput, - ProductEventData, - ProductEvents, - UpdateProductInput, ProductCollectionEventData, ProductCollectionEvents, - UpdateProductVariantInput, + ProductEventData, + ProductEvents, + UpdateCollectionInput, + UpdateProductInput, UpdateProductOptionInput, + UpdateProductVariantInput, } from "../types" import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config" -import { ProductStatus } from "@medusajs/utils" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -222,7 +222,7 @@ export default class ProductModuleService< const createdVariants = await this.baseRepository_.serialize< ProductTypes.ProductVariantDTO[] - >(variants, { populate: true }) + >(variants) return Array.isArray(data) ? createdVariants : createdVariants[0] } @@ -353,7 +353,7 @@ export default class ProductModuleService< ): Promise { // Validation step const variantIdsToUpdate = data.map(({ id }) => id) - const variants = await this.listVariants( + const variants = await this.productVariantService_.list( { id: variantIdsToUpdate }, { relations: ["options"], take: null }, sharedContext @@ -373,7 +373,7 @@ export default class ProductModuleService< (v) => ({ ...data.find((d) => d.id === v.id), id: v.id, - product_id: v.product_id!, + product_id: v.product_id, }) ) @@ -387,90 +387,18 @@ export default class ProductModuleService< sharedContext ) - return await this.diffVariants_( - variantsWithProductId, - productOptions, + return this.productVariantService_.upsertWithReplace( + ProductModuleService.assignOptionsToVariants( + variantsWithProductId, + productOptions + ), + { + relations: ["options"], + }, sharedContext ) } - @InjectTransactionManager("baseRepository_") - protected async diffVariants_( - data: UpdateProductVariantInput[], - productOptions: ProductOption[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - const toCreate = data.filter((o) => !o.id) - const toUpdate = data.filter((o) => o.id) - let createdVariants: TProductVariant[] = [] - let updatedVariants: TProductVariant[] = [] - - if (toCreate.length) { - createdVariants = await this.productVariantService_.create( - ProductModuleService.assignOptionsToVariants(toCreate, productOptions), - sharedContext - ) - } - - if (toUpdate.length) { - const existingVariants = await this.productVariantService_.list( - { id: toUpdate.map((o) => o.id) }, - { take: null }, - sharedContext - ) - - const updateVariants = await promiseAll( - toUpdate.map(async (variantToUpdate) => { - const dbVariant = existingVariants.find( - (o) => o.id === variantToUpdate.id - ) - if (!dbVariant) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Variant with id "${variantToUpdate.id}" does not exist, but was referenced in the update request` - ) - } - - if (!variantToUpdate.options) { - return variantToUpdate - } - - const dbVariantOptions = await this.productVariantOptionService_.list( - { variant_id: dbVariant.id }, - { relations: ["option_value", "option_value.option"], take: null }, - sharedContext - ) - - const variantOptionsToDelete = dbVariantOptions - .filter((variantOption) => { - return !Object.entries(variantToUpdate.options ?? {}).some( - ([optionTitle, optionValue]) => - variantOption.option_value.value === optionValue && - variantOption.option_value.option.title === optionTitle - ) - }) - .map((v) => v.id) - - await this.productVariantOptionService_.delete({ - id: { $in: variantOptionsToDelete }, - }) - - return variantToUpdate - }) - ) - - updatedVariants = await this.productVariantService_.update( - ProductModuleService.assignOptionsToVariants( - updateVariants, - productOptions - ), - sharedContext - ) - } - - return [...createdVariants, ...updatedVariants] - } - @InjectTransactionManager("baseRepository_") async createTags( data: ProductTypes.CreateProductTagDTO[], @@ -481,7 +409,7 @@ export default class ProductModuleService< sharedContext ) - return await this.baseRepository_.serialize(productTags, { populate: true }) + return await this.baseRepository_.serialize(productTags) } @InjectTransactionManager("baseRepository_") @@ -494,7 +422,7 @@ export default class ProductModuleService< sharedContext ) - return await this.baseRepository_.serialize(productTags, { populate: true }) + return await this.baseRepository_.serialize(productTags) } @InjectTransactionManager("baseRepository_") @@ -549,7 +477,7 @@ export default class ProductModuleService< const createdOptions = await this.baseRepository_.serialize< ProductTypes.ProductOptionDTO[] - >(options, { populate: true }) + >(options) return Array.isArray(data) ? createdOptions : createdOptions[0] } @@ -642,6 +570,7 @@ export default class ProductModuleService< ): Promise { let normalizedInput: UpdateProductOptionInput[] = [] if (isString(idOrSelector)) { + await this.productOptionService_.retrieve(idOrSelector, {}, sharedContext) normalizedInput = [{ id: idOrSelector, ...data }] } else { const options = await this.productOptionService_.list( @@ -691,98 +620,11 @@ export default class ProductModuleService< ) } - const productOptions = await this.diffOptions_( + return await this.productOptionService_.upsertWithReplace( normalizedInput, + { relations: ["values"] }, sharedContext ) - - return productOptions - } - - // TODO: Do validation - @InjectTransactionManager("baseRepository_") - protected async diffOptions_( - data: UpdateProductOptionInput[], - @MedusaContext() sharedContext: Context = {} - ) { - const toCreate = data.filter((o) => !o.id) - const toUpdate = data.filter((o) => o.id) - let createdOptions: ProductOption[] = [] - let updatedOptions: ProductOption[] = [] - - if (toCreate.length) { - createdOptions = await this.productOptionService_.create( - toCreate, - sharedContext - ) - } - - if (toUpdate.length) { - const existingOptions = await this.productOptionService_.list( - { id: toUpdate.map((o) => o.id) }, - { take: null }, - sharedContext - ) - - const updateOptions = await promiseAll( - toUpdate.map(async (optionToUpdate) => { - const dbOption = existingOptions.find( - (o) => o.id === optionToUpdate.id - ) - if (!dbOption) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Option with id "${optionToUpdate.id}" does not exist, but was referenced in the update request` - ) - } - - if (!optionToUpdate.values) { - return optionToUpdate - } - - const valuesToDelete = dbOption.values - .filter((dbVal) => { - return !optionToUpdate.values?.some( - (updateVal) => updateVal.value === dbVal.value - ) - }) - .map((v) => v.id) - - const valuesToUpsert = optionToUpdate.values?.map((val) => { - const dbValue = dbOption.values.find((v) => v.value === val.value) - if (dbValue) { - return { - ...val, - id: dbValue.id, - } - } - - return val - }) - - await this.productOptionValueService_.delete({ - id: { $in: valuesToDelete }, - }) - - const updatedValues = await this.productOptionValueService_.upsert( - valuesToUpsert, - sharedContext - ) - - return { - ...optionToUpdate, - values: updatedValues, - } - }) - ) - - updatedOptions = await this.productOptionService_.update( - updateOptions, - sharedContext - ) - } - - return [...createdOptions, ...updatedOptions] } createCollections( @@ -809,7 +651,7 @@ export default class ProductModuleService< const createdCollections = await this.baseRepository_.serialize< ProductTypes.ProductCollectionDTO[] - >(collections, { populate: true }) + >(collections) await this.eventBusModuleService_?.emit( collections.map(({ id }) => ({ @@ -827,11 +669,12 @@ export default class ProductModuleService< @MedusaContext() sharedContext: Context = {} ): Promise { const normalizedInput = data.map( - ProductModuleService.normalizeProductCollectionInput + ProductModuleService.normalizeCreateProductCollectionInput ) - return await this.productCollectionService_.create( + return await this.productCollectionService_.upsertWithReplace( normalizedInput, + { relations: ["products"] }, sharedContext ) } @@ -919,6 +762,11 @@ export default class ProductModuleService< > { let normalizedInput: UpdateCollectionInput[] = [] if (isString(idOrSelector)) { + await this.productCollectionService_.retrieve( + idOrSelector, + {}, + sharedContext + ) normalizedInput = [{ id: idOrSelector, ...data }] } else { const collections = await this.productCollectionService_.list( @@ -958,11 +806,12 @@ export default class ProductModuleService< @MedusaContext() sharedContext: Context = {} ): Promise { const normalizedInput = data.map( - ProductModuleService.normalizeProductCollectionInput + ProductModuleService.normalizeUpdateProductCollectionInput ) - return await this.productCollectionService_.update( + return await this.productCollectionService_.upsertWithReplace( normalizedInput, + { relations: ["products"] }, sharedContext ) } @@ -1041,7 +890,7 @@ export default class ProductModuleService< const createdProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products, { populate: true }) + >(products) await this.eventBusModuleService_?.emit( createdProducts.map(({ id }) => ({ @@ -1087,7 +936,7 @@ export default class ProductModuleService< const result = [...created, ...updated] const allProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] | ProductTypes.ProductDTO - >(result, { populate: true }) + >(result) if (created.length) { await this.eventBusModuleService_?.emit( @@ -1129,6 +978,9 @@ export default class ProductModuleService< ): Promise { let normalizedInput: UpdateProductInput[] = [] if (isString(idOrSelector)) { + // This will throw if the product does not exist + await this.productService_.retrieve(idOrSelector, {}, sharedContext) + normalizedInput = [{ id: idOrSelector, ...data }] } else { const products = await this.productService_.list( @@ -1147,7 +999,7 @@ export default class ProductModuleService< const updatedProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products, { populate: true }) + >(products) await this.eventBusModuleService_?.emit( updatedProducts.map(({ id }) => ({ @@ -1159,14 +1011,6 @@ export default class ProductModuleService< return isString(idOrSelector) ? updatedProducts[0] : updatedProducts } - // Orchestrate product creation (and updates follow a similar logic). For each product: - // 1. Create the base product - // 2. Upsert images, assign to product - // 3. Upsert tags, assign to product - // 4. Upsert product type, assign to product - // 5. Create options and option values - // 6. Assign options to variants - // 7. Create variants @InjectTransactionManager("baseRepository_") protected async create_( data: ProductTypes.CreateProductDTO[], @@ -1175,61 +1019,52 @@ export default class ProductModuleService< const normalizedInput = data.map( ProductModuleService.normalizeCreateProductInput ) - const productsData = await promiseAll( - normalizedInput.map(async (product: any) => { - const productData = { ...product } - if (productData.images?.length) { - productData.images = await this.productImageService_.upsert( - productData.images, - sharedContext - ) + + const productData = await this.productService_.upsertWithReplace( + normalizedInput, + { + relations: ["type", "collection", "images", "tags", "categories"], + }, + sharedContext + ) + + await promiseAll( + // Note: It's safe to rely on the order here as `upsertWithReplace` preserves the order of the input + normalizedInput.map(async (product, i) => { + const upsertedProduct: any = productData[i] + upsertedProduct.options = [] + upsertedProduct.variants = [] + + if (product.options?.length) { + upsertedProduct.options = + await this.productOptionService_.upsertWithReplace( + product.options?.map((option) => ({ + ...option, + product_id: upsertedProduct.id, + })) ?? [], + { relations: ["values"] }, + sharedContext + ) } - if (productData.tags?.length) { - productData.tags = await this.productTagService_.upsert( - productData.tags, - sharedContext - ) + if (product.variants?.length) { + upsertedProduct.variants = + await this.productVariantService_.upsertWithReplace( + ProductModuleService.assignOptionsToVariants( + product.variants?.map((v) => ({ + ...v, + product_id: upsertedProduct.id, + })) ?? [], + upsertedProduct.options + ), + { relations: ["options"] }, + sharedContext + ) } - - if (productData.type) { - productData.type = await this.productTypeService_.upsert( - productData.type, - sharedContext - ) - } - - // This is not the cleanest solution, but it's the easiest way to reassign categories for now - if (productData.categories) { - productData.categories = await this.productCategoryService_.list( - { id: productData.categories.map((c) => c.id) }, - { take: null }, - sharedContext - ) - } - - if (productData.options?.length) { - productData.options = await this.productOptionService_.create( - productData.options, - sharedContext - ) - } - - if (productData.variants?.length) { - productData.variants = await this.productVariantService_.create( - ProductModuleService.assignOptionsToVariants( - productData.variants!, - productData.options - ), - sharedContext - ) - } - - return productData as ProductTypes.CreateProductDTO }) ) - return await this.productService_.create(productsData, sharedContext) + return productData } @InjectTransactionManager("baseRepository_") @@ -1240,122 +1075,117 @@ export default class ProductModuleService< const normalizedInput = data.map( ProductModuleService.normalizeUpdateProductInput ) - const productsData = await promiseAll( - normalizedInput.map(async (product: any) => { - const productData = { ...product } - // TODO: We don't remove images, tags, and types as they can exist independently. However, how do we handle orphaned entities? - if (productData.images) { - productData.images = await this.productImageService_.upsert( - productData.images, + + const productData = await this.productService_.upsertWithReplace( + normalizedInput, + { + relations: ["type", "collection", "images", "tags", "categories"], + }, + sharedContext + ) + + // There is more than 1-level depth of relations here, so we need to handle the options and variants manually + await promiseAll( + // Note: It's safe to rely on the order here as `upsertWithReplace` preserves the order of the input + normalizedInput.map(async (product, i) => { + const upsertedProduct: any = productData[i] + let allOptions: any[] = [] + + if (product.options?.length) { + upsertedProduct.options = + await this.productOptionService_.upsertWithReplace( + product.options?.map((option) => ({ + ...option, + product_id: upsertedProduct.id, + })) ?? [], + { relations: ["values"] }, + sharedContext + ) + + // Since we handle the options and variants outside of the product upsert, we need to clean up manuallys + await this.productOptionService_.delete( + { + product_id: upsertedProduct.id, + id: { + $nin: upsertedProduct.options.map(({ id }) => id), + }, + }, + sharedContext + ) + allOptions = upsertedProduct.options + } else { + // If the options weren't affected, but the user is changing the variants, make sure we have all options available locally + if (product.variants?.length) { + allOptions = await this.productOptionService_.list( + { product_id: upsertedProduct.id }, + { take: null }, + sharedContext + ) + } + } + + if (product.variants?.length) { + upsertedProduct.variants = + await this.productVariantService_.upsertWithReplace( + ProductModuleService.assignOptionsToVariants( + product.variants?.map((v) => ({ + ...v, + product_id: upsertedProduct.id, + })) ?? [], + allOptions + ), + { relations: ["options"] }, + sharedContext + ) + + await this.productVariantService_.delete( + { + product_id: upsertedProduct.id, + id: { + $nin: upsertedProduct.variants.map(({ id }) => id), + }, + }, sharedContext ) } - - if (productData.tags) { - productData.tags = await this.productTagService_.upsert( - productData.tags, - sharedContext - ) - } - - if (productData.type) { - productData.type = await this.productTypeService_.upsert( - productData.type, - sharedContext - ) - } - - // This is not the cleanest solution, but it's the easiest way to reassign categories for now - if (productData.categories) { - productData.categories = await this.productCategoryService_.list( - { id: productData.categories.map((c) => c.id) }, - { take: null }, - sharedContext - ) - } - - // TODO: Maybe we also want to delete the options and variants that are not in the list? - if (productData.options) { - productData.options = await this.diffOptions_( - productData.options, - sharedContext - ) - } - - if (productData.variants) { - const dbOptionsForProduct = await this.productOptionService_.list( - { product_id: productData.id }, - { take: null }, - sharedContext - ) - - // Since the options are not flushed yet, we must do this merge here - const allOptionsForProduct = uniqBy( - [...(productData.options ?? []), ...dbOptionsForProduct], - "id" - ) - - productData.variants = await this.diffVariants_( - productData.variants, - allOptionsForProduct, - sharedContext - ) - } - - return productData as UpdateProductInput }) ) - return await this.productService_.update(productsData, sharedContext) + return productData } protected static normalizeCreateProductInput( product: ProductTypes.CreateProductDTO ): ProductTypes.CreateProductDTO { - const productData = { ...product } + const productData = ProductModuleService.normalizeUpdateProductInput( + product as UpdateProductInput + ) as ProductTypes.CreateProductDTO + if (!productData.handle && productData.title) { productData.handle = kebabCase(productData.title) } - if (productData.is_giftcard) { - productData.discountable = false - } - if (!productData.status) { productData.status = ProductStatus.DRAFT } if (!productData.thumbnail && productData.images?.length) { - productData.thumbnail = isString(productData.images[0]) - ? (productData.images[0] as string) - : (productData.images[0] as { url: string }).url + productData.thumbnail = productData.images[0].url } - return ProductModuleService.normalizeUpdateProductInput( - productData - ) as ProductTypes.CreateProductDTO + return productData } protected static normalizeUpdateProductInput( - product: ProductTypes.UpdateProductDTO - ): ProductTypes.UpdateProductDTO { + product: UpdateProductInput + ): UpdateProductInput { const productData = { ...product } if (productData.is_giftcard) { productData.discountable = false } - if (productData.images?.length) { - productData.images = productData.images?.map((image) => { - if (isString(image)) { - return { url: image } - } - - return image - }) - } - if (productData.options?.length) { - productData.options = productData.options?.map((option) => { + ;(productData as any).options = productData.options?.map((option) => { return { title: option.title, values: option.values?.map((value) => { @@ -1370,12 +1200,31 @@ export default class ProductModuleService< return productData } - protected static normalizeProductCollectionInput( + protected static normalizeCreateProductCollectionInput( + collection: ProductTypes.CreateProductCollectionDTO + ): ProductTypes.CreateProductCollectionDTO { + const collectionData = + ProductModuleService.normalizeUpdateProductCollectionInput( + collection + ) as ProductTypes.CreateProductCollectionDTO + + if (!collectionData.handle && collectionData.title) { + collectionData.handle = kebabCase(collectionData.title) + } + + return collectionData + } + + protected static normalizeUpdateProductCollectionInput( collection: ProductTypes.CreateProductCollectionDTO | UpdateCollectionInput ): ProductTypes.CreateProductCollectionDTO | UpdateCollectionInput { const collectionData = { ...collection } if (collectionData.product_ids?.length) { - ;(collectionData as any).products = collectionData.product_ids + ;(collectionData as any).products = collectionData.product_ids.map( + (pid) => ({ + id: pid, + }) + ) delete collectionData.product_ids } @@ -1390,12 +1239,16 @@ export default class ProductModuleService< ): | ProductTypes.CreateProductVariantDTO[] | ProductTypes.UpdateProductVariantDTO[] { + if (!variants.length) { + return variants + } + const variantsWithOptions = variants.map((variant: any) => { const variantOptions = Object.entries(variant.options ?? {}).map( ([key, val]) => { const option = options.find((o) => o.title === key) const optionValue = option?.values?.find( - (v: any) => (v.value.value ?? v.value) === val + (v: any) => (v.value?.value ?? v.value) === val ) if (!optionValue) { @@ -1421,11 +1274,3 @@ export default class ProductModuleService< return variantsWithOptions } } - -const uniqBy = (arr: T[], key: keyof T) => { - const seen = new Set() - return arr.filter((item) => { - const k = item[key] - return seen.has(k) ? false : seen.add(k) - }) -} diff --git a/packages/product/src/services/product.ts b/packages/product/src/services/product.ts index 835f09b056..c50cf1ed36 100644 --- a/packages/product/src/services/product.ts +++ b/packages/product/src/services/product.ts @@ -1,4 +1,10 @@ -import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" +import { + Context, + DAL, + FindConfig, + ProductTypes, + BaseFilterable, +} from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" import { Product } from "@models" @@ -6,6 +12,12 @@ type InjectedDependencies = { productRepository: DAL.RepositoryService } +type NormalizedFilterableProductProps = ProductTypes.FilterableProductProps & { + categories?: { + id: string | { $in: string[] } + } +} + export default class ProductService< TEntity extends Product = Product > extends ModulesSdkUtils.internalModuleServiceFactory( @@ -27,20 +39,11 @@ export default class ProductService< config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { - if (filters.category_id) { - if (Array.isArray(filters.category_id)) { - filters.categories = { - id: { $in: filters.category_id }, - } - } else { - filters.categories = { - id: filters.category_id, - } - } - delete filters.category_id - } - - return await super.list(filters, config, sharedContext) + return await super.list( + ProductService.normalizeFilters(filters), + config, + sharedContext + ) } @InjectManager("productRepository_") @@ -49,6 +52,16 @@ export default class ProductService< config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { + return await super.listAndCount( + ProductService.normalizeFilters(filters), + config, + sharedContext + ) + } + + protected static normalizeFilters( + filters: NormalizedFilterableProductProps = {} + ): NormalizedFilterableProductProps { if (filters.category_id) { if (Array.isArray(filters.category_id)) { filters.categories = { @@ -56,12 +69,12 @@ export default class ProductService< } } else { filters.categories = { - id: filters.category_id, + id: filters.category_id as string, } } delete filters.category_id } - return await super.listAndCount(filters, config, sharedContext) + return filters } } diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts index 1cb81ae2b2..cd54191d55 100644 --- a/packages/product/src/types/index.ts +++ b/packages/product/src/types/index.ts @@ -25,10 +25,6 @@ export enum ProductEvents { PRODUCT_DELETED = "product.deleted", } -export type UpdateProductInput = ProductTypes.UpdateProductDTO & { - id: string -} - export type ProductCollectionEventData = { id: string } @@ -39,6 +35,10 @@ export enum ProductCollectionEvents { COLLECTION_DELETED = "product-collection.deleted", } +export type UpdateProductInput = ProductTypes.UpdateProductDTO & { + id: string +} + export type UpdateProductCollection = ProductTypes.UpdateProductCollectionDTO & { products?: string[] @@ -55,7 +55,7 @@ export type UpdateCollectionInput = ProductTypes.UpdateProductCollectionDTO & { export type UpdateProductVariantInput = ProductTypes.UpdateProductVariantDTO & { id: string - product_id?: string + product_id?: string | null } export type UpdateProductOptionInput = ProductTypes.UpdateProductOptionDTO & { diff --git a/packages/product/tsconfig.spec.json b/packages/product/tsconfig.spec.json index b887bbfa39..48e47e8cbb 100644 --- a/packages/product/tsconfig.spec.json +++ b/packages/product/tsconfig.spec.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.json", "include": ["src", "integration-tests"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "sourceMap": true + } } diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 7607b52ec5..47b961ddc8 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -86,7 +86,11 @@ export interface ProductDTO { * * @expandable */ - collection: ProductCollectionDTO + collection?: ProductCollectionDTO | null + /** + * The associated product collection id. + */ + collection_id?: string | null /** * The associated product categories. * @@ -98,7 +102,11 @@ export interface ProductDTO { * * @expandable */ - type: ProductTypeDTO + type?: ProductTypeDTO | null + /** + * The associated product type id. + */ + type_id?: string | null /** * The associated product tags. * @@ -239,11 +247,11 @@ export interface ProductVariantDTO { * * @expandable */ - product: ProductDTO + product?: ProductDTO | null /** - * The ID of the associated product. + * The associated product id. */ - product_id: string + product_id?: string | null /** * he ranking of the variant among other variants associated with the product. */ @@ -301,7 +309,11 @@ export interface ProductCategoryDTO { * * @expandable */ - parent_category?: ProductCategoryDTO + parent_category?: ProductCategoryDTO | null + /** + * The associated parent category id. + */ + parent_category_id?: string | null /** * The associated child categories. * @@ -438,6 +450,14 @@ export interface ProductCollectionDTO { * Holds custom data in key-value pairs. */ metadata?: Record | null + /** + * When the product collection was created. + */ + created_at: string | Date + /** + * When the product collection was updated. + */ + updated_at: string | Date /** * When the product collection was deleted. */ @@ -468,6 +488,14 @@ export interface ProductTypeDTO { * Holds custom data in key-value pairs. */ metadata?: Record | null + /** + * When the product type was created. + */ + created_at: string | Date + /** + * When the product type was updated. + */ + updated_at: string | Date /** * When the product type was deleted. */ @@ -494,7 +522,11 @@ export interface ProductOptionDTO { * * @expandable */ - product: ProductDTO + product?: ProductDTO | null + /** + * The associated product id. + */ + product_id?: string | null /** * The associated product option values. * @@ -505,6 +537,14 @@ export interface ProductOptionDTO { * Holds custom data in key-value pairs. */ metadata?: Record | null + /** + * When the product option was created. + */ + created_at: string | Date + /** + * When the product option was updated. + */ + updated_at: string | Date /** * When the product option was deleted. */ @@ -521,13 +561,21 @@ export interface ProductVariantOptionDTO { * * @expandable */ - option_value: ProductOptionValueDTO + option_value?: ProductOptionValueDTO | null + /** + * The value of the product variant option id. + */ + option_value_id?: string | null /** * The associated product variant. * * @expandable */ - variant: ProductVariantDTO + variant?: ProductVariantDTO | null + /** + * The associated product variant id. + */ + variant_id?: string | null } /** @@ -553,6 +601,14 @@ export interface ProductImageDTO { * Holds custom data in key-value pairs. */ metadata?: Record | null + /** + * When the product image was created. + */ + created_at: string | Date + /** + * When the product image was updated. + */ + updated_at: string | Date /** * When the product image was deleted. */ @@ -585,11 +641,23 @@ export interface ProductOptionValueDTO { * * @expandable */ - option: ProductOptionDTO + option?: ProductOptionDTO | null + /** + * The associated product option id. + */ + option_id?: string | null /** * Holds custom data in key-value pairs. */ metadata?: Record | null + /** + * When the product option value was created. + */ + created_at: string | Date + /** + * When the product option value was updated. + */ + updated_at: string | Date /** * When the product option value was deleted. */ @@ -644,23 +712,6 @@ export interface FilterableProductProps */ value?: string[] } - /** - * Filters on a product's categories. - */ - categories?: { - /** - * IDs to filter categories by. - */ - id?: string | string[] | OperatorMap - /** - * Filter categories by whether they're internal - */ - is_internal?: boolean - /** - * Filter categories by whether they're active. - */ - is_active?: boolean - } /** * Filter a product by the ID of the associated type */ @@ -921,10 +972,6 @@ export interface UpdateProductCollectionDTO { * A product type to create. */ export interface CreateProductTypeDTO { - /** - * The product type's ID. - */ - id?: string /** * The product type's value. */ @@ -935,13 +982,11 @@ export interface CreateProductTypeDTO { metadata?: Record } -export interface UpsertProductTypeDTO { - id?: string - value: string +export interface UpsertProductTypeDTO extends UpdateProductTypeDTO { /** - * Holds custom data in key-value pairs. + * The product type's ID. */ - metadata?: Record + id?: string } /** @@ -950,10 +995,6 @@ export interface UpsertProductTypeDTO { * The data to update in a product type. The `id` is used to identify which product type to update. */ export interface UpdateProductTypeDTO { - /** - * The ID of the product type to update. - */ - id: string /** * The new value of the product type. */ @@ -964,6 +1005,45 @@ export interface UpdateProductTypeDTO { metadata?: Record } +/** + * @interface + * + * A product image to create. + */ +export interface CreateProductImageDTO { + /** + * The product image's URL. + */ + url: string + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record +} + +export interface UpsertProductImageDTO extends UpdateProductImageDTO { + /** + * The product image's ID. + */ + id?: string +} + +/** + * @interface + * + * The data to update in a product image. The `id` is used to identify which product image to update. + */ +export interface UpdateProductImageDTO { + /** + * The new URL of the product image. + */ + url?: string + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record +} + /** * @interface * @@ -976,9 +1056,11 @@ export interface CreateProductTagDTO { value: string } -export interface UpsertProductTagDTO { +export interface UpsertProductTagDTO extends UpdateProductTagDTO { + /** + * The ID of the product tag to update. + */ id?: string - value: string } /** @@ -988,10 +1070,6 @@ export interface UpsertProductTagDTO { * The data to update in a product tag. The `id` is used to identify which product tag to update. */ export interface UpdateProductTagDTO { - /** - * The ID of the product tag to update. - */ - id: string /** * The value of the product tag. */ @@ -1011,7 +1089,7 @@ export interface CreateProductOptionDTO { /** * The product option values. */ - values: string[] | { value: string }[] + values: string[] /** * The ID of the associated product. */ @@ -1019,12 +1097,24 @@ export interface CreateProductOptionDTO { } export interface UpsertProductOptionDTO extends UpdateProductOptionDTO { + /** + * The ID of the product option to update. + */ id?: string } export interface UpdateProductOptionDTO { + /** + * The product option's title. + */ title?: string - values?: string[] | { value: string }[] + /** + * The product option values. + */ + values?: string[] + /** + * The ID of the associated product. + */ product_id?: string } @@ -1225,11 +1315,6 @@ export interface CreateProductDTO { * Whether the product can be discounted. */ discountable?: boolean - /** - * The product's images. If an array of strings is supplied, each string will be a URL and a `ProductImage` will be created - * and associated with the product. If an array of objects is supplied, you can pass along the ID of an existing `ProductImage`. - */ - images?: string[] | { id?: string; url: string }[] /** * The URL of the product's thumbnail. */ @@ -1244,25 +1329,25 @@ export interface CreateProductDTO { */ status?: ProductStatus /** - * The product type to create and associate with the product. + * The product's images to upsert and associate with the product */ - type?: CreateProductTypeDTO + images?: UpsertProductImageDTO[] /** - * The product type to be associated with the product. + * The product type id to associate with the product. */ - type_id?: string | null + type_id?: string /** - * The product collection to be associated with the product. + * The product collection to associate with the product. */ - collection_id?: string | null + collection_id?: string /** - * The product tags to be created and associated with the product. + * The product tags to be upserted and associated with the product. */ - tags?: CreateProductTagDTO[] + tags?: UpsertProductTagDTO[] /** * The product categories to associate with the product. */ - categories?: { id: string }[] + category_ids?: string[] /** * The product options to be created and associated with the product. */ @@ -1342,11 +1427,6 @@ export interface UpdateProductDTO { * Whether the product can be discounted. */ discountable?: boolean - /** - * The product's images. If an array of strings is supplied, each string will be a URL and a `ProductImage` will be created - * and associated with the product. If an array of objects is supplied, you can pass along the ID of an existing `ProductImage`. - */ - images?: string[] | { id?: string; url: string }[] /** * The URL of the product's thumbnail. */ @@ -1361,29 +1441,29 @@ export interface UpdateProductDTO { */ status?: ProductStatus /** - * The product type to create and associate with the product. + * The product's images to upsert and associate with the product */ - type?: CreateProductTypeDTO + images?: UpsertProductImageDTO[] /** - * The product type to be associated with the product. + * The product type to associate with the product. */ type_id?: string | null /** - * The product collection to be associated with the product. + * The product collection to associate with the product. */ collection_id?: string | null /** * The product tags to be created and associated with the product. */ - tags?: CreateProductTagDTO[] + tags?: UpsertProductTagDTO[] /** * The product categories to associate with the product. */ - categories?: { id: string }[] + category_ids?: string[] /** * The product options to be created and associated with the product. */ - options?: CreateProductOptionDTO[] + options?: UpsertProductOptionDTO[] /** * The product variants to be created and associated with the product. You can also update existing product variants associated with the product. */ diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index de559f1bfb..be253b2e49 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -305,6 +305,267 @@ export interface IProductModuleService extends IModuleService { sharedContext?: Context ): Promise<[ProductDTO[], number]> + /** + * This method is used to create a list of products. + * + * @param {CreateProductDTO[]} data - The products to be created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The list of created products. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function createProduct (title: string) { + * const productModule = await initializeProductModule() + * + * const products = await productModule.create([ + * { + * title + * } + * ]) + * + * // do something with the products or return them + * } + */ + create( + data: CreateProductDTO[], + sharedContext?: Context + ): Promise + + /** + * This method is used to create a product. + * + * @param {CreateProductDTO} data - The product to be created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created product. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function createProduct (title: string) { + * const productModule = await initializeProductModule() + * + * const product = await productModule.create( + * { + * title + * } + * ) + * + * // do something with the product or return it + * } + */ + create(data: CreateProductDTO, sharedContext?: Context): Promise + + /** + * This method updates existing products, or creates new ones if they don't exist. + * + * @param {CreateProductDTO[]} data - The attributes to update or create for each product. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated and created products. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function upserProduct (title: string) { + * const productModule = await initializeProductModule() + * + * const createdProducts = await productModule.upsert([ + * { + * title + * } + * ]) + * + * // do something with the products or return them + * } + */ + upsert( + data: UpsertProductDTO[], + sharedContext?: Context + ): Promise + + /** + * This method updates the product if it exists, or creates a new ones if it doesn't. + * + * @param {CreateProductDTO} data - The attributes to update or create for the new product. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated or created product. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function upserProduct (title: string) { + * const productModule = await initializeProductModule() + * + * const createdProduct = await productModule.upsert( + * { + * title + * } + * ) + * + * // do something with the product or return it + * } + */ + upsert( + data: UpsertProductDTO[], + sharedContext?: Context + ): Promise + + /** + * This method is used to update a product. + * + * @param {string} id - The ID of the product to be updated. + * @param {UpdateProductDTO} data - The attributes of the product to be updated + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated product. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function updateProduct (id: string, title: string) { + * const productModule = await initializeProductModule() + * + * const product = await productModule.update(id, { + * title + * } + * ) + * + * // do something with the product or return it + * } + */ + update( + id: string, + data: UpdateProductDTO, + sharedContext?: Context + ): Promise + + /** + * This method is used to update a list of products determined by the selector filters. + * + * @param {FilterableProductProps} selector - The filters that will determine which products will be updated. + * @param {UpdateProductDTO} data - The attributes to be updated on the selected products + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated products. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function updateProduct (id: string, title: string) { + * const productModule = await initializeProductModule() + * + * const products = await productModule.update({id}, { + * title + * } + * ) + * + * // do something with the products or return them + * } + */ + update( + selector: FilterableProductProps, + data: UpdateProductDTO, + sharedContext?: Context + ): Promise + + /** + * This method is used to delete products. Unlike the {@link softDelete} method, this method will completely remove the products and they can no longer be accessed or retrieved. + * + * @param {string[]} productIds - The IDs of the products to be deleted. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the products are successfully deleted. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function deleteProducts (ids: string[]) { + * const productModule = await initializeProductModule() + * + * await productModule.delete(ids) + * } + */ + delete(productIds: string[], sharedContext?: Context): Promise + + /** + * This method is used to delete products. Unlike the {@link delete} method, this method won't completely remove the product. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter. + * + * The soft-deleted products can be restored using the {@link restore} method. + * + * @param {string[]} productIds - The IDs of the products to soft-delete. + * @param {SoftDeleteReturn} config - + * Configurations determining which relations to soft delete along with the each of the products. You can pass to its `returnLinkableKeys` + * property any of the product's relation attribute names, such as `variant_id`. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were also soft deleted, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of a record associated with the product through this relation, such as the IDs of associated product variants. + * + * If there are no related records, the promise resolved to `void`. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function deleteProducts (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const cascadedEntities = await productModule.softDelete(ids) + * + * // do something with the returned cascaded entity IDs or return them + * } + */ + softDelete( + productIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method is used to restore products which were deleted using the {@link softDelete} method. + * + * @param {string[]} productIds - The IDs of the products to restore. + * @param {RestoreReturn} config - + * Configurations determining which relations to restore along with each of the products. You can pass to its `returnLinkableKeys` + * property any of the product's relation attribute names, such as `variant_id`. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were restored, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of the record associated with the product through this relation, such as the IDs of associated product variants. + * + * If there are no related records that were restored, the promise resolved to `void`. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function restoreProducts (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const cascadedEntities = await productModule.restore(ids, { + * returnLinkableKeys: ["variant_id"] + * }) + * + * // do something with the returned cascaded entity IDs or return them + * } + */ + restore( + productIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + /** * This method is used to retrieve a tag by its ID. * @@ -1679,6 +1940,112 @@ export interface IProductModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method is used to retrieve a paginated list of product variants along with the total count of available product variants satisfying the provided filters. + * + * @param {FilterableProductVariantProps} filters - The filters applied on the retrieved product variants. + * @param {FindConfig} config - + * The configurations determining how the product variants are retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a product variant. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise<[ProductVariantDTO[], number]>} The list of product variants along with their total count. + * + * @example + * To retrieve a list of product variants using their IDs: + * + * ```ts + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function retrieveProductVariants (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const [variants, count] = await productModule.listAndCountVariants({ + * id: ids + * }) + * + * // do something with the product variants or return them + * } + * ``` + * + * To specify relations that should be retrieved within the product variants: + * + * ```ts + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function retrieveProductVariants (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const [variants, count] = await productModule.listAndCountVariants({ + * id: ids + * }, { + * relations: ["options"] + * }) + * + * // do something with the product variants or return them + * } + * ``` + * + * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: + * + * ```ts + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function retrieveProductVariants (ids: string[], skip: number, take: number) { + * const productModule = await initializeProductModule() + * + * const [variants, count] = await productModule.listAndCountVariants({ + * id: ids + * }, { + * relations: ["options"], + * skip, + * take + * }) + * + * // do something with the product variants or return them + * } + * ``` + * + * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: + * + * ```ts + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function retrieveProductVariants (ids: string[], sku: string, skip: number, take: number) { + * const productModule = await initializeProductModule() + * + * const [variants, count] = await productModule.listAndCountVariants({ + * $and: [ + * { + * id: ids + * }, + * { + * sku + * } + * ] + * }, { + * relations: ["options"], + * skip, + * take + * }) + * + * // do something with the product variants or return them + * } + * ``` + */ + listAndCountVariants( + filters?: FilterableProductVariantProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[ProductVariantDTO[], number]> + /** * This method is used to create product variants. * @@ -1880,112 +2247,6 @@ export interface IProductModuleService extends IModuleService { sharedContext?: Context ): Promise - /** - * This method is used to retrieve a paginated list of product variants along with the total count of available product variants satisfying the provided filters. - * - * @param {FilterableProductVariantProps} filters - The filters applied on the retrieved product variants. - * @param {FindConfig} config - - * The configurations determining how the product variants are retrieved. Its properties, such as `select` or `relations`, accept the - * attributes or relations associated with a product variant. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise<[ProductVariantDTO[], number]>} The list of product variants along with their total count. - * - * @example - * To retrieve a list of product variants using their IDs: - * - * ```ts - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function retrieveProductVariants (ids: string[]) { - * const productModule = await initializeProductModule() - * - * const [variants, count] = await productModule.listAndCountVariants({ - * id: ids - * }) - * - * // do something with the product variants or return them - * } - * ``` - * - * To specify relations that should be retrieved within the product variants: - * - * ```ts - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function retrieveProductVariants (ids: string[]) { - * const productModule = await initializeProductModule() - * - * const [variants, count] = await productModule.listAndCountVariants({ - * id: ids - * }, { - * relations: ["options"] - * }) - * - * // do something with the product variants or return them - * } - * ``` - * - * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * - * ```ts - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function retrieveProductVariants (ids: string[], skip: number, take: number) { - * const productModule = await initializeProductModule() - * - * const [variants, count] = await productModule.listAndCountVariants({ - * id: ids - * }, { - * relations: ["options"], - * skip, - * take - * }) - * - * // do something with the product variants or return them - * } - * ``` - * - * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * - * ```ts - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function retrieveProductVariants (ids: string[], sku: string, skip: number, take: number) { - * const productModule = await initializeProductModule() - * - * const [variants, count] = await productModule.listAndCountVariants({ - * $and: [ - * { - * id: ids - * }, - * { - * sku - * } - * ] - * }, { - * relations: ["options"], - * skip, - * take - * }) - * - * // do something with the product variants or return them - * } - * ``` - */ - listAndCountVariants( - filters?: FilterableProductVariantProps, - config?: FindConfig, - sharedContext?: Context - ): Promise<[ProductVariantDTO[], number]> - /** * This method is used to delete variants. Unlike the {@link delete} method, this method won't completely remove the variant. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter. * @@ -2519,6 +2780,74 @@ export interface IProductModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method is used to delete product collections. Unlike the {@link deleteCollections} method, this method won't completely remove the collection. It can still be accessed or retrieved using methods like {@link retrieveCollections} if you pass the `withDeleted` property to the `config` object parameter. + * + * The soft-deleted collections can be restored using the {@link restoreCollections} method. + * + * @param {string[]} collectionIds - The IDs of the collections to soft-delete. + * @param {SoftDeleteReturn} config - + * Configurations determining which relations to soft delete along with the each of the collections. You can pass to its `returnLinkableKeys` + * property any of the collection's relation attribute names. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were also soft deleted. The object's keys are the ID attribute names of the collection entity's relations. + * + * If there are no related records, the promise resolved to `void`. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function deleteCollections (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const cascadedEntities = await productModule.softDeleteCollections(ids) + * + * // do something with the returned cascaded entity IDs or return them + * } + */ + softDeleteCollections( + collectionIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method is used to restore collections which were deleted using the {@link softDelete} method. + * + * @param {string[]} collectionIds - The IDs of the collections to restore. + * @param {RestoreReturn} config - + * Configurations determining which relations to restore along with each of the collections. You can pass to its `returnLinkableKeys` + * property any of the collection's relation attribute names. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product entity's relations. + * + * If there are no related records that were restored, the promise resolved to `void`. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function restoreCollections (ids: string[]) { + * const productModule = await initializeProductModule() + * + * const cascadedEntities = await productModule.restoreCollections(ids, { + * returnLinkableKeys: [] + * }) + * + * // do something with the returned cascaded entity IDs or return them + * } + */ + restoreCollections( + collectionIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + /** * This method is used to retrieve a product category by its ID. * @@ -2859,365 +3188,4 @@ export interface IProductModuleService extends IModuleService { * } */ deleteCategory(categoryId: string, sharedContext?: Context): Promise - - /** - * This method is used to create a list of products. - * - * @param {CreateProductDTO[]} data - The products to be created. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The list of created products. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function createProduct (title: string) { - * const productModule = await initializeProductModule() - * - * const products = await productModule.create([ - * { - * title - * } - * ]) - * - * // do something with the products or return them - * } - */ - create( - data: CreateProductDTO[], - sharedContext?: Context - ): Promise - - /** - * This method is used to create a product. - * - * @param {CreateProductDTO} data - The product to be created. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The created product. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function createProduct (title: string) { - * const productModule = await initializeProductModule() - * - * const product = await productModule.create( - * { - * title - * } - * ) - * - * // do something with the product or return it - * } - */ - create(data: CreateProductDTO, sharedContext?: Context): Promise - - /** - * This method updates existing products, or creates new ones if they don't exist. - * - * @param {CreateProductDTO[]} data - The attributes to update or create for each product. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated and created products. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function upserProduct (title: string) { - * const productModule = await initializeProductModule() - * - * const createdProducts = await productModule.upsert([ - * { - * title - * } - * ]) - * - * // do something with the products or return them - * } - */ - upsert( - data: UpsertProductDTO[], - sharedContext?: Context - ): Promise - - /** - * This method updates the product if it exists, or creates a new ones if it doesn't. - * - * @param {CreateProductDTO} data - The attributes to update or create for the new product. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated or created product. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function upserProduct (title: string) { - * const productModule = await initializeProductModule() - * - * const createdProduct = await productModule.upsert( - * { - * title - * } - * ) - * - * // do something with the product or return it - * } - */ - upsert( - data: UpsertProductDTO[], - sharedContext?: Context - ): Promise - - /** - * This method is used to update a product. - * - * @param {string} id - The ID of the product to be updated. - * @param {UpdateProductDTO} data - The attributes of the product to be updated - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated product. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function updateProduct (id: string, title: string) { - * const productModule = await initializeProductModule() - * - * const product = await productModule.update(id, { - * title - * } - * ) - * - * // do something with the product or return it - * } - */ - update( - id: string, - data: UpdateProductDTO, - sharedContext?: Context - ): Promise - - /** - * This method is used to update a list of products determined by the selector filters. - * - * @param {FilterableProductProps} selector - The filters that will determine which products will be updated. - * @param {UpdateProductDTO} data - The attributes to be updated on the selected products - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated products. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function updateProduct (id: string, title: string) { - * const productModule = await initializeProductModule() - * - * const products = await productModule.update({id}, { - * title - * } - * ) - * - * // do something with the products or return them - * } - */ - update( - selector: FilterableProductProps, - data: UpdateProductDTO, - sharedContext?: Context - ): Promise - - /** - * This method is used to delete products. Unlike the {@link softDelete} method, this method will completely remove the products and they can no longer be accessed or retrieved. - * - * @param {string[]} productIds - The IDs of the products to be deleted. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} Resolves when the products are successfully deleted. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function deleteProducts (ids: string[]) { - * const productModule = await initializeProductModule() - * - * await productModule.delete(ids) - * } - */ - delete(productIds: string[], sharedContext?: Context): Promise - - /** - * This method is used to delete products. Unlike the {@link delete} method, this method won't completely remove the product. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter. - * - * The soft-deleted products can be restored using the {@link restore} method. - * - * @param {string[]} productIds - The IDs of the products to soft-delete. - * @param {SoftDeleteReturn} config - - * Configurations determining which relations to soft delete along with the each of the products. You can pass to its `returnLinkableKeys` - * property any of the product's relation attribute names, such as `variant_id`. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were also soft deleted, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of a record associated with the product through this relation, such as the IDs of associated product variants. - * - * If there are no related records, the promise resolved to `void`. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function deleteProducts (ids: string[]) { - * const productModule = await initializeProductModule() - * - * const cascadedEntities = await productModule.softDelete(ids) - * - * // do something with the returned cascaded entity IDs or return them - * } - */ - softDelete( - productIds: string[], - config?: SoftDeleteReturn, - sharedContext?: Context - ): Promise | void> - - /** - * This method is used to restore products which were deleted using the {@link softDelete} method. - * - * @param {string[]} productIds - The IDs of the products to restore. - * @param {RestoreReturn} config - - * Configurations determining which relations to restore along with each of the products. You can pass to its `returnLinkableKeys` - * property any of the product's relation attribute names, such as `variant_id`. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of the record associated with the product through this relation, such as the IDs of associated product variants. - * - * If there are no related records that were restored, the promise resolved to `void`. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function restoreProducts (ids: string[]) { - * const productModule = await initializeProductModule() - * - * const cascadedEntities = await productModule.restore(ids, { - * returnLinkableKeys: ["variant_id"] - * }) - * - * // do something with the returned cascaded entity IDs or return them - * } - */ - restore( - productIds: string[], - config?: RestoreReturn, - sharedContext?: Context - ): Promise | void> - - /** - * This method is used to delete product collections. Unlike the {@link deleteCollections} method, this method won't completely remove the collection. It can still be accessed or retrieved using methods like {@link retrieveCollections} if you pass the `withDeleted` property to the `config` object parameter. - * - * The soft-deleted collections can be restored using the {@link restoreCollections} method. - * - * @param {string[]} collectionIds - The IDs of the collections to soft-delete. - * @param {SoftDeleteReturn} config - - * Configurations determining which relations to soft delete along with the each of the collections. You can pass to its `returnLinkableKeys` - * property any of the collection's relation attribute names. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were also soft deleted. The object's keys are the ID attribute names of the collection entity's relations. - * - * If there are no related records, the promise resolved to `void`. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function deleteCollections (ids: string[]) { - * const productModule = await initializeProductModule() - * - * const cascadedEntities = await productModule.softDeleteCollections(ids) - * - * // do something with the returned cascaded entity IDs or return them - * } - */ - softDeleteCollections( - collectionIds: string[], - config?: SoftDeleteReturn, - sharedContext?: Context - ): Promise | void> - - /** - * This method is used to restore collections which were deleted using the {@link softDelete} method. - * - * @param {string[]} collectionIds - The IDs of the collections to restore. - * @param {RestoreReturn} config - - * Configurations determining which relations to restore along with each of the collections. You can pass to its `returnLinkableKeys` - * property any of the collection's relation attribute names. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product entity's relations. - * - * If there are no related records that were restored, the promise resolved to `void`. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function restoreCollections (ids: string[]) { - * const productModule = await initializeProductModule() - * - * const cascadedEntities = await productModule.restoreCollections(ids, { - * returnLinkableKeys: [] - * }) - * - * // do something with the returned cascaded entity IDs or return them - * } - */ - restoreCollections( - collectionIds: string[], - config?: RestoreReturn, - sharedContext?: Context - ): Promise | void> - - /** - * This method is used to restore product varaints that were soft deleted. Product variants are soft deleted when they're not - * provided in a product's details passed to the {@link update} method. - * - * @param {string[]} variantIds - The IDs of the variants to restore. - * @param {RestoreReturn} config - - * Configurations determining which relations to restore along with each of the product variants. You can pass to its `returnLinkableKeys` - * property any of the product variant's relation attribute names. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product variant entity's relations - * and its value is an array of strings, each being the ID of the record associated with the product variant through this relation. - * - * If there are no related records that were restored, the promise resolved to `void`. - * - * @example - * import { - * initialize as initializeProductModule, - * } from "@medusajs/product" - * - * async function restoreProductVariants (ids: string[]) { - * const productModule = await initializeProductModule() - * - * await productModule.restoreVariants(ids) - * } - */ - restoreVariants( - variantIds: string[], - config?: RestoreReturn, - sharedContext?: Context - ): Promise | void> }