diff --git a/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap index 054c0213c9..6e105a829c 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap @@ -43,7 +43,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -157,7 +159,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, diff --git a/integration-tests/api/__tests__/admin/colllections.js b/integration-tests/api/__tests__/admin/colllections.js index 7f109e8ee1..720e9e74b8 100644 --- a/integration-tests/api/__tests__/admin/colllections.js +++ b/integration-tests/api/__tests__/admin/colllections.js @@ -12,10 +12,7 @@ const { DiscountConditionOperator, } = require("@medusajs/medusa") const { IdMap } = require("medusa-test-utils") -const { - simpleDiscountFactory, - simpleSalesChannelFactory, -} = require("../../../factories") +const { simpleDiscountFactory } = require("../../../factories") jest.setTimeout(30000) diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index da034ce7c9..9a6b33298c 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -1879,7 +1879,7 @@ describe("/admin/discounts", () => { const cond = discount.data.discount.rule.conditions[0] const response = await api.post( - `/admin/discounts/test-discount/conditions/${cond.id}?expand=rule,rule.conditions,rule.conditions.products`, + `/admin/discounts/test-discount/conditions/${cond.id}?expand=rule.conditions.products.profiles`, { products: [prod2.id], }, @@ -1911,6 +1911,8 @@ describe("/admin/discounts", () => { created_at: expect.any(String), updated_at: expect.any(String), profile_id: expect.any(String), + profiles: expect.any(Array), + profile: expect.any(Object), type_id: expect.any(String), id: "test-product", }, @@ -2047,7 +2049,7 @@ describe("/admin/discounts", () => { const api = useApi() const discountCondition = await api.get( - "/admin/discounts/test-discount/conditions/test-condition?expand=products&fields=id,type", + "/admin/discounts/test-discount/conditions/test-condition?expand=products.profiles&fields=id,type", adminReqConfig ) @@ -2061,6 +2063,8 @@ describe("/admin/discounts", () => { { id: "test-product", profile_id: expect.any(String), + profiles: expect.any(Array), + profile: expect.any(Object), type_id: expect.any(String), created_at: expect.any(String), updated_at: expect.any(String), @@ -2353,7 +2357,7 @@ describe("/admin/discounts", () => { const cond = discount.data.discount.rule.conditions[0] const response = await api.post( - `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule,rule.conditions,rule.conditions.products`, + `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule.conditions.products.profiles`, { resources: [{ id: prod2.id }, { id: prod3.id }], }, @@ -2490,7 +2494,7 @@ describe("/admin/discounts", () => { const cond = discount.data.discount.rule.conditions[0] const response = await api.delete( - `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule,rule.conditions,rule.conditions.products`, + `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule.conditions.products.profiles`, { ...adminReqConfig, data: { diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index e373bb2974..378dbb4006 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -26,9 +26,6 @@ const { IdMap } = require("medusa-test-utils") jest.setTimeout(50000) -const testProductId = "test-product" -const testProduct1Id = "test-product1" -const testProductFilteringId1 = "test-product_filtering_1" const adminHeaders = { headers: { Authorization: "Bearer test_token", diff --git a/integration-tests/api/__tests__/admin/shipping-options.js b/integration-tests/api/__tests__/admin/shipping-options.js index c194c1f374..a29bf58821 100644 --- a/integration-tests/api/__tests__/admin/shipping-options.js +++ b/integration-tests/api/__tests__/admin/shipping-options.js @@ -28,7 +28,7 @@ describe("/admin/shipping-options", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/admin/shipping-profile.js b/integration-tests/api/__tests__/admin/shipping-profile.js index 40d7401660..3f2fe4423a 100644 --- a/integration-tests/api/__tests__/admin/shipping-profile.js +++ b/integration-tests/api/__tests__/admin/shipping-profile.js @@ -25,7 +25,7 @@ describe("/admin/shipping-profiles", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: false }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index 42db88c7d9..4ec812200d 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -222,6 +222,7 @@ describe("/store/products", () => { "tags", "collection", "type", + "profiles", ]) }) @@ -992,7 +993,12 @@ describe("/store/products", () => { it("response contains only fields defined with `fields` param", async () => { const api = useApi() - const fields = allowedStoreProductsFields + // profile_id is not a column in the products table, so it should be ignored as it + // will be rejected by typeorm as invalid, though, it is an entity property + // that we want to return, so it part of the allowedStoreProductsFields + const fields = allowedStoreProductsFields.filter( + (f) => f !== "profile_id" + ) const response = await api.get( `/store/products/test-product?fields=${fields.join(",")}` diff --git a/integration-tests/factories/simple-product-factory.ts b/integration-tests/factories/simple-product-factory.ts index 5365ad745f..a5a8ce112b 100644 --- a/integration-tests/factories/simple-product-factory.ts +++ b/integration-tests/factories/simple-product-factory.ts @@ -91,8 +91,11 @@ export const simpleProductFactory = async ( discountable: !data.is_giftcard, tags: [] as ProductTag[], profile_id: data.is_giftcard ? gcProfile?.id : defaultProfile?.id, + profiles: [ + { id: data.is_giftcard ? gcProfile?.id : defaultProfile?.id }, + ] as ShippingProfile[], metadata: data.metadata || null, - } as Product + } as unknown as Product if (typeof data.tags !== "undefined") { for (let i = 0; i < data.tags.length; i++) { diff --git a/integration-tests/factories/simple-product-variant-factory.ts b/integration-tests/factories/simple-product-variant-factory.ts index e5922eafe9..ca8bd54da5 100644 --- a/integration-tests/factories/simple-product-variant-factory.ts +++ b/integration-tests/factories/simple-product-variant-factory.ts @@ -52,7 +52,7 @@ export const simpleProductVariantFactory = async ( const options = data.options || [{ option_id: "test-option", value: "Large" }] for (const o of options) { await manager.insert(ProductOptionValue, { - id: `${o.value}-${o.option_id ?? Math.random()}`, + id: `${variant.id}-${o.option_id ?? Math.random()}`, value: o.value, variant_id: id, option_id: o.option_id, diff --git a/integration-tests/factories/simple-shipping-option-factory.ts b/integration-tests/factories/simple-shipping-option-factory.ts index cddfc228ff..1675624fe0 100644 --- a/integration-tests/factories/simple-shipping-option-factory.ts +++ b/integration-tests/factories/simple-shipping-option-factory.ts @@ -40,13 +40,13 @@ export const simpleShippingOptionFactory = async ( const defaultProfile = await manager.findOne(ShippingProfile, { where: { type: ShippingProfileType.DEFAULT, - } + }, }) const gcProfile = await manager.findOne(ShippingProfile, { where: { type: ShippingProfileType.GIFT_CARD, - } + }, }) let region_id = data.region_id @@ -62,7 +62,7 @@ export const simpleShippingOptionFactory = async ( is_return: data.is_return ?? false, region_id: region_id, provider_id: "test-ful", - profile_id: data.is_giftcard ? gcProfile.id : defaultProfile.id, + profile_id: data.is_giftcard ? gcProfile?.id : defaultProfile?.id, price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE, data: data.data ?? {}, requirements: (data.requirements || []) as ShippingOptionRequirement[], diff --git a/integration-tests/helpers/admin-variants-seeder.js b/integration-tests/helpers/admin-variants-seeder.js index 9245746283..2698bb3bc5 100644 --- a/integration-tests/helpers/admin-variants-seeder.js +++ b/integration-tests/helpers/admin-variants-seeder.js @@ -106,6 +106,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-reg", title: "Multi Reg Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", status: "published", collection_id: "test-collection", diff --git a/integration-tests/helpers/cart-seeder.js b/integration-tests/helpers/cart-seeder.js index 6cd3f8aa8d..e6dfc0813c 100644 --- a/integration-tests/helpers/cart-seeder.js +++ b/integration-tests/helpers/cart-seeder.js @@ -444,14 +444,16 @@ module.exports = async (dataSource, data = {}) => { await manager.save(priceList1) - await manager.insert(Product, { + const giftCardProduct = manager.create(Product, { id: "giftcard-product", title: "Giftcard", is_giftcard: true, discountable: false, profile_id: gcProfile.id, + profiles: [{ id: gcProfile.id }], options: [{ id: "denom", title: "Denomination" }], }) + await manager.save(Product, giftCardProduct) await manager.insert(ProductVariant, { id: "giftcard-denom", @@ -466,12 +468,14 @@ module.exports = async (dataSource, data = {}) => { ], }) - await manager.insert(Product, { + const testProduct = manager.create(Product, { id: "test-product", title: "test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], options: [{ id: "test-option", title: "Size" }], }) + await manager.save(Product, testProduct) await manager.insert(ProductVariant, { id: "test-variant-quantity", diff --git a/integration-tests/helpers/draft-order-seeder.js b/integration-tests/helpers/draft-order-seeder.js index 91b289fa9f..9ed9ec47df 100644 --- a/integration-tests/helpers/draft-order-seeder.js +++ b/integration-tests/helpers/draft-order-seeder.js @@ -34,6 +34,7 @@ module.exports = async (dataSource, data = {}) => { id: "test-product", title: "test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], options: [{ id: "test-option", title: "Size" }], }) @@ -52,6 +53,7 @@ module.exports = async (dataSource, data = {}) => { id: "test-product-2", title: "test product 2", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], options: [{ id: "test-option-color", title: "Color" }], }) diff --git a/integration-tests/helpers/order-seeder.js b/integration-tests/helpers/order-seeder.js index 5f1ca7c9c5..a6d3c21108 100644 --- a/integration-tests/helpers/order-seeder.js +++ b/integration-tests/helpers/order-seeder.js @@ -33,6 +33,7 @@ module.exports = async (dataSource, data = {}) => { id: "test-product", title: "test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], options: [{ id: "test-option", title: "Size" }], }) diff --git a/integration-tests/helpers/price-selection-seeder.js b/integration-tests/helpers/price-selection-seeder.js index 03f6564335..a3a053a5d5 100644 --- a/integration-tests/helpers/price-selection-seeder.js +++ b/integration-tests/helpers/price-selection-seeder.js @@ -238,6 +238,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product", title: "Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], @@ -305,6 +306,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-quantity", title: "Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], @@ -413,6 +415,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-sale", title: "Test product sale", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], @@ -480,6 +483,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-sale-overlap", title: "Test product sale", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], @@ -540,6 +544,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-multi-region", title: "Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-multi-region-description1", collection_id: "test-collection", tags: [], @@ -606,6 +611,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-quantity-customer", title: "Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], @@ -713,6 +719,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-sale-customer", title: "Test product sale", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], @@ -781,6 +788,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product-sale-customer-quantity", title: "Test product sale", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", tags: [], diff --git a/integration-tests/helpers/product-seeder.js b/integration-tests/helpers/product-seeder.js index 5d2684724d..dffad8b68f 100644 --- a/integration-tests/helpers/product-seeder.js +++ b/integration-tests/helpers/product-seeder.js @@ -98,6 +98,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product", title: "Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", collection_id: "test-collection", type: { id: "test-type", value: "test-type" }, @@ -215,6 +216,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product1", title: "Test product1", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description1", collection_id: "test-collection", type: { id: "test-type", value: "test-type" }, @@ -283,6 +285,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product_filtering_1", title: "Test product filtering 1", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, collection_id: "test-collection1", @@ -297,6 +300,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product_filtering_2", title: "Test product filtering 2", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, collection_id: "test-collection2", @@ -311,6 +315,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product_filtering_3", title: "Test product filtering 3", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, collection_id: "test-collection1", @@ -325,6 +330,7 @@ module.exports = async (dataSource, data = {}) => { handle: "test-product_filtering_4", title: "Test product filtering 4", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", status: "proposed", deleted_at: new Date().toISOString(), diff --git a/integration-tests/helpers/store-product-seeder.js b/integration-tests/helpers/store-product-seeder.js index f2686e8ba5..30af9cdfe7 100644 --- a/integration-tests/helpers/store-product-seeder.js +++ b/integration-tests/helpers/store-product-seeder.js @@ -130,6 +130,7 @@ module.exports = async (dataSource, defaultSalesChannel) => { handle: "test-product", title: "Test product", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", status: "published", collection_id: "test-collection", @@ -323,6 +324,7 @@ module.exports = async (dataSource, defaultSalesChannel) => { handle: "test-product_filtering_1", title: "Test product filtering 1", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, collection_id: "test-collection1", @@ -338,6 +340,7 @@ module.exports = async (dataSource, defaultSalesChannel) => { handle: "test-product_filtering_2", title: "Test product filtering 2", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, collection_id: "test-collection2", @@ -353,6 +356,7 @@ module.exports = async (dataSource, defaultSalesChannel) => { handle: "test-product_filtering_3", title: "Test product filtering 3", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, collection_id: "test-collection1", @@ -369,6 +373,7 @@ module.exports = async (dataSource, defaultSalesChannel) => { is_giftcard: true, title: "giftcard", profile_id: defaultProfile.id, + profiles: [{ id: defaultProfile.id }], description: "test-product-description", type: { id: "test-type", value: "test-type" }, status: "published", diff --git a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap index e688ddc941..1330dbea87 100644 --- a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap +++ b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap @@ -83,7 +83,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -240,7 +242,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -395,7 +399,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -515,7 +521,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -693,7 +701,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -861,7 +871,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -1112,7 +1124,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -1361,7 +1375,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -1570,7 +1586,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -1690,7 +1708,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -1868,7 +1888,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -2030,7 +2052,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -2139,7 +2163,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -2255,7 +2281,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, @@ -2372,7 +2400,9 @@ Object { "metadata": null, "mid_code": null, "origin_country": null, + "profile": Any, "profile_id": Any, + "profiles": Any, "status": "draft", "subtitle": null, "thumbnail": null, diff --git a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js index 99031e696b..5feddd8e4d 100644 --- a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js +++ b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js @@ -120,6 +120,8 @@ describe("medusa-plugin-sendgrid", () => { updated_at: expect.any(Date), product: { profile_id: expect.any(String), + profile: expect.any(Object), + profiles: expect.any(Array), created_at: expect.any(Date), updated_at: expect.any(Date), }, @@ -242,6 +244,8 @@ describe("medusa-plugin-sendgrid", () => { updated_at: expect.any(Date), product: { profile_id: expect.any(String), + profile: expect.any(Object), + profiles: expect.any(Array), created_at: expect.any(Date), updated_at: expect.any(Date), }, @@ -303,6 +307,8 @@ describe("medusa-plugin-sendgrid", () => { updated_at: expect.any(Date), product: { profile_id: expect.any(String), + profile: expect.any(Object), + profiles: expect.any(Array), created_at: expect.any(Date), updated_at: expect.any(Date), }, @@ -485,6 +491,8 @@ describe("medusa-plugin-sendgrid", () => { updated_at: expect.any(Date), product: { profile_id: expect.any(String), + profile: expect.any(Object), + profiles: expect.any(Array), created_at: expect.any(Date), updated_at: expect.any(Date), }, @@ -592,6 +600,8 @@ describe("medusa-plugin-sendgrid", () => { updated_at: expect.any(Date), product: { profile_id: expect.any(String), + profile: expect.any(Object), + profiles: expect.any(Array), created_at: expect.any(Date), updated_at: expect.any(Date), }, @@ -780,6 +790,8 @@ const getReturnSnap = (received = false) => { updated_at: expect.any(Date), product: { profile_id: expect.any(String), + profile: expect.any(Object), + profiles: expect.any(Array), created_at: expect.any(Date), updated_at: expect.any(Date), }, diff --git a/integration-tests/plugins/__tests__/product/admin/index.ts b/integration-tests/plugins/__tests__/product/admin/index.ts new file mode 100644 index 0000000000..aeab952e3a --- /dev/null +++ b/integration-tests/plugins/__tests__/product/admin/index.ts @@ -0,0 +1,351 @@ +import path from "path" +import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { setPort, useApi } from "../../../../environment-helpers/use-api" +import { initDb, useDb } from "../../../../environment-helpers/use-db" + +import adminSeeder from "../../../../helpers/admin-seeder" +import productSeeder from "../../../../helpers/product-seeder" + +import { simpleSalesChannelFactory } from "../../../../factories" +import { AxiosInstance } from "axios" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + Authorization: "Bearer test_token", + }, +} + +describe("/admin/products", () => { + let medusaProcess + let dbConnection + let express + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd } as any) + const { app, port } = await bootstrapApp({ cwd }) + setPort(port) + express = app.listen(port, () => { + process.send?.(port) + }) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("POST /admin/products", () => { + beforeEach(async () => { + await productSeeder(dbConnection) + await adminSeeder(dbConnection) + + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create a product", async () => { + const api = useApi()! as AxiosInstance + + const payload = { + title: "Test", + description: "test-product-description", + type: { value: "test-type" }, + images: ["test-image.png", "test-image-2.png"], + collection_id: "test-collection", + tags: [{ value: "123" }, { value: "456" }], + options: [{ title: "size" }, { title: "color" }], + variants: [ + { + title: "Test variant", + inventory_quantity: 10, + prices: [ + { + currency_code: "usd", + amount: 100, + }, + { + currency_code: "eur", + amount: 45, + }, + { + currency_code: "dkk", + amount: 30, + }, + ], + options: [{ value: "large" }, { value: "green" }], + }, + ], + } + + const response = await api + .post("/admin/products", payload, adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response?.status).toEqual(200) + expect(response?.data.product).toEqual( + expect.objectContaining({ + id: expect.stringMatching(/^prod_*/), + title: "Test", + discountable: true, + is_giftcard: false, + handle: "test", + status: "draft", + created_at: expect.any(String), + updated_at: expect.any(String), + profile_id: expect.stringMatching(/^sp_*/), + images: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + url: "test-image.png", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: expect.any(String), + url: "test-image-2.png", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + thumbnail: "test-image.png", + tags: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + value: "123", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: expect.any(String), + value: "456", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + type: expect.objectContaining({ + value: "test-type", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + collection: expect.objectContaining({ + id: "test-collection", + title: "Test collection", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^opt_*/), + product_id: expect.stringMatching(/^prod_*/), + title: "size", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: expect.stringMatching(/^opt_*/), + product_id: expect.stringMatching(/^prod_*/), + title: "color", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^variant_*/), + product_id: expect.stringMatching(/^prod_*/), + updated_at: expect.any(String), + created_at: expect.any(String), + title: "Test variant", + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^ma_*/), + currency_code: "usd", + amount: 100, + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + }), + expect.objectContaining({ + id: expect.stringMatching(/^ma_*/), + currency_code: "eur", + amount: 45, + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + }), + expect.objectContaining({ + id: expect.stringMatching(/^ma_*/), + currency_code: "dkk", + amount: 30, + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + value: "large", + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + option_id: expect.stringMatching(/^opt_*/), + id: expect.stringMatching(/^optval_*/), + }), + expect.objectContaining({ + value: "green", + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + option_id: expect.stringMatching(/^opt_*/), + id: expect.stringMatching(/^optval_*/), + }), + ]), + }), + ]), + }) + ) + }) + + it("should create a product that is not discountable", async () => { + const api = useApi()! as AxiosInstance + + const payload = { + title: "Test", + discountable: false, + description: "test-product-description", + type: { value: "test-type" }, + images: ["test-image.png", "test-image-2.png"], + collection_id: "test-collection", + tags: [{ value: "123" }, { value: "456" }], + options: [{ title: "size" }, { title: "color" }], + variants: [ + { + title: "Test variant", + inventory_quantity: 10, + prices: [{ currency_code: "usd", amount: 100 }], + options: [{ value: "large" }, { value: "green" }], + }, + ], + } + + const response = await api + .post("/admin/products", payload, adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response?.status).toEqual(200) + expect(response?.data.product).toEqual( + expect.objectContaining({ + discountable: false, + }) + ) + }) + + it("should sets the variant ranks when creating a product", async () => { + const api = useApi()! as AxiosInstance + + const payload = { + title: "Test product - 1", + description: "test-product-description 1", + type: { value: "test-type 1" }, + images: ["test-image.png", "test-image-2.png"], + collection_id: "test-collection", + tags: [{ value: "123" }, { value: "456" }], + options: [{ title: "size" }, { title: "color" }], + variants: [ + { + title: "Test variant 1", + inventory_quantity: 10, + prices: [{ currency_code: "usd", amount: 100 }], + options: [{ value: "large" }, { value: "green" }], + }, + { + title: "Test variant 2", + inventory_quantity: 10, + prices: [{ currency_code: "usd", amount: 100 }], + options: [{ value: "large" }, { value: "green" }], + }, + ], + } + + const creationResponse = await api + .post("/admin/products", payload, adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(creationResponse?.status).toEqual(200) + + const productId = creationResponse?.data.product.id + + const response = await api + .get(`/admin/products/${productId}`, adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response?.data.product).toEqual( + expect.objectContaining({ + title: "Test product - 1", + variants: [ + expect.objectContaining({ + title: "Test variant 1", + }), + expect.objectContaining({ + title: "Test variant 2", + }), + ], + }) + ) + }) + + it("should create a giftcard", async () => { + const api = useApi()! as AxiosInstance + + const payload = { + title: "Test Giftcard", + is_giftcard: true, + description: "test-giftcard-description", + options: [{ title: "Denominations" }], + variants: [ + { + title: "Test variant", + prices: [{ currency_code: "usd", amount: 100 }], + options: [{ value: "100" }], + }, + ], + } + + const response = await api + .post("/admin/products", payload, adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response?.status).toEqual(200) + + expect(response?.data.product).toEqual( + expect.objectContaining({ + title: "Test Giftcard", + discountable: false, + }) + ) + }) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index d6cbf105ba..7cd5e423d1 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -2,6 +2,7 @@ const DB_HOST = process.env.DB_HOST const DB_USERNAME = process.env.DB_USERNAME const DB_PASSWORD = process.env.DB_PASSWORD const DB_NAME = process.env.DB_TEMP_NAME +const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}` module.exports = { plugins: [ @@ -23,7 +24,7 @@ module.exports = { ], projectConfig: { // redis_url: REDIS_URL, - database_url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`, + database_url: DB_URL, database_type: "postgres", jwt_secret: "test", cookie_secret: "test", @@ -44,5 +45,16 @@ module.exports = { resolve: "@medusajs/cache-inmemory", options: { ttl: 5 }, }, + productModuleService: { + scope: "internal", + resources: "isolated", + resolve: "@medusajs/product", + options: { + database: { + clientUrl: DB_URL, + debug: false, + }, + }, + }, }, } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 434dfc5e9d..f87092336e 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -12,6 +12,7 @@ "@medusajs/cache-inmemory": "workspace:*", "@medusajs/event-bus-local": "workspace:*", "@medusajs/medusa": "workspace:*", + "@medusajs/product": "workspace:^", "faker": "^5.5.3", "medusa-fulfillment-webshipper": "workspace:*", "medusa-interfaces": "workspace:*", diff --git a/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts b/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts index b39bbc4a3e..3bef60c20b 100644 --- a/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts +++ b/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts @@ -16,19 +16,12 @@ const orderRelations = [ "returns.shipping_method", "returns.shipping_method.tax_lines", "refunds", - "claims", - "claims.claim_items", "claims.claim_items.item", "claims.fulfillments", "claims.return_order", - "claims.additional_items", - "claims.additional_items.variant", - "claims.additional_items.variant.product", - "swaps", + "claims.additional_items.variant.product.profiles", "swaps.return_order", - "swaps.additional_items", - "swaps.additional_items.variant", - "swaps.additional_items.variant.product", + "swaps.additional_items.variant.product.profiles", "swaps.fulfillments", "returnable_items", ] diff --git a/packages/generated/client-types/src/lib/models/AdminDraftOrdersRes.ts b/packages/generated/client-types/src/lib/models/AdminDraftOrdersRes.ts index 6c8d1fe048..b2be42c75a 100644 --- a/packages/generated/client-types/src/lib/models/AdminDraftOrdersRes.ts +++ b/packages/generated/client-types/src/lib/models/AdminDraftOrdersRes.ts @@ -7,6 +7,7 @@ import type { Cart } from "./Cart" import type { Discount } from "./Discount" import type { DraftOrder } from "./DraftOrder" import type { LineItem } from "./LineItem" +import type { Product } from "./Product" import type { ProductVariant } from "./ProductVariant" import type { Region } from "./Region" import type { ShippingMethod } from "./ShippingMethod" @@ -58,7 +59,12 @@ export interface AdminDraftOrdersRes { | "variant" >, { - variant: SetRelation + variant: Merge< + SetRelation, + { + product: SetRelation + } + > } > > diff --git a/packages/generated/client-types/src/lib/models/AdminOrdersListRes.ts b/packages/generated/client-types/src/lib/models/AdminOrdersListRes.ts index e48039db3b..b5513fc881 100644 --- a/packages/generated/client-types/src/lib/models/AdminOrdersListRes.ts +++ b/packages/generated/client-types/src/lib/models/AdminOrdersListRes.ts @@ -10,6 +10,7 @@ import type { Fulfillment } from "./Fulfillment" import type { GiftCardTransaction } from "./GiftCardTransaction" import type { LineItem } from "./LineItem" import type { Order } from "./Order" +import type { Product } from "./Product" import type { ProductVariant } from "./ProductVariant" import type { Region } from "./Region" import type { Return } from "./Return" @@ -112,7 +113,12 @@ export interface AdminOrdersListRes { | "variant" >, { - variant: SetRelation + variant: Merge< + SetRelation, + { + product: SetRelation + } + > } > > diff --git a/packages/generated/client-types/src/lib/models/AdminOrdersRes.ts b/packages/generated/client-types/src/lib/models/AdminOrdersRes.ts index 95af1c0bcb..275bd55a5f 100644 --- a/packages/generated/client-types/src/lib/models/AdminOrdersRes.ts +++ b/packages/generated/client-types/src/lib/models/AdminOrdersRes.ts @@ -10,6 +10,7 @@ import type { Fulfillment } from "./Fulfillment" import type { GiftCardTransaction } from "./GiftCardTransaction" import type { LineItem } from "./LineItem" import type { Order } from "./Order" +import type { Product } from "./Product" import type { ProductVariant } from "./ProductVariant" import type { Region } from "./Region" import type { Return } from "./Return" @@ -109,7 +110,12 @@ export interface AdminOrdersRes { | "variant" >, { - variant: SetRelation + variant: Merge< + SetRelation, + { + product: SetRelation + } + > } > > diff --git a/packages/generated/client-types/src/lib/models/Product.ts b/packages/generated/client-types/src/lib/models/Product.ts index 295cb58633..67ce82dc8e 100644 --- a/packages/generated/client-types/src/lib/models/Product.ts +++ b/packages/generated/client-types/src/lib/models/Product.ts @@ -73,6 +73,10 @@ export interface Product { * Available if the relation `profile` is expanded. */ profile?: ShippingProfile | null + /** + * Available if the relation `profiles` is expanded. + */ + profiles?: Array | null /** * The weight of the Product Variant. May be used in shipping rate calculations. */ diff --git a/packages/generated/client-types/src/lib/models/StoreCartsRes.ts b/packages/generated/client-types/src/lib/models/StoreCartsRes.ts index c5864d0ada..ed26cb8f96 100644 --- a/packages/generated/client-types/src/lib/models/StoreCartsRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreCartsRes.ts @@ -6,6 +6,7 @@ import { SetRelation, Merge } from "../core/ModelUtils" import type { Cart } from "./Cart" import type { Discount } from "./Discount" import type { LineItem } from "./LineItem" +import type { Product } from "./Product" import type { ProductVariant } from "./ProductVariant" import type { Region } from "./Region" import type { ShippingMethod } from "./ShippingMethod" @@ -54,7 +55,12 @@ export interface StoreCartsRes { | "tax_lines" >, { - variant: SetRelation + variant: Merge< + SetRelation, + { + product: SetRelation + } + > } > > diff --git a/packages/generated/client-types/src/lib/models/StoreCustomersListOrdersRes.ts b/packages/generated/client-types/src/lib/models/StoreCustomersListOrdersRes.ts index 72957782db..dce71ad18f 100644 --- a/packages/generated/client-types/src/lib/models/StoreCustomersListOrdersRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreCustomersListOrdersRes.ts @@ -9,6 +9,7 @@ import type { Fulfillment } from "./Fulfillment" import type { GiftCardTransaction } from "./GiftCardTransaction" import type { LineItem } from "./LineItem" import type { Order } from "./Order" +import type { Product } from "./Product" import type { ProductVariant } from "./ProductVariant" import type { Region } from "./Region" import type { ShippingMethod } from "./ShippingMethod" @@ -63,7 +64,12 @@ export interface StoreCustomersListOrdersRes { | "tax_lines" >, { - variant: SetRelation + variant: Merge< + SetRelation, + { + product: SetRelation + } + > } > > diff --git a/packages/generated/client-types/src/lib/models/StoreOrdersRes.ts b/packages/generated/client-types/src/lib/models/StoreOrdersRes.ts index f60b33407a..546e5cfa98 100644 --- a/packages/generated/client-types/src/lib/models/StoreOrdersRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreOrdersRes.ts @@ -9,6 +9,7 @@ import type { Fulfillment } from "./Fulfillment" import type { GiftCardTransaction } from "./GiftCardTransaction" import type { LineItem } from "./LineItem" import type { Order } from "./Order" +import type { Product } from "./Product" import type { ProductVariant } from "./ProductVariant" import type { Region } from "./Region" import type { ShippingMethod } from "./ShippingMethod" @@ -62,7 +63,12 @@ export interface StoreOrdersRes { | "tax_lines" >, { - variant: SetRelation + variant: Merge< + SetRelation, + { + product: SetRelation + } + > } > > diff --git a/packages/medusa-payment-klarna/src/api/routes/hooks/address.js b/packages/medusa-payment-klarna/src/api/routes/hooks/address.js index 09c2e58d63..ef70598cea 100644 --- a/packages/medusa-payment-klarna/src/api/routes/hooks/address.js +++ b/packages/medusa-payment-klarna/src/api/routes/hooks/address.js @@ -42,9 +42,7 @@ export default async (req, res) => { "region", "shipping_methods", "shipping_methods.shipping_option", - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", ], }) const shippingOptions = await shippingProfileService.fetchCartOptions(cart) diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index 98757a4e05..e47642e6c9 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -264,6 +264,7 @@ class SendGridService extends NotificationService { } let status + await SendGrid.send(sendOptions) .then(() => { status = "sent" @@ -613,11 +614,8 @@ class SendGridService extends NotificationService { // Fetch the return request const returnRequest = await this.returnService_.retrieve(return_id, { relations: [ - "items", - "items.item", "items.item.tax_lines", - "items.item.variant", - "items.item.variant.product", + "items.item.variant.product.profiles", "shipping_method", "shipping_method.tax_lines", "shipping_method.shipping_option", @@ -628,7 +626,9 @@ class SendGridService extends NotificationService { { id: returnRequest.items.map(({ item_id }) => item_id), }, - { relations: ["tax_lines", "variant", "variant.product"] } + { + relations: ["tax_lines", "variant", "variant.product.profiles"], + } ) // Fetch the order @@ -774,7 +774,7 @@ class SendGridService extends NotificationService { }) const cart = await this.cartService_.retrieve(swap.cart_id, { - relations: ["items", "items.variant", "items.variant.product"], + relations: ["items.variant.product.profiles"], select: [ "total", "tax_total", @@ -855,8 +855,7 @@ class SendGridService extends NotificationService { }) const swap = await this.swapService_.retrieve(id, { relations: [ - "additional_items", - "additional_items.variant.product", + "additional_items.variant.product.profiles", "additional_items.tax_lines", "return_order", "return_order.items", @@ -873,7 +872,7 @@ class SendGridService extends NotificationService { id: returnRequest.items.map(({ item_id }) => item_id), }, { - relations: ["tax_lines", "variant", "variant.product"], + relations: ["tax_lines", "variant.product.profiles"], } ) @@ -893,9 +892,7 @@ class SendGridService extends NotificationService { const order = await this.orderService_.retrieve(swap.order_id, { select: ["total"], relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "items.tax_lines", "discounts", "discounts.rule", @@ -915,7 +912,7 @@ class SendGridService extends NotificationService { "shipping_total", "subtotal", ], - relations: ["items", "items.variant", "items.variant.product"], + relations: ["items.variant.product.profiles"], }) const currencyCode = order.currency_code.toUpperCase() @@ -997,9 +994,7 @@ class SendGridService extends NotificationService { "shipping_methods", "shipping_methods.shipping_option", "shipping_methods.tax_lines", - "additional_items", - "additional_items.variant", - "additional_items.variant.product", + "additional_items.variant.product.profiles", "additional_items.tax_lines", "return_order", "return_order.items", @@ -1011,14 +1006,11 @@ class SendGridService extends NotificationService { "region", "items", "items.tax_lines", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "discounts", "discounts.rule", "swaps", - "swaps.additional_items", - "swaps.additional_items.variant", - "swaps.additional_items.variant.product", + "swaps.additional_items.variant.product.profiles", "swaps.additional_items.tax_lines", ], }) @@ -1031,7 +1023,7 @@ class SendGridService extends NotificationService { "shipping_total", "subtotal", ], - relations: ["items", "items.variant", "items.variant.product"], + relations: ["items.variant.product.profiles"], }) const returnRequest = swap.return_order @@ -1040,7 +1032,7 @@ class SendGridService extends NotificationService { id: returnRequest.items.map(({ item_id }) => item_id), }, { - relations: ["tax_lines", "variant", "variant.product"], + relations: ["tax_lines", "variant.product.profiles"], } ) @@ -1147,10 +1139,7 @@ class SendGridService extends NotificationService { async claimShipmentCreatedData({ id, fulfillment_id }) { const claim = await this.claimService_.retrieve(id, { relations: [ - "order", - "order.items", - "order.items.variant", - "order.items.variant.product", + "order.items.variant.product.profiles", "order.shipping_address", ], }) diff --git a/packages/medusa/src/api/routes/admin/collections/index.ts b/packages/medusa/src/api/routes/admin/collections/index.ts index f17db5b76b..e2729ba78e 100644 --- a/packages/medusa/src/api/routes/admin/collections/index.ts +++ b/packages/medusa/src/api/routes/admin/collections/index.ts @@ -67,7 +67,7 @@ export const defaultAdminCollectionsFields = [ "created_at", "updated_at", ] -export const defaultAdminCollectionsRelations = ["products"] +export const defaultAdminCollectionsRelations = ["products.profiles"] /** * @schema AdminCollectionsListRes diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js index 1bbe2dc0ae..6fd72d8932 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js @@ -48,7 +48,7 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => { "metadata", "valid_duration", ], - relations: ["rule", "parent_discount", "regions", "rule.conditions"], + relations: ["parent_discount", "regions", "rule.conditions"], } ) diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js index 9522708e68..978e843415 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js @@ -20,12 +20,7 @@ const defaultFields = [ "valid_duration", ] -const defaultRelations = [ - "rule", - "parent_discount", - "regions", - "rule.conditions", -] +const defaultRelations = ["parent_discount", "regions", "rule.conditions"] describe("GET /admin/discounts/:discount_id", () => { describe("successful retrieval", () => { diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js index 44662e7873..613397cbb3 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js @@ -20,12 +20,7 @@ const defaultFields = [ "valid_duration", ] -const defaultRelations = [ - "rule", - "parent_discount", - "regions", - "rule.conditions", -] +const defaultRelations = ["parent_discount", "regions", "rule.conditions"] describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => { describe("successful removal", () => { diff --git a/packages/medusa/src/api/routes/admin/discounts/index.ts b/packages/medusa/src/api/routes/admin/discounts/index.ts index e1e4b5ea39..5582a3ba52 100644 --- a/packages/medusa/src/api/routes/admin/discounts/index.ts +++ b/packages/medusa/src/api/routes/admin/discounts/index.ts @@ -209,7 +209,6 @@ export const defaultAdminDiscountsFields: (keyof Discount)[] = [ ] export const defaultAdminDiscountsRelations = [ - "rule", "parent_discount", "regions", "rule.conditions", diff --git a/packages/medusa/src/api/routes/admin/draft-orders/index.ts b/packages/medusa/src/api/routes/admin/draft-orders/index.ts index 616079750a..171dbf47d5 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/index.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/index.ts @@ -139,6 +139,7 @@ export type AdminPostDraftOrdersDraftOrderRegisterPaymentRes = { * - cart.items.tax_lines * - cart.items.variant * - cart.items.variant.product + * - cart.items.variant.product.profiles * - cart.region * - cart.region.tax_rates * - cart.shipping_address diff --git a/packages/medusa/src/api/routes/admin/orders/index.ts b/packages/medusa/src/api/routes/admin/orders/index.ts index 0e28ec739b..6045b8d0c7 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.ts +++ b/packages/medusa/src/api/routes/admin/orders/index.ts @@ -543,6 +543,7 @@ export default (app, featureFlagRouter: FlagRouter) => { * - items.tax_lines * - items.variant * - items.variant.product + * - items.variant.product.profiles * - refunds * - region * - shipping_methods @@ -659,6 +660,7 @@ export type AdminOrdersRes = { * - items.tax_lines * - items.variant * - items.variant.product + * - items.variant.product.profiles * - refunds * - region * - shipping_methods diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js index 87a54663cc..edf018afba 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js @@ -40,7 +40,6 @@ describe("GET /admin/products/:id", () => { "is_giftcard", "discountable", "thumbnail", - "profile_id", "collection_id", "type_id", "weight", @@ -60,6 +59,7 @@ describe("GET /admin/products/:id", () => { "variants", "variants.prices", "variants.options", + "profiles", "images", "options", "tags", diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index a6f737b67f..fb7f23678f 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -42,6 +42,7 @@ import { createVariantsTransaction, revertVariantTransaction, } from "./transaction/create-product-variant" +import { createProductsWorkflow } from "../../../../workflows/admin/create-products" /** * @oas [post] /admin/products @@ -129,6 +130,19 @@ export default async (req, res) => { ) const entityManager: EntityManager = req.scope.resolve("manager") + const productModuleService = req.scope.resolve("productModuleService") + + if (productModuleService) { + const products = await createProductsWorkflow( + { + container: req.scope, + manager: entityManager, + }, + [validated] + ) + + return res.json({ product: products[0] }) + } const product = await entityManager.transaction(async (manager) => { const { variants } = validated diff --git a/packages/medusa/src/api/routes/admin/products/index.ts b/packages/medusa/src/api/routes/admin/products/index.ts index 08ee8f539b..e7ad218476 100644 --- a/packages/medusa/src/api/routes/admin/products/index.ts +++ b/packages/medusa/src/api/routes/admin/products/index.ts @@ -101,6 +101,7 @@ export const defaultAdminProductRelations = [ "variants", "variants.prices", "variants.options", + "profiles", "images", "options", "tags", @@ -119,7 +120,6 @@ export const defaultAdminProductFields: (keyof Product)[] = [ "is_giftcard", "discountable", "thumbnail", - "profile_id", "collection_id", "type_id", "weight", diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js index 2074baa787..71ff42b8c8 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/__tests__/get-shipping-profile.js @@ -12,7 +12,7 @@ const defaultFields = [ "metadata", ] -const defaultRelations = ["products", "shipping_options"] +const defaultRelations = ["products.profiles", "shipping_options"] describe("GET /admin/shipping-profiles/:profile_id", () => { describe("successful retrieval", () => { diff --git a/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts b/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts index e88ed1caf4..b9d177aea8 100644 --- a/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts +++ b/packages/medusa/src/api/routes/admin/shipping-profiles/index.ts @@ -41,8 +41,10 @@ export const defaultAdminShippingProfilesFields: (keyof ShippingProfile)[] = [ "metadata", ] -export const defaultAdminShippingProfilesRelations: (keyof ShippingProfile)[] = - ["products", "shipping_options"] +export const defaultAdminShippingProfilesRelations: string[] = [ + "products.profiles", + "shipping_options", +] /** * @schema AdminDeleteShippingProfileRes diff --git a/packages/medusa/src/api/routes/store/carts/index.ts b/packages/medusa/src/api/routes/store/carts/index.ts index c443d69603..cdc2cecdca 100644 --- a/packages/medusa/src/api/routes/store/carts/index.ts +++ b/packages/medusa/src/api/routes/store/carts/index.ts @@ -262,6 +262,7 @@ export const defaultStoreCartRelations = [ * - items * - items.variant * - items.variant.product + * - items.variant.product.profiles * - items.tax_lines * - items.adjustments * - gift_cards diff --git a/packages/medusa/src/api/routes/store/customers/index.ts b/packages/medusa/src/api/routes/store/customers/index.ts index ab61aa1f4d..41ffcc2fa3 100644 --- a/packages/medusa/src/api/routes/store/customers/index.ts +++ b/packages/medusa/src/api/routes/store/customers/index.ts @@ -181,6 +181,7 @@ export type StoreCustomersResetPasswordRes = { * - items.tax_lines * - items.variant * - items.variant.product + * - items.variant.product.profiles * - refunds * - region * - shipping_address diff --git a/packages/medusa/src/api/routes/store/orders/index.ts b/packages/medusa/src/api/routes/store/orders/index.ts index eca95e5bdb..09a1c97251 100644 --- a/packages/medusa/src/api/routes/store/orders/index.ts +++ b/packages/medusa/src/api/routes/store/orders/index.ts @@ -168,6 +168,7 @@ export const allowedStoreOrdersFields = [ * - items.tax_lines * - items.variant * - items.variant.product + * - items.variant.product.profiles * - refunds * - region * - shipping_methods diff --git a/packages/medusa/src/api/routes/store/products/index.ts b/packages/medusa/src/api/routes/store/products/index.ts index 48b63e77c7..1d579d9fa5 100644 --- a/packages/medusa/src/api/routes/store/products/index.ts +++ b/packages/medusa/src/api/routes/store/products/index.ts @@ -65,6 +65,7 @@ export const defaultStoreProductsRelations = [ "tags", "collection", "type", + "profiles", ] export const defaultStoreProductsFields: (keyof Product)[] = [ @@ -78,7 +79,6 @@ export const defaultStoreProductsFields: (keyof Product)[] = [ "is_giftcard", "discountable", "thumbnail", - "profile_id", "collection_id", "type_id", "weight", @@ -97,7 +97,10 @@ export const defaultStoreProductsFields: (keyof Product)[] = [ export const allowedStoreProductsFields = [ ...defaultStoreProductsFields, - // TODO: order prop validation + // profile_id is not a column in the products table, so it should be ignored as it + // will be rejected by typeorm as invalid, though, it is an entity property + // that we want to return, so it part of the allowedStoreProductsFields + "profile_id", "variants.title", "variants.prices.amount", ] diff --git a/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js b/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js index ae64176c9c..1b77104b06 100644 --- a/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js +++ b/packages/medusa/src/api/routes/store/shipping-options/__tests__/list-shipping-options.js @@ -21,10 +21,7 @@ describe("GET /store/shipping-options", () => { it("calls CartService retrieve", () => { expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(1) expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledWith( - IdMap.getId("emptyCart"), - { - relations: ["items.variant", "items.variant.product"], - } + IdMap.getId("emptyCart") ) }) diff --git a/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.ts b/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.ts index 094df7a610..e2cd920a6a 100644 --- a/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.ts +++ b/packages/medusa/src/api/routes/store/shipping-options/list-shipping-options.ts @@ -53,12 +53,8 @@ export default async (req, res) => { "shippingProfileService" ) - const cart = await cartService.retrieveWithTotals(cart_id, { - relations: ["items.variant", "items.variant.product"], - }) - + const cart = await cartService.retrieveWithTotals(cart_id) const options = await shippingProfileService.fetchCartOptions(cart) - const data = await pricingService.setShippingOptionPrices(options, { cart_id, }) diff --git a/packages/medusa/src/api/routes/store/swaps/create-swap.ts b/packages/medusa/src/api/routes/store/swaps/create-swap.ts index ad902b5977..cc8633b757 100644 --- a/packages/medusa/src/api/routes/store/swaps/create-swap.ts +++ b/packages/medusa/src/api/routes/store/swaps/create-swap.ts @@ -139,13 +139,9 @@ export default async (req, res) => { .retrieve(swapDto.order_id, { select: ["refunded_total", "total"], relations: [ - "items", "items.variant", "items.tax_lines", - "swaps", - "swaps.additional_items", - "swaps.additional_items.variant", - "swaps.additional_items.variant.product", + "swaps.additional_items.variant.product.profiles", "swaps.additional_items.tax_lines", ], }) diff --git a/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts b/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts new file mode 100644 index 0000000000..2a081010a4 --- /dev/null +++ b/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addTableProductShippingProfile1680857773273 + implements MigrationInterface +{ + name = "addTableProductShippingProfile1680857773273" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + CREATE TABLE IF NOT EXISTS "product_shipping_profile" + ( + "profile_id" text NOT NULL, + "product_id" text NOT NULL + ); + + INSERT INTO "product_shipping_profile" ("profile_id", "product_id") + SELECT "profile_id", "id" FROM "product"; + + ALTER TABLE "product" DROP COLUMN IF EXISTS "profile_id"; + CREATE UNIQUE INDEX IF NOT EXISTS "idx_product_shipping_profile_profile_id_product_id_unique" ON "product_shipping_profile" ("profile_id", "product_id"); + CREATE INDEX IF NOT EXISTS "idx_product_shipping_profile_product_id" ON "product_shipping_profile" ("product_id"); + CREATE INDEX IF NOT EXISTS "idx_product_shipping_profile_profile_id" ON "product_shipping_profile" ("profile_id"); + DROP INDEX IF EXISTS "IDX_80823b7ae866dc5acae2dac6d2"; + ` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX IF EXISTS "idx_product_shipping_profile_profile_id_product_id_unique"; + DROP INDEX IF EXISTS "idx_product_shipping_profile_product_id"; + DROP INDEX IF EXISTS "idx_product_shipping_profile_profile_id"; + + DROP TABLE IF EXISTS "product_shipping_profile"; + + ALTER TABLE "product" ADD COLUMN IF NOT EXISTS "profile_id"; + + UPDATE "product" SET "profile_id" = "product_shipping_profile"."profile_id" + FROM "product_shipping_profile" + WHERE "product"."id" = "product_shipping_profile"."product_id"; + + ALTER TABLE "product" ALTER COLUMN profile_id SET NOT NULL; + + CREATE INDEX IF NOT EXISTS "IDX_80823b7ae866dc5acae2dac6d2" ON "product" ("profile_id"); + ALTER TABLE "product" ADD CONSTRAINT "FK_80823b7ae866dc5acae2dac6d2c" FOREIGN KEY ("profile_id") REFERENCES "shipping_profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/migrations/1680857773273-drop-product-id-fk-sales-channels.ts b/packages/medusa/src/migrations/1680857773273-drop-product-id-fk-sales-channels.ts new file mode 100644 index 0000000000..57cffea2b9 --- /dev/null +++ b/packages/medusa/src/migrations/1680857773273-drop-product-id-fk-sales-channels.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class dropProductIdFkSalesChannels1680857773273 + implements MigrationInterface +{ + name = "dropProductIdFkSalesChannels1680857773273" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + alter table if exists "product_sales_channel" drop constraint if exists "FK_5a4d5e1e60f97633547821ec8d6"; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE if exists "product_sales_channel" ADD CONSTRAINT if not exists "FK_5a4d5e1e60f97633547821ec8d6" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE cascade ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/migrations/1680857773273-drop-variant-id-fk-money-amount.ts b/packages/medusa/src/migrations/1680857773273-drop-variant-id-fk-money-amount.ts new file mode 100644 index 0000000000..f544ce52ec --- /dev/null +++ b/packages/medusa/src/migrations/1680857773273-drop-variant-id-fk-money-amount.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class dropVariantIdFkMoneyAmount1680857773273 + implements MigrationInterface +{ + name = "dropVariantIdFkMoneyAmount1680857773273" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + alter table if exists "money_amount" drop constraint if exists "FK_17a06d728e4cfbc5bd2ddb70af0"; + ` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE if exists "money_amount" ADD CONSTRAINT if not exists "FK_17a06d728e4cfbc5bd2ddb70af0" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE cascade ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index bd09677cc7..c00d601e6c 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -1,5 +1,7 @@ import { + AfterLoad, BeforeInsert, + BeforeUpdate, Column, Entity, Index, @@ -23,7 +25,7 @@ import { SalesChannel } from "./sales-channel" import { ShippingProfile } from "./shipping-profile" import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" import _ from "lodash" -import { generateEntityId } from "../utils/generate-entity-id" +import { generateEntityId } from "../utils" export enum ProductStatus { DRAFT = "draft", @@ -92,14 +94,26 @@ export class Product extends SoftDeletableEntity { }) categories: ProductCategory[] - @Index() - @Column() profile_id: string - @ManyToOne(() => ShippingProfile) - @JoinColumn({ name: "profile_id" }) profile: ShippingProfile + @ManyToMany(() => ShippingProfile, { + cascade: ["remove", "soft-remove"], + }) + @JoinTable({ + name: "product_shipping_profile", + joinColumn: { + name: "product_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "profile_id", + referencedColumnName: "id", + }, + }) + profiles: ShippingProfile[] + @Column({ type: "int", nullable: true }) weight: number | null @@ -185,6 +199,25 @@ export class Product extends SoftDeletableEntity { if (!this.handle) { this.handle = _.kebabCase(this.title) } + + if (this.profile_id) { + this.profiles = [{ id: this.profile_id }] as ShippingProfile[] + } + } + + @BeforeUpdate() + private beforeUpdate(): void { + if (this.profile_id) { + this.profiles = [{ id: this.profile_id }] as ShippingProfile[] + } + } + + @AfterLoad() + private afterLoad(): void { + if (this.profiles) { + this.profile = this.profiles[this.profiles.length - 1]! + this.profile_id = this.profile?.id + } } } @@ -288,6 +321,12 @@ export class Product extends SoftDeletableEntity { * description: Available if the relation `profile` is expanded. * nullable: true * $ref: "#/components/schemas/ShippingProfile" + * profiles: + * description: Available if the relation `profiles` is expanded. + * nullable: true + * type: array + * items: + * $ref: "#/components/schemas/ShippingProfile" * weight: * description: The weight of the Product Variant. May be used in shipping rate calculations. * nullable: true diff --git a/packages/medusa/src/models/shipping-profile.ts b/packages/medusa/src/models/shipping-profile.ts index abdc4bb8e3..ac89702618 100644 --- a/packages/medusa/src/models/shipping-profile.ts +++ b/packages/medusa/src/models/shipping-profile.ts @@ -1,10 +1,16 @@ -import { BeforeInsert, Column, Entity, OneToMany } from "typeorm" +import { + BeforeInsert, + Column, + Entity, + JoinTable, + ManyToMany, + OneToMany, +} from "typeorm" -import { DbAwareColumn } from "../utils/db-aware-column" +import { DbAwareColumn, generateEntityId } from "../utils" import { Product } from "./product" import { ShippingOption } from "./shipping-option" -import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" -import { generateEntityId } from "../utils/generate-entity-id" +import { SoftDeletableEntity } from "../interfaces" export enum ShippingProfileType { DEFAULT = "default", @@ -20,7 +26,18 @@ export class ShippingProfile extends SoftDeletableEntity { @DbAwareColumn({ type: "enum", enum: ShippingProfileType }) type: ShippingProfileType - @OneToMany(() => Product, (product) => product.profile) + @ManyToMany(() => Product) + @JoinTable({ + name: "product_shipping_profile", + joinColumn: { + name: "profile_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "product_id", + referencedColumnName: "id", + }, + }) products: Product[] @OneToMany(() => ShippingOption, (so) => so.profile) diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index d9cab503d4..65214305f4 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -825,25 +825,6 @@ export const ProductRepository = dataSource.getRepository(Product).extend({ return queryBuilder },*/ - - /** - * Upserts shipping profile for products - * @param productIds IDs of products to update - * @param shippingProfileId ID of shipping profile to assign to products - * @returns updated products - */ - async upsertShippingProfile( - productIds: string[], - shippingProfileId: string - ): Promise { - await this.createQueryBuilder() - .update(Product) - .set({ profile_id: shippingProfileId }) - .where({ id: In(productIds) }) - .execute() - - return await this.findByIds(productIds) - }, }) export default ProductRepository diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 212f0a45c7..58731648b0 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -859,7 +859,9 @@ describe("CartService", () => { gift_cards: true, items: { variant: { - product: true, + product: { + profiles: true, + }, }, }, payment_sessions: true, diff --git a/packages/medusa/src/services/__tests__/store.js b/packages/medusa/src/services/__tests__/store.js index ab79b70f4f..ca25598874 100644 --- a/packages/medusa/src/services/__tests__/store.js +++ b/packages/medusa/src/services/__tests__/store.js @@ -46,18 +46,20 @@ describe("StoreService", () => { it("successfully retrieve store", async () => { await storeService.retrieve().catch(() => void 0) - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.find).toHaveBeenCalledTimes(1) }) }) describe("update", () => { const storeRepository = MockRepository({ - findOne: () => - Promise.resolve({ - id: IdMap.getId("store"), - name: "Medusa", - default_currency_code: "usd", - }), + find: () => + Promise.resolve([ + { + id: IdMap.getId("store"), + name: "Medusa", + default_currency_code: "usd", + }, + ]), }) const currencyRepository = MockRepository({}) @@ -77,7 +79,7 @@ describe("StoreService", () => { name: "Medusa Commerce", }) - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.find).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledWith({ @@ -94,18 +96,20 @@ describe("StoreService", () => { }) ).rejects.toThrow("Currency with code 1cd does not exist") - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.find).toHaveBeenCalledTimes(1) }) }) describe("addCurrency", () => { const storeRepository = MockRepository({ - findOne: () => - Promise.resolve({ - id: IdMap.getId("store"), - name: "Medusa", - currencies: [{ code: "dkk" }], - }), + find: () => + Promise.resolve([ + { + id: IdMap.getId("store"), + name: "Medusa", + currencies: [{ code: "dkk" }], + }, + ]), }) const currencyRepository = MockRepository({ @@ -134,7 +138,7 @@ describe("StoreService", () => { it("successfully adds currency", async () => { await storeService.addCurrency("sek") - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.find).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledWith({ @@ -155,18 +159,20 @@ describe("StoreService", () => { "Currency already added" ) - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.find).toHaveBeenCalledTimes(1) }) }) describe("removeCurrency", () => { const storeRepository = MockRepository({ - findOne: () => - Promise.resolve({ - id: IdMap.getId("store"), - name: "Medusa", - currencies: [{ code: "dkk" }], - }), + find: () => + Promise.resolve([ + { + id: IdMap.getId("store"), + name: "Medusa", + currencies: [{ code: "dkk" }], + }, + ]), }) const currencyRepository = MockRepository({ @@ -195,7 +201,7 @@ describe("StoreService", () => { it("successfully removes currency", async () => { await storeService.removeCurrency("dkk") - expect(storeRepository.findOne).toHaveBeenCalledTimes(1) + expect(storeRepository.find).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledWith({ diff --git a/packages/medusa/src/services/__tests__/swap.ts b/packages/medusa/src/services/__tests__/swap.ts index a168eae7a3..5f787586dd 100644 --- a/packages/medusa/src/services/__tests__/swap.ts +++ b/packages/medusa/src/services/__tests__/swap.ts @@ -280,7 +280,9 @@ describe("SwapService", () => { }, items: { variant: { - product: true, + product: { + profiles: true, + }, }, }, swaps: { diff --git a/packages/medusa/src/services/__tests__/user.js b/packages/medusa/src/services/__tests__/user.js index f95f04dcac..bc3d84c643 100644 --- a/packages/medusa/src/services/__tests__/user.js +++ b/packages/medusa/src/services/__tests__/user.js @@ -3,7 +3,7 @@ import UserService from "../user" const eventBusService = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } @@ -11,7 +11,7 @@ const eventBusService = { describe("UserService", () => { describe("retrieve", () => { const userRepository = MockRepository({ - findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + find: () => Promise.resolve([{ id: IdMap.getId("ironman") }]), }) const userService = new UserService({ manager: MockManager, @@ -25,8 +25,8 @@ describe("UserService", () => { it("successfully retrieves a user", async () => { const result = await userService.retrieve(IdMap.getId("ironman")) - expect(userRepository.findOne).toHaveBeenCalledTimes(1) - expect(userRepository.findOne).toHaveBeenCalledWith({ + expect(userRepository.find).toHaveBeenCalledTimes(1) + expect(userRepository.find).toHaveBeenCalledWith({ where: { id: IdMap.getId("ironman") }, }) @@ -78,7 +78,7 @@ describe("UserService", () => { describe("update", () => { const userRepository = MockRepository({ - findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + find: () => Promise.resolve([{ id: IdMap.getId("ironman") }]), }) const userService = new UserService({ manager: MockManager, @@ -159,8 +159,8 @@ describe("UserService", () => { describe("generateResetPasswordToken", () => { const userRepository = MockRepository({ - findOne: () => - Promise.resolve({ id: IdMap.getId("ironman"), password_hash: "lol" }), + find: () => + Promise.resolve([{ id: IdMap.getId("ironman"), password_hash: "lol" }]), }) const userService = new UserService({ diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 1abd23d29f..119f033911 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -480,12 +480,7 @@ class CartService extends TransactionBaseService { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { - relations: [ - "items", - "items.variant", - "items.variant.product", - "payment_sessions", - ], + relations: ["items.variant.product.profiles", "payment_sessions"], }) const lineItem = cart.items.find((item) => item.id === lineItemId) @@ -518,9 +513,7 @@ class CartService extends TransactionBaseService { const result = await this.retrieve(cartId, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "discounts", "discounts.rule", "region", @@ -719,9 +712,7 @@ class CartService extends TransactionBaseService { cart = await this.retrieve(cart.id, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "discounts", "discounts.rule", "region", @@ -899,9 +890,7 @@ class CartService extends TransactionBaseService { cart = await this.retrieve(cart.id, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "discounts", "discounts.rule", "region", @@ -978,9 +967,7 @@ class CartService extends TransactionBaseService { const updatedCart = await this.retrieve(cartId, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "discounts", "discounts.rule", "region", @@ -1054,9 +1041,7 @@ class CartService extends TransactionBaseService { async (transactionManager: EntityManager) => { const cartRepo = transactionManager.withRepository(this.cartRepository_) const relations = [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "shipping_methods", "shipping_methods.shipping_option", "shipping_address", @@ -1076,7 +1061,7 @@ class CartService extends TransactionBaseService { ) && data.sales_channel_id ) { - relations.push("items.variant", "items.variant.product") + relations.push("items.variant.product.profiles") } const cart = await this.retrieve(cartId, { @@ -1523,9 +1508,7 @@ class CartService extends TransactionBaseService { async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "region", "discounts", "discounts.rule", @@ -1622,12 +1605,7 @@ class CartService extends TransactionBaseService { ) const cart = await this.retrieveWithTotals(cartId, { - relations: [ - "payment_sessions", - "items", - "items.variant", - "items.variant.product", - ], + relations: ["payment_sessions", "items.variant.product.profiles"], }) // If cart total is 0, we don't perform anything payment related @@ -1697,9 +1675,7 @@ class CartService extends TransactionBaseService { const cart = await this.retrieveWithTotals(cartId, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "customer", "region", "region.payment_providers", @@ -1823,9 +1799,7 @@ class CartService extends TransactionBaseService { cartId, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "items.adjustments", "discounts", "discounts.rule", @@ -2107,9 +2081,7 @@ class CartService extends TransactionBaseService { relations: [ "shipping_methods", "shipping_methods.shipping_option", - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "payment_sessions", ], }) @@ -2292,7 +2264,7 @@ class CartService extends TransactionBaseService { cart.items = await lineItemServiceTx.list( { id: cart.items.map((i) => i.id) }, { - relations: ["variant", "variant.product"], + relations: ["variant.product.profiles"], } ) } @@ -2448,9 +2420,7 @@ class CartService extends TransactionBaseService { async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { relations: [ - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "discounts", "discounts.rule", "payment_sessions", @@ -2537,9 +2507,7 @@ class CartService extends TransactionBaseService { "discounts", "discounts.rule", "gift_cards", - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "items.adjustments", "region", "region.tax_rates", @@ -2809,9 +2777,7 @@ class CartService extends TransactionBaseService { private getTotalsRelations(config: FindConfig): string[] { const relationSet = new Set(config.relations) - relationSet.add("items") - relationSet.add("items.variant") - relationSet.add("items.variant.product") + relationSet.add("items.variant.product.profiles") relationSet.add("items.tax_lines") relationSet.add("items.adjustments") relationSet.add("gift_cards") diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index 978ced59b2..6893b9747e 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -430,7 +430,7 @@ export default class ClaimService extends TransactionBaseService { id: result.additional_items.map((i) => i.id), }, { - relations: ["variant", "variant.product"], + relations: ["variant.product.profiles"], } ) @@ -525,10 +525,8 @@ export default class ClaimService extends TransactionBaseService { async (transactionManager: EntityManager) => { const claim = await this.retrieve(id, { relations: [ - "additional_items", "additional_items.tax_lines", - "additional_items.variant", - "additional_items.variant.product", + "additional_items.variant.product.profiles", "shipping_methods", "shipping_methods.shipping_option", "shipping_methods.tax_lines", diff --git a/packages/medusa/src/services/draft-order.ts b/packages/medusa/src/services/draft-order.ts index 1232ba4816..887fed3383 100644 --- a/packages/medusa/src/services/draft-order.ts +++ b/packages/medusa/src/services/draft-order.ts @@ -384,9 +384,7 @@ class DraftOrderService extends TransactionBaseService { relations: [ "shipping_methods", "shipping_methods.shipping_option", - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "payment_sessions", ], }) diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index af3ae17b43..c6a01430eb 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -575,7 +575,7 @@ export default class OrderEditService extends TransactionBaseService { let lineItem = await lineItemServiceTx.create(lineItemData) lineItem = await lineItemServiceTx.retrieve(lineItem.id, { - relations: ["variant", "variant.product"], + relations: ["variant.product.profiles"], }) await this.refreshAdjustments(orderEditId) diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index a2414480f0..b09fdfa9f8 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -342,11 +342,9 @@ class OrderService extends TransactionBaseService { const totalsToSelect = select.filter((v) => totalFields.includes(v)) if (totalsToSelect.length > 0) { const relationSet = new Set(relations) - relationSet.add("items") relationSet.add("items.tax_lines") relationSet.add("items.adjustments") - relationSet.add("items.variant") - relationSet.add("items.variant.product") + relationSet.add("items.variant.product.profiles") relationSet.add("swaps") relationSet.add("swaps.additional_items") relationSet.add("swaps.additional_items.tax_lines") @@ -1034,9 +1032,7 @@ class OrderService extends TransactionBaseService { relations: [ "shipping_methods", "shipping_methods.shipping_option", - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", ], }) const { shipping_methods } = order @@ -1403,10 +1399,8 @@ class OrderService extends TransactionBaseService { "billing_address", "shipping_methods", "shipping_methods.shipping_option", - "items", "items.adjustments", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "payments", ], }) diff --git a/packages/medusa/src/services/product-variant-inventory.ts b/packages/medusa/src/services/product-variant-inventory.ts index 6be14da8f1..1900c0a3ca 100644 --- a/packages/medusa/src/services/product-variant-inventory.ts +++ b/packages/medusa/src/services/product-variant-inventory.ts @@ -3,14 +3,14 @@ import { ICacheService, IEventBusService, IInventoryService, - IStockLocationService, InventoryItemDTO, InventoryLevelDTO, + IStockLocationService, ReservationItemDTO, ReserveQuantityContext, } from "@medusajs/types" import { LineItem, Product, ProductVariant } from "../models" -import { MedusaError, isDefined } from "@medusajs/utils" +import { isDefined, MedusaError } from "@medusajs/utils" import { PricedProduct, PricedVariant } from "../types/pricing" import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item" @@ -303,9 +303,14 @@ class ProductVariantInventoryService extends TransactionBaseService { // Verify that variant exists const variants = await this.productVariantService_ .withTransaction(this.activeManager_) - .list({ - id: data.map((d) => d.variantId), - }) + .list( + { + id: data.map((d) => d.variantId), + }, + { + select: ["id"], + } + ) const foundVariantIds = new Set(variants.map((v) => v.id)) const requestedVariantIds = new Set(data.map((v) => v.variantId)) diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 3bb264968b..05d90f2315 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -1,5 +1,5 @@ import { isDefined, MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" +import { EntityManager, In } from "typeorm" import { ProductVariantService, SearchService } from "." import { TransactionBaseService } from "../interfaces" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" @@ -11,6 +11,7 @@ import { ProductType, ProductVariant, SalesChannel, + ShippingProfile, } from "../models" import { ImageRepository } from "../repositories/image" import { @@ -446,6 +447,10 @@ class ProductService extends TransactionBaseService { let product = productRepo.create(rest) + if (rest.profile_id) { + product.profiles = [{ id: rest.profile_id! }] as ShippingProfile[] + } + if (images?.length) { product.images = await imageRepo.upsertImages(images) } @@ -563,6 +568,10 @@ class ProductService extends TransactionBaseService { ...rest } = update + if (rest.profile_id) { + product.profiles = [{ id: rest.profile_id! }] as ShippingProfile[] + } + if (!product.thumbnail && !update.thumbnail && images?.length) { product.thumbnail = images[0] } @@ -918,21 +927,33 @@ class ProductService extends TransactionBaseService { } /** - * + * Assign a product to a profile, if a profile id null is provided then detach the product from the profile * @param productIds ID or IDs of the products to update * @param profileId Shipping profile ID to update the shipping options with - * @returns updated shipping options + * @returns updated products */ async updateShippingProfile( productIds: string | string[], - profileId: string + profileId: string | null ): Promise { return await this.atomicPhase_(async (manager) => { const productRepo = manager.withRepository(this.productRepository_) const ids = isString(productIds) ? [productIds] : productIds - const products = await productRepo.upsertShippingProfile(ids, profileId) + let products = ( + await this.list( + { id: In(ids) }, + { relations: ["profiles"], select: ["id"] } + ) + ).map((product) => { + product.profiles = !profileId + ? [] + : ([{ id: profileId }] as ShippingProfile[]) + return product + }) + + products = await productRepo.save(products) await this.eventBus_ .withTransaction(manager) diff --git a/packages/medusa/src/services/return.ts b/packages/medusa/src/services/return.ts index a1edd55e03..57d0e1a852 100644 --- a/packages/medusa/src/services/return.ts +++ b/packages/medusa/src/services/return.ts @@ -530,7 +530,9 @@ class ReturnService extends TransactionBaseService { { id: returnOrder.items.map(({ item_id }) => item_id), }, - { relations: ["tax_lines", "variant", "variant.product"] } + { + relations: ["tax_lines", "variant.product.profiles"], + } ) returnData.items = returnOrder.items.map((item) => { diff --git a/packages/medusa/src/services/shipping-profile.ts b/packages/medusa/src/services/shipping-profile.ts index 008fe58169..b65ce9195d 100644 --- a/packages/medusa/src/services/shipping-profile.ts +++ b/packages/medusa/src/services/shipping-profile.ts @@ -290,23 +290,16 @@ class ShippingProfileService extends TransactionBaseService { this.shippingProfileRepository_ ) - let profile = await this.retrieve(profileId, { - relations: [ - "products", - "products.profile", - "shipping_options", - "shipping_options.profile", - ], - }) + const profile = await this.retrieve(profileId) const { metadata, products, shipping_options, ...rest } = update if (products) { - profile = await this.addProduct(profile.id, products) + await this.addProduct(profile.id, products) } if (shipping_options) { - profile = await this.addShippingOption(profile.id, shipping_options) + await this.addShippingOption(profile.id, shipping_options) } if (metadata) { @@ -347,12 +340,22 @@ class ShippingProfileService extends TransactionBaseService { } /** - * Adds a product of an array of products to the profile. + * @deprecated use {@link addProducts} instead + */ + async addProduct( + profileId: string, + productId: string | string[] + ): Promise { + return await this.addProducts(profileId, productId) + } + + /** + * Adds a product or an array of products to the profile. * @param profileId - the profile to add the products to. * @param productId - the ID of the product or multiple products to add. * @return the result of update */ - async addProduct( + async addProducts( profileId: string, productId: string | string[] ): Promise { @@ -364,14 +367,27 @@ class ShippingProfileService extends TransactionBaseService { profileId ) - return await this.retrieve(profileId, { - relations: [ - "products", - "products.profile", - "shipping_options", - "shipping_options.profile", - ], - }) + return await this.retrieve(profileId) + }) + } + + /** + * Removes a product or an array of products from the profile. + * @param profileId - the profile to add the products to. + * @param productId - the ID of the product or multiple products to add. + * @return the result of update + */ + async removeProducts( + profileId: string | null, + productId: string | string[] + ): Promise { + return await this.atomicPhase_(async (manager) => { + const productServiceTx = this.productService_.withTransaction(manager) + + await productServiceTx.updateShippingProfile( + isString(productId) ? [productId] : productId, + null + ) }) } @@ -396,12 +412,7 @@ class ShippingProfileService extends TransactionBaseService { ) return await this.retrieve(profileId, { - relations: [ - "products", - "products.profile", - "shipping_options", - "shipping_options.profile", - ], + relations: ["products.profiles", "shipping_options.profile"], }) }) } diff --git a/packages/medusa/src/services/store.ts b/packages/medusa/src/services/store.ts index 465c378036..999a12faed 100644 --- a/packages/medusa/src/services/store.ts +++ b/packages/medusa/src/services/store.ts @@ -88,13 +88,13 @@ class StoreService extends TransactionBaseService { }, config ) - const store = await storeRepo.findOne(query) + const stores = await storeRepo.find(query) - if (!store) { + if (!stores.length) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Store does not exist") } - return store + return stores[0] } protected getDefaultCurrency_(code: string): Partial { diff --git a/packages/medusa/src/services/swap.ts b/packages/medusa/src/services/swap.ts index 1c9ba5ab88..84340352f7 100644 --- a/packages/medusa/src/services/swap.ts +++ b/packages/medusa/src/services/swap.ts @@ -584,10 +584,7 @@ class SwapService extends TransactionBaseService { const swap = await this.retrieve(swapId, { relations: [ - "order", - "order.items", - "order.items.variant", - "order.items.variant.product", + "order.items.variant.product.profiles", "order.swaps", "order.swaps.additional_items", "order.discounts", @@ -934,10 +931,8 @@ class SwapService extends TransactionBaseService { relations: [ "payment", "shipping_address", - "additional_items", "additional_items.tax_lines", - "additional_items.variant", - "additional_items.variant.product", + "additional_items.variant.product.profiles", "shipping_methods", "shipping_methods.shipping_option", "shipping_methods.tax_lines", diff --git a/packages/medusa/src/services/user.ts b/packages/medusa/src/services/user.ts index 8c3a87fcd4..5879a17051 100644 --- a/packages/medusa/src/services/user.ts +++ b/packages/medusa/src/services/user.ts @@ -85,16 +85,16 @@ class UserService extends TransactionBaseService { const userRepo = this.activeManager_.withRepository(this.userRepository_) const query = buildQuery({ id: userId }, config) - const user = await userRepo.findOne(query) + const users = await userRepo.find(query) - if (!user) { + if (!users.length) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `User with id: ${userId} was not found` ) } - return user + return users[0] } /** diff --git a/packages/medusa/src/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index e4d4bfdbaf..a6073a8caa 100644 --- a/packages/medusa/src/strategies/cart-completion.ts +++ b/packages/medusa/src/strategies/cart-completion.ts @@ -189,9 +189,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { "discounts", "discounts.rule", "gift_cards", - "items", - "items.variant", - "items.variant.product", + "items.variant.product.profiles", "items.adjustments", "region", "region.tax_rates", @@ -295,7 +293,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { "region", "payment", "payment_sessions", - "items.variant.product", + "items.variant.product.profiles", ], }) diff --git a/packages/medusa/src/workflows/admin/create-product/create-product.ts b/packages/medusa/src/workflows/admin/create-product/create-product.ts deleted file mode 100644 index 2354a3059c..0000000000 --- a/packages/medusa/src/workflows/admin/create-product/create-product.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { EntityManager } from "typeorm" -import { - IInventoryService, - MedusaContainer, - ProductTypes, -} from "@medusajs/types" -import { ulid } from "ulid" -import { MedusaError } from "@medusajs/utils" -import { - DistributedTransaction, - TransactionHandlerType, - TransactionOrchestrator, - TransactionPayload, - TransactionState, - TransactionStepsDefinition, -} from "../../../utils/transaction" -import { CreateProductVariantInput } from "../../../types/product-variant" -import { - attachInventoryItems, - createInventoryItems, - createProducts, - removeInventoryItems, - removeProducts, -} from "../../functions" - -enum Actions { - createProduct = "createProduct", - createPrices = "createPrices", - attachToSalesChannel = "attachToSalesChannel", - createInventoryItems = "createInventoryItems", - attachInventoryItems = "attachInventoryItems", -} - -const workflowSteps: TransactionStepsDefinition = { - next: { - action: Actions.createProduct, - saveResponse: true, - next: { - action: Actions.attachToSalesChannel, - saveResponse: true, - next: { - action: Actions.createPrices, - saveResponse: true, - next: { - action: Actions.createInventoryItems, - saveResponse: true, - next: { - action: Actions.attachInventoryItems, - noCompensation: true, - }, - }, - }, - }, - }, -} - -const createProductOrchestrator = new TransactionOrchestrator( - "create-product", - workflowSteps -) - -type InjectedDependencies = { - manager: EntityManager - container: MedusaContainer - inventoryService?: IInventoryService -} - -export async function createProductWorkflow( - dependencies: InjectedDependencies, - productId: string, - input: CreateProductVariantInput[] -): Promise { - const { manager, container } = dependencies - async function transactionHandler( - actionId: string, - type: TransactionHandlerType, - payload: TransactionPayload - ) { - const command = { - [Actions.createProduct]: { - [TransactionHandlerType.INVOKE]: async ( - data: ProductTypes.CreateProductDTO[] - ) => { - return await createProducts({ - container, - data, - }) - }, - [TransactionHandlerType.COMPENSATE]: async ( - data: any[], - { invoke } - ) => { - const createdProducts = invoke[Actions.createProduct] - return await removeProducts({ container, data: createdProducts }) - }, - }, - [Actions.createInventoryItems]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductVariantInput[], - { invoke } - ) => { - const { [Actions.createProduct]: products } = invoke - - return await createInventoryItems({ - container, - manager, - data: products, - }) - }, - [TransactionHandlerType.COMPENSATE]: async (_, { invoke }) => { - const variantInventoryItemsData = invoke[Actions.createInventoryItems] - await removeInventoryItems({ - container, - manager, - data: variantInventoryItemsData, - }) - }, - }, - [Actions.attachInventoryItems]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductVariantInput[], - { invoke } - ) => { - const { [Actions.createInventoryItems]: inventoryItemsResult } = - invoke - - return await attachInventoryItems({ - container, - manager, - data: inventoryItemsResult, - }) - }, - }, - } - return command[actionId][type](payload.data, payload.context) - } - - const orchestrator = createProductOrchestrator - - const transaction = await orchestrator.beginTransaction( - ulid(), - transactionHandler, - input - ) - - await orchestrator.resume(transaction) - - if (transaction.getState() !== TransactionState.DONE) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - transaction - .getErrors() - .map((err) => err.error?.message) - .join("\n") - ) - } - - return transaction -} diff --git a/packages/medusa/src/workflows/admin/create-products/definition.ts b/packages/medusa/src/workflows/admin/create-products/definition.ts new file mode 100644 index 0000000000..dfc02bea55 --- /dev/null +++ b/packages/medusa/src/workflows/admin/create-products/definition.ts @@ -0,0 +1,421 @@ +import { + TransactionHandlerType, + TransactionPayload, + TransactionStepHandler, + TransactionStepsDefinition, +} from "../../../utils/transaction" +import { + IInventoryService, + MedusaContainer, + ProductTypes, +} from "@medusajs/types" +import { + defaultAdminProductFields, + defaultAdminProductRelations, +} from "../../../api" +import { + attachInventoryItems, + attachSalesChannelToProducts, + attachShippingProfileToProducts, + createInventoryItems, + createProducts, + CreateProductsData, + CreateProductsPreparedData, + detachInventoryItems, + detachSalesChannelFromProducts, + detachShippingProfileFromProducts, + prepareCreateProductsData, + removeInventoryItems, + removeProducts, + updateProductsVariantsPrices, +} from "../../functions" +import { PricingService, ProductService } from "../../../services" +import { CreateProductsWorkflowInputData, InjectedDependencies } from "./types" + +export enum CreateProductsWorkflowActions { + prepare = "prepare", + createProducts = "createProducts", + attachToSalesChannel = "attachToSalesChannel", + attachShippingProfile = "attachShippingProfile", + createPrices = "createPrices", + createInventoryItems = "createInventoryItems", + attachInventoryItems = "attachInventoryItems", + result = "result", +} + +export const workflowSteps: TransactionStepsDefinition = { + next: { + action: CreateProductsWorkflowActions.prepare, + noCompensation: true, + next: { + action: CreateProductsWorkflowActions.createProducts, + next: [ + { + action: CreateProductsWorkflowActions.attachShippingProfile, + saveResponse: false, + }, + { + action: CreateProductsWorkflowActions.attachToSalesChannel, + saveResponse: false, + }, + { + action: CreateProductsWorkflowActions.createPrices, + next: { + action: CreateProductsWorkflowActions.createInventoryItems, + next: { + action: CreateProductsWorkflowActions.attachInventoryItems, + next: { + action: CreateProductsWorkflowActions.result, + noCompensation: true, + }, + }, + }, + }, + ], + }, + }, +} + +const shouldSkipInventoryStep = ( + container: MedusaContainer, + stepName: string +) => { + const inventoryService = container.resolve( + "inventoryService" + ) as IInventoryService + if (!inventoryService) { + const logger = container.resolve("logger") + logger.warn( + `Inventory service not found. You should install the @medusajs/inventory package to use inventory. The '${stepName}' will be skipped.` + ) + return true + } + + return false +} + +export function transactionHandler( + dependencies: InjectedDependencies +): TransactionStepHandler { + const { manager, container } = dependencies + + const command = { + [CreateProductsWorkflowActions.prepare]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData + ) => { + return await prepareCreateProductsData({ + container, + manager, + data, + }) + }, + }, + + [CreateProductsWorkflowActions.createProducts]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsData + ): Promise => { + return await createProducts({ + container, + data, + }) + }, + [TransactionHandlerType.COMPENSATE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + + if (!products?.length) { + return + } + + return await removeProducts({ + container, + data: products, + }) + }, + }, + + [CreateProductsWorkflowActions.attachShippingProfile]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + const { productsHandleShippingProfileIdMap } = invoke[ + CreateProductsWorkflowActions.prepare + ] as CreateProductsPreparedData + + return await attachShippingProfileToProducts({ + container, + manager, + data: { + productsHandleShippingProfileIdMap, + products, + }, + }) + }, + [TransactionHandlerType.COMPENSATE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + const { productsHandleShippingProfileIdMap } = invoke[ + CreateProductsWorkflowActions.prepare + ] as CreateProductsPreparedData + + return await detachShippingProfileFromProducts({ + container, + manager, + data: { + productsHandleShippingProfileIdMap, + products, + }, + }) + }, + }, + + [CreateProductsWorkflowActions.attachToSalesChannel]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + const { productsHandleSalesChannelsMap } = invoke[ + CreateProductsWorkflowActions.prepare + ] as CreateProductsPreparedData + + return await attachSalesChannelToProducts({ + container, + manager, + data: { + productsHandleSalesChannelsMap, + products, + }, + }) + }, + [TransactionHandlerType.COMPENSATE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + const { productsHandleSalesChannelsMap } = invoke[ + CreateProductsWorkflowActions.prepare + ] as CreateProductsPreparedData + + return await detachSalesChannelFromProducts({ + container, + manager, + data: { + productsHandleSalesChannelsMap, + products, + }, + }) + }, + }, + + [CreateProductsWorkflowActions.createInventoryItems]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const shouldSkipStep_ = shouldSkipInventoryStep( + container, + CreateProductsWorkflowActions.createInventoryItems + ) + if (shouldSkipStep_) { + return + } + + const { [CreateProductsWorkflowActions.createProducts]: products } = + invoke + + return await createInventoryItems({ + container, + manager, + data: products, + }) + }, + [TransactionHandlerType.COMPENSATE]: async (_, { invoke }) => { + const shouldSkipStep_ = shouldSkipInventoryStep( + container, + CreateProductsWorkflowActions.createInventoryItems + ) + + const variantInventoryItemsData = + invoke[CreateProductsWorkflowActions.createInventoryItems] + + if (shouldSkipStep_ || !variantInventoryItemsData?.length) { + return + } + + await removeInventoryItems({ + container, + manager, + data: variantInventoryItemsData, + }) + }, + }, + + [CreateProductsWorkflowActions.attachInventoryItems]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const shouldSkipStep_ = shouldSkipInventoryStep( + container, + CreateProductsWorkflowActions.attachInventoryItems + ) + if (shouldSkipStep_) { + return + } + + const { + [CreateProductsWorkflowActions.createInventoryItems]: + inventoryItemsResult, + } = invoke + + return await attachInventoryItems({ + container, + manager, + data: inventoryItemsResult, + }) + }, + [TransactionHandlerType.COMPENSATE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const shouldSkipStep_ = shouldSkipInventoryStep( + container, + CreateProductsWorkflowActions.attachInventoryItems + ) + + const { + [CreateProductsWorkflowActions.createInventoryItems]: + inventoryItemsResult, + } = invoke + + if (shouldSkipStep_ || !inventoryItemsResult?.length) { + return + } + + return await detachInventoryItems({ + container, + manager, + data: inventoryItemsResult, + }) + }, + }, + + [CreateProductsWorkflowActions.createPrices]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const { productsHandleVariantsIndexPricesMap } = invoke[ + CreateProductsWorkflowActions.prepare + ] as CreateProductsPreparedData + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + + return await updateProductsVariantsPrices({ + container, + manager, + data: { + products, + productsHandleVariantsIndexPricesMap, + }, + }) + }, + [TransactionHandlerType.COMPENSATE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const { productsHandleVariantsIndexPricesMap } = invoke[ + CreateProductsWorkflowActions.prepare + ] as CreateProductsPreparedData + const products = invoke[ + CreateProductsWorkflowActions.createProducts + ] as ProductTypes.ProductDTO[] + + if (!productsHandleVariantsIndexPricesMap?.size) { + return + } + + const updatedProductsHandleVariantsIndexPricesMap = new Map() + productsHandleVariantsIndexPricesMap.forEach((items, productHandle) => { + const existingItems = + updatedProductsHandleVariantsIndexPricesMap.get(productHandle) ?? [] + + items.forEach(({ index }) => { + existingItems.push({ + index, + prices: [], + }) + }) + + updatedProductsHandleVariantsIndexPricesMap.set(productHandle, items) + }) + + return await updateProductsVariantsPrices({ + container, + manager, + data: { + products, + productsHandleVariantsIndexPricesMap: + updatedProductsHandleVariantsIndexPricesMap, + }, + }) + }, + }, + + [CreateProductsWorkflowActions.result]: { + [TransactionHandlerType.INVOKE]: async ( + data: CreateProductsWorkflowInputData, + { invoke } + ) => { + const { [CreateProductsWorkflowActions.createProducts]: products } = + invoke + + const productService = container.resolve( + "productService" + ) as ProductService + const pricingService = container.resolve( + "pricingService" + ) as PricingService + + const rawProduct = await productService + .withTransaction(manager) + .retrieve(products[0].id, { + select: defaultAdminProductFields, + relations: defaultAdminProductRelations, + }) + + const res = await pricingService + .withTransaction(manager) + .setProductPrices([rawProduct]) + + return res + }, + }, + } + + return ( + actionId: string, + type: TransactionHandlerType, + payload: TransactionPayload + ) => command[actionId][type](payload.data, payload.context) +} diff --git a/packages/medusa/src/workflows/admin/create-products/index.ts b/packages/medusa/src/workflows/admin/create-products/index.ts new file mode 100644 index 0000000000..b6c7d5eb32 --- /dev/null +++ b/packages/medusa/src/workflows/admin/create-products/index.ts @@ -0,0 +1,47 @@ +import { ulid } from "ulid" +import { MedusaError } from "@medusajs/utils" +import { + TransactionOrchestrator, + TransactionState, +} from "../../../utils/transaction" +import { AdminPostProductsReq } from "../../../api" +import { Product } from "../../../models" +import { PricedProduct } from "../../../types/pricing" +import { + CreateProductsWorkflowActions, + transactionHandler, + workflowSteps, +} from "./definition" +import { InjectedDependencies } from "./types" + +const createProductsOrchestrator = new TransactionOrchestrator( + "create-products", + workflowSteps +) + +export async function createProductsWorkflow( + dependencies: InjectedDependencies, + input: AdminPostProductsReq[] +): Promise<(Product | PricedProduct)[]> { + const transaction = await createProductsOrchestrator.beginTransaction( + ulid(), + transactionHandler(dependencies), + input + ) + + await createProductsOrchestrator.resume(transaction) + + if (transaction.getState() !== TransactionState.DONE) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + transaction + .getErrors() + .map((err) => err.error?.message) + .join("\n") + ) + } + + return transaction.getContext().invoke[ + CreateProductsWorkflowActions.result + ] as (Product | PricedProduct)[] +} diff --git a/packages/medusa/src/workflows/admin/create-products/types.ts b/packages/medusa/src/workflows/admin/create-products/types.ts new file mode 100644 index 0000000000..2526460d77 --- /dev/null +++ b/packages/medusa/src/workflows/admin/create-products/types.ts @@ -0,0 +1,10 @@ +import { EntityManager } from "typeorm" +import { MedusaContainer } from "@medusajs/types" +import { AdminPostProductsReq } from "../../../api" + +export type InjectedDependencies = { + manager: EntityManager + container: MedusaContainer +} + +export type CreateProductsWorkflowInputData = AdminPostProductsReq[] diff --git a/packages/medusa/src/workflows/functions/attach-inventory-items.ts b/packages/medusa/src/workflows/functions/attach-inventory-items.ts index 0659850a69..e2f749681b 100644 --- a/packages/medusa/src/workflows/functions/attach-inventory-items.ts +++ b/packages/medusa/src/workflows/functions/attach-inventory-items.ts @@ -4,6 +4,7 @@ import { ProductTypes, } from "@medusajs/types" import { EntityManager } from "typeorm" +import { ProductVariantInventoryService } from "../../services" export async function attachInventoryItems({ container, @@ -17,18 +18,13 @@ export async function attachInventoryItems({ inventoryItem: InventoryItemDTO }[] }) { - const productVariantInventoryService = container - .resolve("productVariantInventoryService") - .withTransaction(manager) + const productVariantInventoryService: ProductVariantInventoryService = + container.resolve("productVariantInventoryService").withTransaction(manager) - return await Promise.all( - data - .filter((d) => d) - .map(async ({ variant, inventoryItem }) => { - return await productVariantInventoryService.attachInventoryItem( - variant.id, - inventoryItem.id - ) - }) - ) + const inventoryData = data.map(({ variant, inventoryItem }) => ({ + variantId: variant.id, + inventoryItemId: inventoryItem.id, + })) + + return await productVariantInventoryService.attachInventoryItem(inventoryData) } diff --git a/packages/medusa/src/workflows/functions/attach-sales-channel-to-products.ts b/packages/medusa/src/workflows/functions/attach-sales-channel-to-products.ts new file mode 100644 index 0000000000..87fb325ce3 --- /dev/null +++ b/packages/medusa/src/workflows/functions/attach-sales-channel-to-products.ts @@ -0,0 +1,49 @@ +import { MedusaContainer, ProductTypes } from "@medusajs/types" +import { EntityManager } from "typeorm" +import { SalesChannelService } from "../../services" + +type ProductHandle = string +type SalesChannelId = string + +export async function attachSalesChannelToProducts({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: { + productsHandleSalesChannelsMap: Map + products: ProductTypes.ProductDTO[] + } +}): Promise { + const salesChannelService: SalesChannelService = container.resolve( + "salesChannelService" + ) + const salesChannelServiceTx = salesChannelService.withTransaction(manager) + + const salesChannelIdProductIdsMap = new Map() + data.products.forEach((product) => { + const salesChannelIds = data.productsHandleSalesChannelsMap.get( + product.handle! + ) + if (salesChannelIds) { + salesChannelIds.forEach((salesChannelId) => { + const productIds = salesChannelIdProductIdsMap.get(salesChannelId) || [] + productIds.push(product.id) + salesChannelIdProductIdsMap.set(salesChannelId, productIds) + }) + } + }) + + await Promise.all( + Array.from(salesChannelIdProductIdsMap.entries()).map( + async ([salesChannelId, productIds]) => { + return await salesChannelServiceTx.addProducts( + salesChannelId, + productIds + ) + } + ) + ) +} diff --git a/packages/medusa/src/workflows/functions/attach-shipping-profile-to-products.ts b/packages/medusa/src/workflows/functions/attach-shipping-profile-to-products.ts new file mode 100644 index 0000000000..6952f3a541 --- /dev/null +++ b/packages/medusa/src/workflows/functions/attach-shipping-profile-to-products.ts @@ -0,0 +1,45 @@ +import { MedusaContainer, ProductTypes } from "@medusajs/types" +import { EntityManager } from "typeorm" +import { ShippingProfileService } from "../../services" + +type ProductHandle = string +type ShippingProfileId = string + +export async function attachShippingProfileToProducts({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: { + productsHandleShippingProfileIdMap: Map + products: ProductTypes.ProductDTO[] + } +}): Promise { + const shippingProfileService: ShippingProfileService = container.resolve( + "shippingProfileService" + ) + const shippingProfileServiceTx = + shippingProfileService.withTransaction(manager) + + const profileIdProductIdsMap = new Map() + data.products.forEach((product) => { + const profileId = data.productsHandleShippingProfileIdMap.get( + product.handle! + ) + if (profileId) { + const productIds = profileIdProductIdsMap.get(profileId) || [] + productIds.push(product.id) + profileIdProductIdsMap.set(profileId, productIds) + } + }) + + await Promise.all( + Array.from(profileIdProductIdsMap.entries()).map( + async ([profileId, productIds]) => { + return await shippingProfileServiceTx.addProducts(profileId, productIds) + } + ) + ) +} diff --git a/packages/medusa/src/workflows/functions/create-prducts.ts b/packages/medusa/src/workflows/functions/create-prducts.ts index 4af2fea25c..171e9e3955 100644 --- a/packages/medusa/src/workflows/functions/create-prducts.ts +++ b/packages/medusa/src/workflows/functions/create-prducts.ts @@ -1,12 +1,16 @@ import { MedusaContainer, ProductTypes } from "@medusajs/types" -export async function removeProducts({ +export type CreateProductsData = ProductTypes.CreateProductDTO[] + +export async function createProducts({ container, data, }: { container: MedusaContainer - data: ProductTypes.ProductDTO[] + data: CreateProductsData }): Promise { - const productModuleService = container.resolve("productModuleService") - return await productModuleService.softDelete(data.map((p) => p.id)) + const productModuleService: ProductTypes.IProductModuleService = + container.resolve("productModuleService") + + return await productModuleService.create(data) } diff --git a/packages/medusa/src/workflows/functions/detach-inventory-items.ts b/packages/medusa/src/workflows/functions/detach-inventory-items.ts new file mode 100644 index 0000000000..f899e4dfa7 --- /dev/null +++ b/packages/medusa/src/workflows/functions/detach-inventory-items.ts @@ -0,0 +1,32 @@ +import { + InventoryItemDTO, + MedusaContainer, + ProductTypes, +} from "@medusajs/types" +import { EntityManager } from "typeorm" +import { ProductVariantInventoryService } from "../../services" + +export async function detachInventoryItems({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: { + variant: ProductTypes.ProductVariantDTO + inventoryItem: InventoryItemDTO + }[] +}) { + const productVariantInventoryService: ProductVariantInventoryService = + container.resolve("productVariantInventoryService").withTransaction(manager) + + return await Promise.all( + data.map(async ({ variant, inventoryItem }) => { + return await productVariantInventoryService.detachInventoryItem( + inventoryItem.id, + variant.id + ) + }) + ) +} diff --git a/packages/medusa/src/workflows/functions/detach-sales-channel-from-products.ts b/packages/medusa/src/workflows/functions/detach-sales-channel-from-products.ts new file mode 100644 index 0000000000..14c6130c90 --- /dev/null +++ b/packages/medusa/src/workflows/functions/detach-sales-channel-from-products.ts @@ -0,0 +1,49 @@ +import { MedusaContainer, ProductTypes } from "@medusajs/types" +import { EntityManager } from "typeorm" +import { SalesChannelService } from "../../services" + +type ProductHandle = string +type SalesChannelId = string + +export async function detachSalesChannelFromProducts({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: { + productsHandleSalesChannelsMap: Map + products: ProductTypes.ProductDTO[] + } +}): Promise { + const salesChannelService: SalesChannelService = container.resolve( + "salesChannelService" + ) + const salesChannelServiceTx = salesChannelService.withTransaction(manager) + + const salesChannelIdProductIdsMap = new Map() + data.products.forEach((product) => { + const salesChannelIds = data.productsHandleSalesChannelsMap.get( + product.handle! + ) + if (salesChannelIds) { + salesChannelIds.forEach((salesChannelId) => { + const productIds = salesChannelIdProductIdsMap.get(salesChannelId) || [] + productIds.push(product.id) + salesChannelIdProductIdsMap.set(salesChannelId, productIds) + }) + } + }) + + await Promise.all( + Array.from(salesChannelIdProductIdsMap.entries()).map( + async ([salesChannelId, productIds]) => { + return await salesChannelServiceTx.removeProducts( + salesChannelId, + productIds + ) + } + ) + ) +} diff --git a/packages/medusa/src/workflows/functions/detach-shipping-profile-from-products.ts b/packages/medusa/src/workflows/functions/detach-shipping-profile-from-products.ts new file mode 100644 index 0000000000..77755c349f --- /dev/null +++ b/packages/medusa/src/workflows/functions/detach-shipping-profile-from-products.ts @@ -0,0 +1,48 @@ +import { MedusaContainer, ProductTypes } from "@medusajs/types" +import { EntityManager } from "typeorm" +import { ShippingProfileService } from "../../services" + +type ProductHandle = string +type ShippingProfileId = string + +export async function detachShippingProfileFromProducts({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: { + productsHandleShippingProfileIdMap: Map + products: ProductTypes.ProductDTO[] + } +}): Promise { + const shippingProfileService: ShippingProfileService = container.resolve( + "shippingProfileService" + ) + const shippingProfileServiceTx = + shippingProfileService.withTransaction(manager) + + const profileIdProductIdsMap = new Map() + data.products.forEach((product) => { + const profileId = data.productsHandleShippingProfileIdMap.get( + product.handle! + ) + if (profileId) { + const productIds = profileIdProductIdsMap.get(profileId) || [] + productIds.push(product.id) + profileIdProductIdsMap.set(profileId, productIds) + } + }) + + await Promise.all( + Array.from(profileIdProductIdsMap.entries()).map( + async ([profileId, productIds]) => { + return await shippingProfileServiceTx.removeProducts( + profileId, + productIds + ) + } + ) + ) +} diff --git a/packages/medusa/src/workflows/functions/index.ts b/packages/medusa/src/workflows/functions/index.ts index 6a4a8a48af..7e118f4275 100644 --- a/packages/medusa/src/workflows/functions/index.ts +++ b/packages/medusa/src/workflows/functions/index.ts @@ -3,3 +3,10 @@ export * from "./remove-products" export * from "./create-inventory-items" export * from "./remove-inventory-items" export * from "./attach-inventory-items" +export * from "./prepare-create-products-data" +export * from "./attach-shipping-profile-to-products" +export * from "./detach-shipping-profile-from-products" +export * from "./attach-sales-channel-to-products" +export * from "./detach-sales-channel-from-products" +export * from "./update-products-variants-prices" +export * from "./detach-inventory-items" diff --git a/packages/medusa/src/workflows/functions/prepare-create-products-data.ts b/packages/medusa/src/workflows/functions/prepare-create-products-data.ts new file mode 100644 index 0000000000..69efd891de --- /dev/null +++ b/packages/medusa/src/workflows/functions/prepare-create-products-data.ts @@ -0,0 +1,142 @@ +import { MedusaContainer, ProductTypes } from "@medusajs/types" +import { EntityManager } from "typeorm" +import { SalesChannelService, ShippingProfileService } from "../../services" +import { kebabCase } from "@medusajs/utils" +import { FlagRouter } from "../../utils/flag-router" +import SalesChannelFeatureFlag from "../../loaders/feature-flags/sales-channels" +import { SalesChannel, ShippingProfileType } from "../../models" +import { ProductVariantPricesCreateReq } from "../../types/product-variant" +import { AdminPostProductsReq } from "../../api" + +type CreateProductsInputData = AdminPostProductsReq[] + +type ShippingProfileId = string +type SalesChannelId = string +type ProductHandle = string +type VariantIndexAndPrices = { + index: number + prices: ProductVariantPricesCreateReq[] +} + +export type CreateProductsPreparedData = { + products: ProductTypes.CreateProductDTO[] + productsHandleShippingProfileIdMap: Map + productsHandleSalesChannelsMap: Map + productsHandleVariantsIndexPricesMap: Map< + ProductHandle, + VariantIndexAndPrices[] + > +} + +export async function prepareCreateProductsData({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: CreateProductsInputData +}): Promise { + const shippingProfileService: ShippingProfileService = container + .resolve("shippingProfileService") + .withTransaction(manager) + const featureFlagRouter: FlagRouter = container.resolve("featureFlagRouter") + const salesChannelService: SalesChannelService = container + .resolve("salesChannelService") + .withTransaction(manager) + + const shippingProfileServiceTx = + shippingProfileService.withTransaction(manager) + + const shippingProfiles = await shippingProfileServiceTx.list({ + type: [ShippingProfileType.DEFAULT, ShippingProfileType.GIFT_CARD], + }) + const defaultShippingProfile = shippingProfiles.find( + (sp) => sp.type === ShippingProfileType.DEFAULT + ) + const gitCardShippingProfile = shippingProfiles.find( + (sp) => sp.type === ShippingProfileType.GIFT_CARD + ) + + let defaultSalesChannel: SalesChannel | undefined + if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) { + defaultSalesChannel = await salesChannelService + .withTransaction(manager) + .retrieveDefault() + } + + const productsHandleShippingProfileIdMap = new Map< + ProductHandle, + ShippingProfileId + >() + const productsHandleSalesChannelsMap = new Map< + ProductHandle, + SalesChannelId[] + >() + const productsHandleVariantsIndexPricesMap = new Map< + ProductHandle, + VariantIndexAndPrices[] + >() + + for (const product of data) { + product.handle ??= kebabCase(product.title) + + if (product.is_giftcard) { + productsHandleShippingProfileIdMap.set( + product.handle!, + gitCardShippingProfile!.id + ) + } else { + productsHandleShippingProfileIdMap.set( + product.handle!, + defaultShippingProfile!.id + ) + } + + if ( + featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key) && + !product.sales_channels?.length + ) { + productsHandleSalesChannelsMap.set(product.handle!, [ + defaultSalesChannel!.id, + ]) + } else { + productsHandleSalesChannelsMap.set( + product.handle!, + product.sales_channels!.map((s) => s.id) + ) + } + + if (product.variants) { + const hasPrices = product.variants.some((variant) => { + return variant.prices.length > 0 + }) + + if (hasPrices) { + const items = + productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] + + product.variants.forEach((variant, index) => { + items.push({ + index, + prices: variant.prices, + }) + }) + + productsHandleVariantsIndexPricesMap.set(product.handle!, items) + } + } + } + + data = data.map((productData) => { + delete productData.sales_channels + return productData + }) + + return { + products: data as ProductTypes.CreateProductDTO[], + productsHandleShippingProfileIdMap, + productsHandleSalesChannelsMap, + productsHandleVariantsIndexPricesMap, + } +} diff --git a/packages/medusa/src/workflows/functions/remove-inventory-items.ts b/packages/medusa/src/workflows/functions/remove-inventory-items.ts index 1ec59b7a52..a3bfe3d725 100644 --- a/packages/medusa/src/workflows/functions/remove-inventory-items.ts +++ b/packages/medusa/src/workflows/functions/remove-inventory-items.ts @@ -15,12 +15,8 @@ export async function removeInventoryItems({ const inventoryService = container.resolve("inventoryService") const context = { transactionManager: manager } - return await Promise.all( - data.map(async ({ inventoryItem }) => { - return await inventoryService!.deleteInventoryItem( - inventoryItem.id, - context - ) - }) + return await inventoryService!.deleteInventoryItem( + data.map(({ inventoryItem }) => inventoryItem.id), + context ) } diff --git a/packages/medusa/src/workflows/functions/remove-products.ts b/packages/medusa/src/workflows/functions/remove-products.ts index 926cded0f8..8a22e10d59 100644 --- a/packages/medusa/src/workflows/functions/remove-products.ts +++ b/packages/medusa/src/workflows/functions/remove-products.ts @@ -1,12 +1,13 @@ import { MedusaContainer, ProductTypes } from "@medusajs/types" -export async function createProducts({ +export async function removeProducts({ container, data, }: { container: MedusaContainer - data: ProductTypes.CreateProductDTO[] + data: ProductTypes.ProductDTO[] }) { - const productModuleService = container.resolve("productModuleService") - return await productModuleService.create(data) + const productModuleService: ProductTypes.IProductModuleService = + container.resolve("productModuleService") + return await productModuleService.softDelete(data.map((p) => p.id)) } diff --git a/packages/medusa/src/workflows/functions/update-products-variants-prices.ts b/packages/medusa/src/workflows/functions/update-products-variants-prices.ts new file mode 100644 index 0000000000..d0b1793244 --- /dev/null +++ b/packages/medusa/src/workflows/functions/update-products-variants-prices.ts @@ -0,0 +1,62 @@ +import { MedusaContainer, ProductTypes } from "@medusajs/types" +import { EntityManager } from "typeorm" +import { ProductVariantService } from "../../services" +import { + ProductVariantPricesCreateReq, + UpdateVariantPricesData, +} from "../../types/product-variant" +import { MedusaError } from "@medusajs/utils" + +type ProductHandle = string +type VariantIndexAndPrices = { + index: number + prices: ProductVariantPricesCreateReq[] +} + +export async function updateProductsVariantsPrices({ + container, + manager, + data, +}: { + container: MedusaContainer + manager: EntityManager + data: { + products: ProductTypes.ProductDTO[] + productsHandleVariantsIndexPricesMap: Map< + ProductHandle, + VariantIndexAndPrices[] + > + } +}) { + const productVariantService: ProductVariantService = container.resolve( + "productVariantService" + ) + const productVariantServiceTx = productVariantService.withTransaction(manager) + + const variantIdsPricesData: UpdateVariantPricesData[] = [] + const productsMap = new Map( + data.products.map((p) => [p.handle!, p]) + ) + + for (const mapData of data.productsHandleVariantsIndexPricesMap.entries()) { + const [handle, variantData] = mapData + + const product = productsMap.get(handle) + if (!product) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product with handle ${handle} not found` + ) + } + + variantData.forEach((item, index) => { + const variant = product.variants[index] + variantIdsPricesData.push({ + variantId: variant.id, + prices: item.prices, + }) + }) + } + + await productVariantServiceTx.updateVariantPrices(variantIdsPricesData) +} diff --git a/packages/product/src/migrations/.snapshot-medusa-products.json b/packages/product/src/migrations/.snapshot-medusa-products.json index cf29bd0bcf..523b4eed1c 100644 --- a/packages/product/src/migrations/.snapshot-medusa-products.json +++ b/packages/product/src/migrations/.snapshot-medusa-products.json @@ -845,8 +845,8 @@ "nullable": false, "mappedType": "text" }, - "product_image_id": { - "name": "product_image_id", + "image_id": { + "name": "image_id", "type": "text", "unsigned": false, "autoincrement": false, @@ -860,7 +860,10 @@ "indexes": [ { "keyName": "product_images_pkey", - "columnNames": ["product_id", "product_image_id"], + "columnNames": [ + "product_id", + "image_id" + ], "composite": true, "primary": true, "unique": true @@ -877,9 +880,11 @@ "deleteRule": "cascade", "updateRule": "cascade" }, - "product_images_product_image_id_foreign": { - "constraintName": "product_images_product_image_id_foreign", - "columnNames": ["product_image_id"], + "product_images_image_id_foreign": { + "constraintName": "product_images_image_id_foreign", + "columnNames": [ + "image_id" + ], "localTableName": "public.product_images", "referencedColumnNames": ["id"], "referencedTableName": "public.image", @@ -1119,6 +1124,15 @@ "default": "0", "mappedType": "decimal" }, + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -1148,15 +1162,6 @@ "nullable": true, "length": 6, "mappedType": "datetime" - }, - "product_id": { - "name": "product_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" } }, "name": "product_variant", diff --git a/packages/product/src/migrations/Migration20230710091208.ts b/packages/product/src/migrations/Migration20230719100648.ts similarity index 94% rename from packages/product/src/migrations/Migration20230710091208.ts rename to packages/product/src/migrations/Migration20230719100648.ts index 2efaa5f9d6..cc5f853733 100644 --- a/packages/product/src/migrations/Migration20230710091208.ts +++ b/packages/product/src/migrations/Migration20230719100648.ts @@ -1,6 +1,6 @@ import { Migration } from "@mikro-orm/migrations" -export class Migration20230710091208 extends Migration { +export class Migration20230719100648 extends Migration { async up(): Promise { this.addSql( 'create table "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, "updated_at" timestamptz not null, constraint "product_category_pkey" primary key ("id"));' @@ -70,7 +70,7 @@ export class Migration20230710091208 extends Migration { ) this.addSql( - 'create table "product_images" ("product_id" text not null, "product_image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "product_image_id"));' + 'create table "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));' ) this.addSql( @@ -78,7 +78,7 @@ export class Migration20230710091208 extends Migration { ) this.addSql( - 'create table "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, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "product_id" text not null, constraint "product_variant_pkey" primary key ("id"));' + 'create table "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 not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, constraint "product_variant_pkey" primary key ("id"));' ) this.addSql( 'create index "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");' @@ -138,7 +138,7 @@ export class Migration20230710091208 extends Migration { 'alter table "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 "product_images" add constraint "product_images_product_image_id_foreign" foreign key ("product_image_id") references "image" ("id") on update cascade on delete cascade;' + 'alter table "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;' ) this.addSql( diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index 941e40c0c6..4eea997655 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -32,7 +32,7 @@ class ProductOptionValue { @Property({ columnType: "text" }) value: string - @Property({ persist: false }) + @Property({ columnType: "text", nullable: true }) option_id!: string @ManyToOne(() => ProductOption, { @@ -41,7 +41,7 @@ class ProductOptionValue { }) option: ProductOption - @Property({ persist: false }) + @Property({ columnType: "text", nullable: true }) variant_id!: string @ManyToOne(() => ProductVariant, { diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index a764795d31..d05999ae3e 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -29,7 +29,7 @@ class ProductOption { @Property({ columnType: "text" }) title: string - @Property({ persist: false }) + @Property({ columnType: "text", nullable: true }) product_id!: string @ManyToOne(() => Product, { diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index 3bc78441fc..5e754d698d 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -105,7 +105,7 @@ class ProductVariant { @Property({ columnType: "numeric", nullable: true, default: 0 }) variant_rank?: number | null - @Property({ persist: false }) + @Property({ columnType: "text", nullable: true }) product_id!: string @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 1a228b9ea5..45f77a7dcd 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -100,7 +100,7 @@ class Product { @Property({ columnType: "text", nullable: true }) material?: string | null - @Property({ persist: false }) + @Property({ columnType: "text", nullable: true }) collection_id!: string @ManyToOne(() => ProductCollection, { @@ -109,7 +109,7 @@ class Product { }) collection!: ProductCollection - @Property({ persist: false }) + @Property({ columnType: "text", nullable: true }) type_id!: string @ManyToOne(() => ProductType, { @@ -132,6 +132,8 @@ class Product { pivotTable: "product_images", index: "IDX_product_image_id", cascade: ["soft-remove"] as any, + joinColumn: "product_id", + inverseJoinColumn: "image_id", }) images = new Collection(this) diff --git a/packages/product/src/module-definition.ts b/packages/product/src/module-definition.ts index 0485dfdbd0..d47414c790 100644 --- a/packages/product/src/module-definition.ts +++ b/packages/product/src/module-definition.ts @@ -1,5 +1,3 @@ -import * as ProductModels from "@models" - import { ModuleExports } from "@medusajs/types" import { ProductModuleService } from "@services" import loadConnection from "./loaders/connection" @@ -7,10 +5,8 @@ import loadContainer from "./loaders/container" const service = ProductModuleService const loaders = [loadContainer, loadConnection] as any -const models = Object.values(ProductModels) export const moduleDefinition: ModuleExports = { service, loaders, - models, } diff --git a/packages/product/src/repositories/base.ts b/packages/product/src/repositories/base.ts index 0321f86610..ab7b9f3af9 100644 --- a/packages/product/src/repositories/base.ts +++ b/packages/product/src/repositories/base.ts @@ -6,7 +6,6 @@ import { MedusaContext, } from "@medusajs/utils" import { serialize } from "@mikro-orm/core" -import { doNotForceTransaction } from "../utils" // TODO: Should we create a mikro orm specific package for this and the soft deletable decorator util? @@ -28,28 +27,17 @@ async function transactionWrapper( return await task(transaction) } - const forkedManager = this.manager_.fork() - const options = {} + + if (transaction) { + Object.assign(options, { ctx: transaction }) + } + if (isolationLevel) { Object.assign(options, { isolationLevel }) } - if (transaction) { - Object.assign(options, { ctx: transaction }) - await forkedManager.begin(options) - } else { - await forkedManager.begin(options) - } - - try { - const result = await task(forkedManager) - await forkedManager.commit() - return result - } catch (e) { - await forkedManager.rollback() - throw e - } + return await (this.manager_ as SqlEntityManager).transactional(task, options) } const updateDeletedAtRecursively = async ( diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts index 5efaef2ff3..2bba6dd8e2 100644 --- a/packages/product/src/repositories/product.ts +++ b/packages/product/src/repositories/product.ts @@ -126,7 +126,6 @@ export class ProductRepository extends AbstractBaseRepository { @MedusaContext() { transactionManager: manager }: Context = {} ): Promise { - console.log((this as any).prototype) const products = data.map((product) => { return (manager as SqlEntityManager).create(Product, product) }) diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 68c185745a..c58e7ba35a 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -349,12 +349,12 @@ export default class ProductModuleService< } if (isDefined(productData.type)) { - productData.type_id = ( + productData.type = ( await this.productTypeService_.upsert( [productData.type as ProductTypes.CreateProductTypeDTO], sharedContext ) - )?.[0]!.id + )?.[0]! } return productData as CreateProductOnlyDTO diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 3d49dd09ee..449ad3448c 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -12,6 +12,7 @@ export enum ProductStatus { * DTO in and out of the module (module API) */ +// TODO: This DTO should represent the product, when used in config we should use Partial, it means that some props like handle should be updated to not be optional export interface ProductDTO { id: string title: string @@ -63,6 +64,7 @@ export interface ProductVariantDTO { options: ProductOptionValueDTO metadata?: Record | null product: ProductDTO + product_id: string variant_rank?: number | null created_at: string | Date updated_at: string | Date diff --git a/packages/utils/src/common/build-query.ts b/packages/utils/src/common/build-query.ts index ae835a94af..641c5b63cd 100644 --- a/packages/utils/src/common/build-query.ts +++ b/packages/utils/src/common/build-query.ts @@ -65,6 +65,7 @@ export function buildRelations(relationCollection: string[]): Relations { * @param collection */ function buildRelationsOrSelect(collection: string[]): Selects | Relations { + collection = collection.sort() const output: Selects | Relations = {} for (const relation of collection) { diff --git a/yarn.lock b/yarn.lock index 8caefb9e82..8f14a9ab93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6486,7 +6486,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/product@workspace:packages/product": +"@medusajs/product@workspace:^, @medusajs/product@workspace:packages/product": version: 0.0.0-use.local resolution: "@medusajs/product@workspace:packages/product" dependencies: @@ -24942,6 +24942,7 @@ __metadata: "@medusajs/cache-inmemory": "workspace:*" "@medusajs/event-bus-local": "workspace:*" "@medusajs/medusa": "workspace:*" + "@medusajs/product": "workspace:^" babel-preset-medusa-package: "*" faker: ^5.5.3 jest: ^26.6.3