feat(medusa): Continue create product workflow changes (#4473)

This commit is contained in:
Adrien de Peretti authored and GitHub committed 2023-07-24 13:30:24 +02:00
1 parent edf93d972d
commit d2a8cf0378
103 files changed
+1845 -510

No files matched your search

@@ -43,7 +43,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -157,7 +159,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -12,10 +12,7 @@ const {
DiscountConditionOperator, DiscountConditionOperator,
} = require("@medusajs/medusa") } = require("@medusajs/medusa")
const { IdMap } = require("medusa-test-utils") const { IdMap } = require("medusa-test-utils")
const { const { simpleDiscountFactory } = require("../../../factories")
simpleDiscountFactory,
simpleSalesChannelFactory,
} = require("../../../factories")
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -1879,7 +1879,7 @@ describe("/admin/discounts", () => {
const cond = discount.data.discount.rule.conditions[0] const cond = discount.data.discount.rule.conditions[0]
const response = await api.post( 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], products: [prod2.id],
}, },
@@ -1911,6 +1911,8 @@ describe("/admin/discounts", () => {
created_at: expect.any(String), created_at: expect.any(String),
updated_at: expect.any(String), updated_at: expect.any(String),
profile_id: expect.any(String), profile_id: expect.any(String),
profiles: expect.any(Array),
profile: expect.any(Object),
type_id: expect.any(String), type_id: expect.any(String),
id: "test-product", id: "test-product",
}, },
@@ -2047,7 +2049,7 @@ describe("/admin/discounts", () => {
const api = useApi() const api = useApi()
const discountCondition = await api.get( 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 adminReqConfig
) )
@@ -2061,6 +2063,8 @@ describe("/admin/discounts", () => {
{ {
id: "test-product", id: "test-product",
profile_id: expect.any(String), profile_id: expect.any(String),
profiles: expect.any(Array),
profile: expect.any(Object),
type_id: expect.any(String), type_id: expect.any(String),
created_at: expect.any(String), created_at: expect.any(String),
updated_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 cond = discount.data.discount.rule.conditions[0]
const response = await api.post( 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 }], resources: [{ id: prod2.id }, { id: prod3.id }],
}, },
@@ -2490,7 +2494,7 @@ describe("/admin/discounts", () => {
const cond = discount.data.discount.rule.conditions[0] const cond = discount.data.discount.rule.conditions[0]
const response = await api.delete( 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, ...adminReqConfig,
data: { data: {
@@ -26,9 +26,6 @@ const { IdMap } = require("medusa-test-utils")
jest.setTimeout(50000) jest.setTimeout(50000)
const testProductId = "test-product"
const testProduct1Id = "test-product1"
const testProductFilteringId1 = "test-product_filtering_1"
const adminHeaders = { const adminHeaders = {
headers: { headers: {
Authorization: "Bearer test_token", Authorization: "Bearer test_token",
@@ -28,7 +28,7 @@ describe("/admin/shipping-options", () => {
beforeAll(async () => { beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..")) const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd }) dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd, verbose: false }) medusaProcess = await setupServer({ cwd })
}) })
afterAll(async () => { afterAll(async () => {
@@ -25,7 +25,7 @@ describe("/admin/shipping-profiles", () => {
beforeAll(async () => { beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..")) const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd }) dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd, verbose: false }) medusaProcess = await setupServer({ cwd })
}) })
afterAll(async () => { afterAll(async () => {
@@ -222,6 +222,7 @@ describe("/store/products", () => {
"tags", "tags",
"collection", "collection",
"type", "type",
"profiles",
]) ])
}) })
@@ -992,7 +993,12 @@ describe("/store/products", () => {
it("response contains only fields defined with `fields` param", async () => { it("response contains only fields defined with `fields` param", async () => {
const api = useApi() 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( const response = await api.get(
`/store/products/test-product?fields=${fields.join(",")}` `/store/products/test-product?fields=${fields.join(",")}`
@@ -91,8 +91,11 @@ export const simpleProductFactory = async (
discountable: !data.is_giftcard, discountable: !data.is_giftcard,
tags: [] as ProductTag[], tags: [] as ProductTag[],
profile_id: data.is_giftcard ? gcProfile?.id : defaultProfile?.id, profile_id: data.is_giftcard ? gcProfile?.id : defaultProfile?.id,
profiles: [
{ id: data.is_giftcard ? gcProfile?.id : defaultProfile?.id },
] as ShippingProfile[],
metadata: data.metadata || null, metadata: data.metadata || null,
} as Product } as unknown as Product
if (typeof data.tags !== "undefined") { if (typeof data.tags !== "undefined") {
for (let i = 0; i < data.tags.length; i++) { for (let i = 0; i < data.tags.length; i++) {
@@ -52,7 +52,7 @@ export const simpleProductVariantFactory = async (
const options = data.options || [{ option_id: "test-option", value: "Large" }] const options = data.options || [{ option_id: "test-option", value: "Large" }]
for (const o of options) { for (const o of options) {
await manager.insert(ProductOptionValue, { await manager.insert(ProductOptionValue, {
id: `${o.value}-${o.option_id ?? Math.random()}`, id: `${variant.id}-${o.option_id ?? Math.random()}`,
value: o.value, value: o.value,
variant_id: id, variant_id: id,
option_id: o.option_id, option_id: o.option_id,
@@ -40,13 +40,13 @@ export const simpleShippingOptionFactory = async (
const defaultProfile = await manager.findOne(ShippingProfile, { const defaultProfile = await manager.findOne(ShippingProfile, {
where: { where: {
type: ShippingProfileType.DEFAULT, type: ShippingProfileType.DEFAULT,
} },
}) })
const gcProfile = await manager.findOne(ShippingProfile, { const gcProfile = await manager.findOne(ShippingProfile, {
where: { where: {
type: ShippingProfileType.GIFT_CARD, type: ShippingProfileType.GIFT_CARD,
} },
}) })
let region_id = data.region_id let region_id = data.region_id
@@ -62,7 +62,7 @@ export const simpleShippingOptionFactory = async (
is_return: data.is_return ?? false, is_return: data.is_return ?? false,
region_id: region_id, region_id: region_id,
provider_id: "test-ful", 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, price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE,
data: data.data ?? {}, data: data.data ?? {},
requirements: (data.requirements || []) as ShippingOptionRequirement[], requirements: (data.requirements || []) as ShippingOptionRequirement[],
@@ -106,6 +106,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-reg", handle: "test-product-reg",
title: "Multi Reg Test product", title: "Multi Reg Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
status: "published", status: "published",
collection_id: "test-collection", collection_id: "test-collection",
+6 -2
View File
@@ -444,14 +444,16 @@ module.exports = async (dataSource, data = {}) => {
await manager.save(priceList1) await manager.save(priceList1)
await manager.insert(Product, { const giftCardProduct = manager.create(Product, {
id: "giftcard-product", id: "giftcard-product",
title: "Giftcard", title: "Giftcard",
is_giftcard: true, is_giftcard: true,
discountable: false, discountable: false,
profile_id: gcProfile.id, profile_id: gcProfile.id,
profiles: [{ id: gcProfile.id }],
options: [{ id: "denom", title: "Denomination" }], options: [{ id: "denom", title: "Denomination" }],
}) })
await manager.save(Product, giftCardProduct)
await manager.insert(ProductVariant, { await manager.insert(ProductVariant, {
id: "giftcard-denom", id: "giftcard-denom",
@@ -466,12 +468,14 @@ module.exports = async (dataSource, data = {}) => {
], ],
}) })
await manager.insert(Product, { const testProduct = manager.create(Product, {
id: "test-product", id: "test-product",
title: "test product", title: "test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
options: [{ id: "test-option", title: "Size" }], options: [{ id: "test-option", title: "Size" }],
}) })
await manager.save(Product, testProduct)
await manager.insert(ProductVariant, { await manager.insert(ProductVariant, {
id: "test-variant-quantity", id: "test-variant-quantity",
@@ -34,6 +34,7 @@ module.exports = async (dataSource, data = {}) => {
id: "test-product", id: "test-product",
title: "test product", title: "test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
options: [{ id: "test-option", title: "Size" }], options: [{ id: "test-option", title: "Size" }],
}) })
@@ -52,6 +53,7 @@ module.exports = async (dataSource, data = {}) => {
id: "test-product-2", id: "test-product-2",
title: "test product 2", title: "test product 2",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
options: [{ id: "test-option-color", title: "Color" }], options: [{ id: "test-option-color", title: "Color" }],
}) })
@@ -33,6 +33,7 @@ module.exports = async (dataSource, data = {}) => {
id: "test-product", id: "test-product",
title: "test product", title: "test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
options: [{ id: "test-option", title: "Size" }], options: [{ id: "test-option", title: "Size" }],
}) })
@@ -238,6 +238,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product", handle: "test-product",
title: "Test product", title: "Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -305,6 +306,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-quantity", handle: "test-product-quantity",
title: "Test product", title: "Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -413,6 +415,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-sale", handle: "test-product-sale",
title: "Test product sale", title: "Test product sale",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -480,6 +483,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-sale-overlap", handle: "test-product-sale-overlap",
title: "Test product sale", title: "Test product sale",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -540,6 +544,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-multi-region", handle: "test-product-multi-region",
title: "Test product", title: "Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-multi-region-description1", description: "test-product-multi-region-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -606,6 +611,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-quantity-customer", handle: "test-product-quantity-customer",
title: "Test product", title: "Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -713,6 +719,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-sale-customer", handle: "test-product-sale-customer",
title: "Test product sale", title: "Test product sale",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -781,6 +788,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product-sale-customer-quantity", handle: "test-product-sale-customer-quantity",
title: "Test product sale", title: "Test product sale",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
tags: [], tags: [],
@@ -98,6 +98,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product", handle: "test-product",
title: "Test product", title: "Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
collection_id: "test-collection", collection_id: "test-collection",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
@@ -215,6 +216,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product1", handle: "test-product1",
title: "Test product1", title: "Test product1",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description1", description: "test-product-description1",
collection_id: "test-collection", collection_id: "test-collection",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
@@ -283,6 +285,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product_filtering_1", handle: "test-product_filtering_1",
title: "Test product filtering 1", title: "Test product filtering 1",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
collection_id: "test-collection1", collection_id: "test-collection1",
@@ -297,6 +300,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product_filtering_2", handle: "test-product_filtering_2",
title: "Test product filtering 2", title: "Test product filtering 2",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
collection_id: "test-collection2", collection_id: "test-collection2",
@@ -311,6 +315,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product_filtering_3", handle: "test-product_filtering_3",
title: "Test product filtering 3", title: "Test product filtering 3",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
collection_id: "test-collection1", collection_id: "test-collection1",
@@ -325,6 +330,7 @@ module.exports = async (dataSource, data = {}) => {
handle: "test-product_filtering_4", handle: "test-product_filtering_4",
title: "Test product filtering 4", title: "Test product filtering 4",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
status: "proposed", status: "proposed",
deleted_at: new Date().toISOString(), deleted_at: new Date().toISOString(),
@@ -130,6 +130,7 @@ module.exports = async (dataSource, defaultSalesChannel) => {
handle: "test-product", handle: "test-product",
title: "Test product", title: "Test product",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
status: "published", status: "published",
collection_id: "test-collection", collection_id: "test-collection",
@@ -323,6 +324,7 @@ module.exports = async (dataSource, defaultSalesChannel) => {
handle: "test-product_filtering_1", handle: "test-product_filtering_1",
title: "Test product filtering 1", title: "Test product filtering 1",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
collection_id: "test-collection1", collection_id: "test-collection1",
@@ -338,6 +340,7 @@ module.exports = async (dataSource, defaultSalesChannel) => {
handle: "test-product_filtering_2", handle: "test-product_filtering_2",
title: "Test product filtering 2", title: "Test product filtering 2",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
collection_id: "test-collection2", collection_id: "test-collection2",
@@ -353,6 +356,7 @@ module.exports = async (dataSource, defaultSalesChannel) => {
handle: "test-product_filtering_3", handle: "test-product_filtering_3",
title: "Test product filtering 3", title: "Test product filtering 3",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
collection_id: "test-collection1", collection_id: "test-collection1",
@@ -369,6 +373,7 @@ module.exports = async (dataSource, defaultSalesChannel) => {
is_giftcard: true, is_giftcard: true,
title: "giftcard", title: "giftcard",
profile_id: defaultProfile.id, profile_id: defaultProfile.id,
profiles: [{ id: defaultProfile.id }],
description: "test-product-description", description: "test-product-description",
type: { id: "test-type", value: "test-type" }, type: { id: "test-type", value: "test-type" },
status: "published", status: "published",
@@ -83,7 +83,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -240,7 +242,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -395,7 +399,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -515,7 +521,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -693,7 +701,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -861,7 +871,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -1112,7 +1124,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -1361,7 +1375,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -1570,7 +1586,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -1690,7 +1708,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -1868,7 +1888,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -2030,7 +2052,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -2139,7 +2163,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -2255,7 +2281,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -2372,7 +2400,9 @@ Object {
"metadata": null, "metadata": null,
"mid_code": null, "mid_code": null,
"origin_country": null, "origin_country": null,
"profile": Any<Object>,
"profile_id": Any<String>, "profile_id": Any<String>,
"profiles": Any<Array>,
"status": "draft", "status": "draft",
"subtitle": null, "subtitle": null,
"thumbnail": null, "thumbnail": null,
@@ -120,6 +120,8 @@ describe("medusa-plugin-sendgrid", () => {
updated_at: expect.any(Date), updated_at: expect.any(Date),
product: { product: {
profile_id: expect.any(String), profile_id: expect.any(String),
profile: expect.any(Object),
profiles: expect.any(Array),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
}, },
@@ -242,6 +244,8 @@ describe("medusa-plugin-sendgrid", () => {
updated_at: expect.any(Date), updated_at: expect.any(Date),
product: { product: {
profile_id: expect.any(String), profile_id: expect.any(String),
profile: expect.any(Object),
profiles: expect.any(Array),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
}, },
@@ -303,6 +307,8 @@ describe("medusa-plugin-sendgrid", () => {
updated_at: expect.any(Date), updated_at: expect.any(Date),
product: { product: {
profile_id: expect.any(String), profile_id: expect.any(String),
profile: expect.any(Object),
profiles: expect.any(Array),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
}, },
@@ -485,6 +491,8 @@ describe("medusa-plugin-sendgrid", () => {
updated_at: expect.any(Date), updated_at: expect.any(Date),
product: { product: {
profile_id: expect.any(String), profile_id: expect.any(String),
profile: expect.any(Object),
profiles: expect.any(Array),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
}, },
@@ -592,6 +600,8 @@ describe("medusa-plugin-sendgrid", () => {
updated_at: expect.any(Date), updated_at: expect.any(Date),
product: { product: {
profile_id: expect.any(String), profile_id: expect.any(String),
profile: expect.any(Object),
profiles: expect.any(Array),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
}, },
@@ -780,6 +790,8 @@ const getReturnSnap = (received = false) => {
updated_at: expect.any(Date), updated_at: expect.any(Date),
product: { product: {
profile_id: expect.any(String), profile_id: expect.any(String),
profile: expect.any(Object),
profiles: expect.any(Array),
created_at: expect.any(Date), created_at: expect.any(Date),
updated_at: expect.any(Date), updated_at: expect.any(Date),
}, },
@@ -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,
})
)
})
})
})
+13 -1
View File
@@ -2,6 +2,7 @@ const DB_HOST = process.env.DB_HOST
const DB_USERNAME = process.env.DB_USERNAME const DB_USERNAME = process.env.DB_USERNAME
const DB_PASSWORD = process.env.DB_PASSWORD const DB_PASSWORD = process.env.DB_PASSWORD
const DB_NAME = process.env.DB_TEMP_NAME const DB_NAME = process.env.DB_TEMP_NAME
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
module.exports = { module.exports = {
plugins: [ plugins: [
@@ -23,7 +24,7 @@ module.exports = {
], ],
projectConfig: { projectConfig: {
// redis_url: REDIS_URL, // redis_url: REDIS_URL,
database_url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`, database_url: DB_URL,
database_type: "postgres", database_type: "postgres",
jwt_secret: "test", jwt_secret: "test",
cookie_secret: "test", cookie_secret: "test",
@@ -44,5 +45,16 @@ module.exports = {
resolve: "@medusajs/cache-inmemory", resolve: "@medusajs/cache-inmemory",
options: { ttl: 5 }, options: { ttl: 5 },
}, },
productModuleService: {
scope: "internal",
resources: "isolated",
resolve: "@medusajs/product",
options: {
database: {
clientUrl: DB_URL,
debug: false,
},
},
},
}, },
} }
+1
View File
@@ -12,6 +12,7 @@
"@medusajs/cache-inmemory": "workspace:*", "@medusajs/cache-inmemory": "workspace:*",
"@medusajs/event-bus-local": "workspace:*", "@medusajs/event-bus-local": "workspace:*",
"@medusajs/medusa": "workspace:*", "@medusajs/medusa": "workspace:*",
"@medusajs/product": "workspace:^",
"faker": "^5.5.3", "faker": "^5.5.3",
"medusa-fulfillment-webshipper": "workspace:*", "medusa-fulfillment-webshipper": "workspace:*",
"medusa-interfaces": "workspace:*", "medusa-interfaces": "workspace:*",
@@ -16,19 +16,12 @@ const orderRelations = [
"returns.shipping_method", "returns.shipping_method",
"returns.shipping_method.tax_lines", "returns.shipping_method.tax_lines",
"refunds", "refunds",
"claims",
"claims.claim_items",
"claims.claim_items.item", "claims.claim_items.item",
"claims.fulfillments", "claims.fulfillments",
"claims.return_order", "claims.return_order",
"claims.additional_items", "claims.additional_items.variant.product.profiles",
"claims.additional_items.variant",
"claims.additional_items.variant.product",
"swaps",
"swaps.return_order", "swaps.return_order",
"swaps.additional_items", "swaps.additional_items.variant.product.profiles",
"swaps.additional_items.variant",
"swaps.additional_items.variant.product",
"swaps.fulfillments", "swaps.fulfillments",
"returnable_items", "returnable_items",
] ]
@@ -7,6 +7,7 @@ import type { Cart } from "./Cart"
import type { Discount } from "./Discount" import type { Discount } from "./Discount"
import type { DraftOrder } from "./DraftOrder" import type { DraftOrder } from "./DraftOrder"
import type { LineItem } from "./LineItem" import type { LineItem } from "./LineItem"
import type { Product } from "./Product"
import type { ProductVariant } from "./ProductVariant" import type { ProductVariant } from "./ProductVariant"
import type { Region } from "./Region" import type { Region } from "./Region"
import type { ShippingMethod } from "./ShippingMethod" import type { ShippingMethod } from "./ShippingMethod"
@@ -58,7 +59,12 @@ export interface AdminDraftOrdersRes {
| "variant" | "variant"
>, >,
{ {
variant: SetRelation<ProductVariant, "product"> variant: Merge<
SetRelation<ProductVariant, "product">,
{
product: SetRelation<Product, "profiles">
}
>
} }
> >
> >
@@ -10,6 +10,7 @@ import type { Fulfillment } from "./Fulfillment"
import type { GiftCardTransaction } from "./GiftCardTransaction" import type { GiftCardTransaction } from "./GiftCardTransaction"
import type { LineItem } from "./LineItem" import type { LineItem } from "./LineItem"
import type { Order } from "./Order" import type { Order } from "./Order"
import type { Product } from "./Product"
import type { ProductVariant } from "./ProductVariant" import type { ProductVariant } from "./ProductVariant"
import type { Region } from "./Region" import type { Region } from "./Region"
import type { Return } from "./Return" import type { Return } from "./Return"
@@ -112,7 +113,12 @@ export interface AdminOrdersListRes {
| "variant" | "variant"
>, >,
{ {
variant: SetRelation<ProductVariant, "product"> variant: Merge<
SetRelation<ProductVariant, "product">,
{
product: SetRelation<Product, "profiles">
}
>
} }
> >
> >
@@ -10,6 +10,7 @@ import type { Fulfillment } from "./Fulfillment"
import type { GiftCardTransaction } from "./GiftCardTransaction" import type { GiftCardTransaction } from "./GiftCardTransaction"
import type { LineItem } from "./LineItem" import type { LineItem } from "./LineItem"
import type { Order } from "./Order" import type { Order } from "./Order"
import type { Product } from "./Product"
import type { ProductVariant } from "./ProductVariant" import type { ProductVariant } from "./ProductVariant"
import type { Region } from "./Region" import type { Region } from "./Region"
import type { Return } from "./Return" import type { Return } from "./Return"
@@ -109,7 +110,12 @@ export interface AdminOrdersRes {
| "variant" | "variant"
>, >,
{ {
variant: SetRelation<ProductVariant, "product"> variant: Merge<
SetRelation<ProductVariant, "product">,
{
product: SetRelation<Product, "profiles">
}
>
} }
> >
> >
@@ -73,6 +73,10 @@ export interface Product {
* Available if the relation `profile` is expanded. * Available if the relation `profile` is expanded.
*/ */
profile?: ShippingProfile | null profile?: ShippingProfile | null
/**
* Available if the relation `profiles` is expanded.
*/
profiles?: Array<ShippingProfile> | null
/** /**
* The weight of the Product Variant. May be used in shipping rate calculations. * The weight of the Product Variant. May be used in shipping rate calculations.
*/ */
@@ -6,6 +6,7 @@ import { SetRelation, Merge } from "../core/ModelUtils"
import type { Cart } from "./Cart" import type { Cart } from "./Cart"
import type { Discount } from "./Discount" import type { Discount } from "./Discount"
import type { LineItem } from "./LineItem" import type { LineItem } from "./LineItem"
import type { Product } from "./Product"
import type { ProductVariant } from "./ProductVariant" import type { ProductVariant } from "./ProductVariant"
import type { Region } from "./Region" import type { Region } from "./Region"
import type { ShippingMethod } from "./ShippingMethod" import type { ShippingMethod } from "./ShippingMethod"
@@ -54,7 +55,12 @@ export interface StoreCartsRes {
| "tax_lines" | "tax_lines"
>, >,
{ {
variant: SetRelation<ProductVariant, "product"> variant: Merge<
SetRelation<ProductVariant, "product">,
{
product: SetRelation<Product, "profiles">
}
>
} }
> >
> >
@@ -9,6 +9,7 @@ import type { Fulfillment } from "./Fulfillment"
import type { GiftCardTransaction } from "./GiftCardTransaction" import type { GiftCardTransaction } from "./GiftCardTransaction"
import type { LineItem } from "./LineItem" import type { LineItem } from "./LineItem"
import type { Order } from "./Order" import type { Order } from "./Order"
import type { Product } from "./Product"
import type { ProductVariant } from "./ProductVariant" import type { ProductVariant } from "./ProductVariant"
import type { Region } from "./Region" import type { Region } from "./Region"
import type { ShippingMethod } from "./ShippingMethod" import type { ShippingMethod } from "./ShippingMethod"
@@ -63,7 +64,12 @@ export interface StoreCustomersListOrdersRes {
| "tax_lines" | "tax_lines"
>, >,
{ {
variant: SetRelation<ProductVariant, "product"> variant: Merge<
SetRelation<ProductVariant, "product">,
{
product: SetRelation<Product, "profiles">
}
>
} }
> >
> >
@@ -9,6 +9,7 @@ import type { Fulfillment } from "./Fulfillment"
import type { GiftCardTransaction } from "./GiftCardTransaction" import type { GiftCardTransaction } from "./GiftCardTransaction"
import type { LineItem } from "./LineItem" import type { LineItem } from "./LineItem"
import type { Order } from "./Order" import type { Order } from "./Order"
import type { Product } from "./Product"
import type { ProductVariant } from "./ProductVariant" import type { ProductVariant } from "./ProductVariant"
import type { Region } from "./Region" import type { Region } from "./Region"
import type { ShippingMethod } from "./ShippingMethod" import type { ShippingMethod } from "./ShippingMethod"
@@ -62,7 +63,12 @@ export interface StoreOrdersRes {
| "tax_lines" | "tax_lines"
>, >,
{ {
variant: SetRelation<ProductVariant, "product"> variant: Merge<
SetRelation<ProductVariant, "product">,
{
product: SetRelation<Product, "profiles">
}
>
} }
> >
> >
@@ -42,9 +42,7 @@ export default async (req, res) => {
"region", "region",
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
], ],
}) })
const shippingOptions = await shippingProfileService.fetchCartOptions(cart) const shippingOptions = await shippingProfileService.fetchCartOptions(cart)
@@ -264,6 +264,7 @@ class SendGridService extends NotificationService {
} }
let status let status
await SendGrid.send(sendOptions) await SendGrid.send(sendOptions)
.then(() => { .then(() => {
status = "sent" status = "sent"
@@ -613,11 +614,8 @@ class SendGridService extends NotificationService {
// Fetch the return request // Fetch the return request
const returnRequest = await this.returnService_.retrieve(return_id, { const returnRequest = await this.returnService_.retrieve(return_id, {
relations: [ relations: [
"items",
"items.item",
"items.item.tax_lines", "items.item.tax_lines",
"items.item.variant", "items.item.variant.product.profiles",
"items.item.variant.product",
"shipping_method", "shipping_method",
"shipping_method.tax_lines", "shipping_method.tax_lines",
"shipping_method.shipping_option", "shipping_method.shipping_option",
@@ -628,7 +626,9 @@ class SendGridService extends NotificationService {
{ {
id: returnRequest.items.map(({ item_id }) => item_id), id: returnRequest.items.map(({ item_id }) => item_id),
}, },
{ relations: ["tax_lines", "variant", "variant.product"] } {
relations: ["tax_lines", "variant", "variant.product.profiles"],
}
) )
// Fetch the order // Fetch the order
@@ -774,7 +774,7 @@ class SendGridService extends NotificationService {
}) })
const cart = await this.cartService_.retrieve(swap.cart_id, { const cart = await this.cartService_.retrieve(swap.cart_id, {
relations: ["items", "items.variant", "items.variant.product"], relations: ["items.variant.product.profiles"],
select: [ select: [
"total", "total",
"tax_total", "tax_total",
@@ -855,8 +855,7 @@ class SendGridService extends NotificationService {
}) })
const swap = await this.swapService_.retrieve(id, { const swap = await this.swapService_.retrieve(id, {
relations: [ relations: [
"additional_items", "additional_items.variant.product.profiles",
"additional_items.variant.product",
"additional_items.tax_lines", "additional_items.tax_lines",
"return_order", "return_order",
"return_order.items", "return_order.items",
@@ -873,7 +872,7 @@ class SendGridService extends NotificationService {
id: returnRequest.items.map(({ item_id }) => item_id), 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, { const order = await this.orderService_.retrieve(swap.order_id, {
select: ["total"], select: ["total"],
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"items.tax_lines", "items.tax_lines",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
@@ -915,7 +912,7 @@ class SendGridService extends NotificationService {
"shipping_total", "shipping_total",
"subtotal", "subtotal",
], ],
relations: ["items", "items.variant", "items.variant.product"], relations: ["items.variant.product.profiles"],
}) })
const currencyCode = order.currency_code.toUpperCase() const currencyCode = order.currency_code.toUpperCase()
@@ -997,9 +994,7 @@ class SendGridService extends NotificationService {
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"shipping_methods.tax_lines", "shipping_methods.tax_lines",
"additional_items", "additional_items.variant.product.profiles",
"additional_items.variant",
"additional_items.variant.product",
"additional_items.tax_lines", "additional_items.tax_lines",
"return_order", "return_order",
"return_order.items", "return_order.items",
@@ -1011,14 +1006,11 @@ class SendGridService extends NotificationService {
"region", "region",
"items", "items",
"items.tax_lines", "items.tax_lines",
"items.variant", "items.variant.product.profiles",
"items.variant.product",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"swaps", "swaps",
"swaps.additional_items", "swaps.additional_items.variant.product.profiles",
"swaps.additional_items.variant",
"swaps.additional_items.variant.product",
"swaps.additional_items.tax_lines", "swaps.additional_items.tax_lines",
], ],
}) })
@@ -1031,7 +1023,7 @@ class SendGridService extends NotificationService {
"shipping_total", "shipping_total",
"subtotal", "subtotal",
], ],
relations: ["items", "items.variant", "items.variant.product"], relations: ["items.variant.product.profiles"],
}) })
const returnRequest = swap.return_order const returnRequest = swap.return_order
@@ -1040,7 +1032,7 @@ class SendGridService extends NotificationService {
id: returnRequest.items.map(({ item_id }) => item_id), 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 }) { async claimShipmentCreatedData({ id, fulfillment_id }) {
const claim = await this.claimService_.retrieve(id, { const claim = await this.claimService_.retrieve(id, {
relations: [ relations: [
"order", "order.items.variant.product.profiles",
"order.items",
"order.items.variant",
"order.items.variant.product",
"order.shipping_address", "order.shipping_address",
], ],
}) })
@@ -67,7 +67,7 @@ export const defaultAdminCollectionsFields = [
"created_at", "created_at",
"updated_at", "updated_at",
] ]
export const defaultAdminCollectionsRelations = ["products"] export const defaultAdminCollectionsRelations = ["products.profiles"]
/** /**
* @schema AdminCollectionsListRes * @schema AdminCollectionsListRes
@@ -48,7 +48,7 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
"metadata", "metadata",
"valid_duration", "valid_duration",
], ],
relations: ["rule", "parent_discount", "regions", "rule.conditions"], relations: ["parent_discount", "regions", "rule.conditions"],
} }
) )
@@ -20,12 +20,7 @@ const defaultFields = [
"valid_duration", "valid_duration",
] ]
const defaultRelations = [ const defaultRelations = ["parent_discount", "regions", "rule.conditions"]
"rule",
"parent_discount",
"regions",
"rule.conditions",
]
describe("GET /admin/discounts/:discount_id", () => { describe("GET /admin/discounts/:discount_id", () => {
describe("successful retrieval", () => { describe("successful retrieval", () => {
@@ -20,12 +20,7 @@ const defaultFields = [
"valid_duration", "valid_duration",
] ]
const defaultRelations = [ const defaultRelations = ["parent_discount", "regions", "rule.conditions"]
"rule",
"parent_discount",
"regions",
"rule.conditions",
]
describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => { describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => {
describe("successful removal", () => { describe("successful removal", () => {
@@ -209,7 +209,6 @@ export const defaultAdminDiscountsFields: (keyof Discount)[] = [
] ]
export const defaultAdminDiscountsRelations = [ export const defaultAdminDiscountsRelations = [
"rule",
"parent_discount", "parent_discount",
"regions", "regions",
"rule.conditions", "rule.conditions",
@@ -139,6 +139,7 @@ export type AdminPostDraftOrdersDraftOrderRegisterPaymentRes = {
* - cart.items.tax_lines * - cart.items.tax_lines
* - cart.items.variant * - cart.items.variant
* - cart.items.variant.product * - cart.items.variant.product
* - cart.items.variant.product.profiles
* - cart.region * - cart.region
* - cart.region.tax_rates * - cart.region.tax_rates
* - cart.shipping_address * - cart.shipping_address
@@ -543,6 +543,7 @@ export default (app, featureFlagRouter: FlagRouter) => {
* - items.tax_lines * - items.tax_lines
* - items.variant * - items.variant
* - items.variant.product * - items.variant.product
* - items.variant.product.profiles
* - refunds * - refunds
* - region * - region
* - shipping_methods * - shipping_methods
@@ -659,6 +660,7 @@ export type AdminOrdersRes = {
* - items.tax_lines * - items.tax_lines
* - items.variant * - items.variant
* - items.variant.product * - items.variant.product
* - items.variant.product.profiles
* - refunds * - refunds
* - region * - region
* - shipping_methods * - shipping_methods
@@ -40,7 +40,6 @@ describe("GET /admin/products/:id", () => {
"is_giftcard", "is_giftcard",
"discountable", "discountable",
"thumbnail", "thumbnail",
"profile_id",
"collection_id", "collection_id",
"type_id", "type_id",
"weight", "weight",
@@ -60,6 +59,7 @@ describe("GET /admin/products/:id", () => {
"variants", "variants",
"variants.prices", "variants.prices",
"variants.options", "variants.options",
"profiles",
"images", "images",
"options", "options",
"tags", "tags",
@@ -42,6 +42,7 @@ import {
createVariantsTransaction, createVariantsTransaction,
revertVariantTransaction, revertVariantTransaction,
} from "./transaction/create-product-variant" } from "./transaction/create-product-variant"
import { createProductsWorkflow } from "../../../../workflows/admin/create-products"
/** /**
* @oas [post] /admin/products * @oas [post] /admin/products
@@ -129,6 +130,19 @@ export default async (req, res) => {
) )
const entityManager: EntityManager = req.scope.resolve("manager") 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 product = await entityManager.transaction(async (manager) => {
const { variants } = validated const { variants } = validated
@@ -101,6 +101,7 @@ export const defaultAdminProductRelations = [
"variants", "variants",
"variants.prices", "variants.prices",
"variants.options", "variants.options",
"profiles",
"images", "images",
"options", "options",
"tags", "tags",
@@ -119,7 +120,6 @@ export const defaultAdminProductFields: (keyof Product)[] = [
"is_giftcard", "is_giftcard",
"discountable", "discountable",
"thumbnail", "thumbnail",
"profile_id",
"collection_id", "collection_id",
"type_id", "type_id",
"weight", "weight",
@@ -12,7 +12,7 @@ const defaultFields = [
"metadata", "metadata",
] ]
const defaultRelations = ["products", "shipping_options"] const defaultRelations = ["products.profiles", "shipping_options"]
describe("GET /admin/shipping-profiles/:profile_id", () => { describe("GET /admin/shipping-profiles/:profile_id", () => {
describe("successful retrieval", () => { describe("successful retrieval", () => {
@@ -41,8 +41,10 @@ export const defaultAdminShippingProfilesFields: (keyof ShippingProfile)[] = [
"metadata", "metadata",
] ]
export const defaultAdminShippingProfilesRelations: (keyof ShippingProfile)[] = export const defaultAdminShippingProfilesRelations: string[] = [
["products", "shipping_options"] "products.profiles",
"shipping_options",
]
/** /**
* @schema AdminDeleteShippingProfileRes * @schema AdminDeleteShippingProfileRes
@@ -262,6 +262,7 @@ export const defaultStoreCartRelations = [
* - items * - items
* - items.variant * - items.variant
* - items.variant.product * - items.variant.product
* - items.variant.product.profiles
* - items.tax_lines * - items.tax_lines
* - items.adjustments * - items.adjustments
* - gift_cards * - gift_cards
@@ -181,6 +181,7 @@ export type StoreCustomersResetPasswordRes = {
* - items.tax_lines * - items.tax_lines
* - items.variant * - items.variant
* - items.variant.product * - items.variant.product
* - items.variant.product.profiles
* - refunds * - refunds
* - region * - region
* - shipping_address * - shipping_address
@@ -168,6 +168,7 @@ export const allowedStoreOrdersFields = [
* - items.tax_lines * - items.tax_lines
* - items.variant * - items.variant
* - items.variant.product * - items.variant.product
* - items.variant.product.profiles
* - refunds * - refunds
* - region * - region
* - shipping_methods * - shipping_methods
@@ -65,6 +65,7 @@ export const defaultStoreProductsRelations = [
"tags", "tags",
"collection", "collection",
"type", "type",
"profiles",
] ]
export const defaultStoreProductsFields: (keyof Product)[] = [ export const defaultStoreProductsFields: (keyof Product)[] = [
@@ -78,7 +79,6 @@ export const defaultStoreProductsFields: (keyof Product)[] = [
"is_giftcard", "is_giftcard",
"discountable", "discountable",
"thumbnail", "thumbnail",
"profile_id",
"collection_id", "collection_id",
"type_id", "type_id",
"weight", "weight",
@@ -97,7 +97,10 @@ export const defaultStoreProductsFields: (keyof Product)[] = [
export const allowedStoreProductsFields = [ export const allowedStoreProductsFields = [
...defaultStoreProductsFields, ...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.title",
"variants.prices.amount", "variants.prices.amount",
] ]
@@ -21,10 +21,7 @@ describe("GET /store/shipping-options", () => {
it("calls CartService retrieve", () => { it("calls CartService retrieve", () => {
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(1) expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledWith( expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledWith(
IdMap.getId("emptyCart"), IdMap.getId("emptyCart")
{
relations: ["items.variant", "items.variant.product"],
}
) )
}) })
@@ -53,12 +53,8 @@ export default async (req, res) => {
"shippingProfileService" "shippingProfileService"
) )
const cart = await cartService.retrieveWithTotals(cart_id, { const cart = await cartService.retrieveWithTotals(cart_id)
relations: ["items.variant", "items.variant.product"],
})
const options = await shippingProfileService.fetchCartOptions(cart) const options = await shippingProfileService.fetchCartOptions(cart)
const data = await pricingService.setShippingOptionPrices(options, { const data = await pricingService.setShippingOptionPrices(options, {
cart_id, cart_id,
}) })
@@ -139,13 +139,9 @@ export default async (req, res) => {
.retrieve(swapDto.order_id, { .retrieve(swapDto.order_id, {
select: ["refunded_total", "total"], select: ["refunded_total", "total"],
relations: [ relations: [
"items",
"items.variant", "items.variant",
"items.tax_lines", "items.tax_lines",
"swaps", "swaps.additional_items.variant.product.profiles",
"swaps.additional_items",
"swaps.additional_items.variant",
"swaps.additional_items.variant.product",
"swaps.additional_items.tax_lines", "swaps.additional_items.tax_lines",
], ],
}) })
@@ -0,0 +1,49 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class addTableProductShippingProfile1680857773273
implements MigrationInterface
{
name = "addTableProductShippingProfile1680857773273"
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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;
`)
}
}
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class dropProductIdFkSalesChannels1680857773273
implements MigrationInterface
{
name = "dropProductIdFkSalesChannels1680857773273"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
alter table if exists "product_sales_channel" drop constraint if exists "FK_5a4d5e1e60f97633547821ec8d6";
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
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;
`)
}
}
@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class dropVariantIdFkMoneyAmount1680857773273
implements MigrationInterface
{
name = "dropVariantIdFkMoneyAmount1680857773273"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`
alter table if exists "money_amount" drop constraint if exists "FK_17a06d728e4cfbc5bd2ddb70af0";
`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
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;
`)
}
}
+44 -5
View File
@@ -1,5 +1,7 @@
import { import {
AfterLoad,
BeforeInsert, BeforeInsert,
BeforeUpdate,
Column, Column,
Entity, Entity,
Index, Index,
@@ -23,7 +25,7 @@ import { SalesChannel } from "./sales-channel"
import { ShippingProfile } from "./shipping-profile" import { ShippingProfile } from "./shipping-profile"
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
import _ from "lodash" import _ from "lodash"
import { generateEntityId } from "../utils/generate-entity-id" import { generateEntityId } from "../utils"
export enum ProductStatus { export enum ProductStatus {
DRAFT = "draft", DRAFT = "draft",
@@ -92,14 +94,26 @@ export class Product extends SoftDeletableEntity {
}) })
categories: ProductCategory[] categories: ProductCategory[]
@Index()
@Column()
profile_id: string profile_id: string
@ManyToOne(() => ShippingProfile)
@JoinColumn({ name: "profile_id" })
profile: ShippingProfile 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 }) @Column({ type: "int", nullable: true })
weight: number | null weight: number | null
@@ -185,6 +199,25 @@ export class Product extends SoftDeletableEntity {
if (!this.handle) { if (!this.handle) {
this.handle = _.kebabCase(this.title) 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. * description: Available if the relation `profile` is expanded.
* nullable: true * nullable: true
* $ref: "#/components/schemas/ShippingProfile" * $ref: "#/components/schemas/ShippingProfile"
* profiles:
* description: Available if the relation `profiles` is expanded.
* nullable: true
* type: array
* items:
* $ref: "#/components/schemas/ShippingProfile"
* weight: * weight:
* description: The weight of the Product Variant. May be used in shipping rate calculations. * description: The weight of the Product Variant. May be used in shipping rate calculations.
* nullable: true * nullable: true
+22 -5
View File
@@ -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 { Product } from "./product"
import { ShippingOption } from "./shipping-option" import { ShippingOption } from "./shipping-option"
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" import { SoftDeletableEntity } from "../interfaces"
import { generateEntityId } from "../utils/generate-entity-id"
export enum ShippingProfileType { export enum ShippingProfileType {
DEFAULT = "default", DEFAULT = "default",
@@ -20,7 +26,18 @@ export class ShippingProfile extends SoftDeletableEntity {
@DbAwareColumn({ type: "enum", enum: ShippingProfileType }) @DbAwareColumn({ type: "enum", enum: ShippingProfileType })
type: 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[] products: Product[]
@OneToMany(() => ShippingOption, (so) => so.profile) @OneToMany(() => ShippingOption, (so) => so.profile)
@@ -825,25 +825,6 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
return queryBuilder 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<Product[]> {
await this.createQueryBuilder()
.update(Product)
.set({ profile_id: shippingProfileId })
.where({ id: In(productIds) })
.execute()
return await this.findByIds(productIds)
},
}) })
export default ProductRepository export default ProductRepository
@@ -859,7 +859,9 @@ describe("CartService", () => {
gift_cards: true, gift_cards: true,
items: { items: {
variant: { variant: {
product: true, product: {
profiles: true,
},
}, },
}, },
payment_sessions: true, payment_sessions: true,
+21 -15
View File
@@ -46,18 +46,20 @@ describe("StoreService", () => {
it("successfully retrieve store", async () => { it("successfully retrieve store", async () => {
await storeService.retrieve().catch(() => void 0) await storeService.retrieve().catch(() => void 0)
expect(storeRepository.findOne).toHaveBeenCalledTimes(1) expect(storeRepository.find).toHaveBeenCalledTimes(1)
}) })
}) })
describe("update", () => { describe("update", () => {
const storeRepository = MockRepository({ const storeRepository = MockRepository({
findOne: () => find: () =>
Promise.resolve({ Promise.resolve([
{
id: IdMap.getId("store"), id: IdMap.getId("store"),
name: "Medusa", name: "Medusa",
default_currency_code: "usd", default_currency_code: "usd",
}), },
]),
}) })
const currencyRepository = MockRepository({}) const currencyRepository = MockRepository({})
@@ -77,7 +79,7 @@ describe("StoreService", () => {
name: "Medusa Commerce", name: "Medusa Commerce",
}) })
expect(storeRepository.findOne).toHaveBeenCalledTimes(1) expect(storeRepository.find).toHaveBeenCalledTimes(1)
expect(storeRepository.save).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledTimes(1)
expect(storeRepository.save).toHaveBeenCalledWith({ expect(storeRepository.save).toHaveBeenCalledWith({
@@ -94,18 +96,20 @@ describe("StoreService", () => {
}) })
).rejects.toThrow("Currency with code 1cd does not exist") ).rejects.toThrow("Currency with code 1cd does not exist")
expect(storeRepository.findOne).toHaveBeenCalledTimes(1) expect(storeRepository.find).toHaveBeenCalledTimes(1)
}) })
}) })
describe("addCurrency", () => { describe("addCurrency", () => {
const storeRepository = MockRepository({ const storeRepository = MockRepository({
findOne: () => find: () =>
Promise.resolve({ Promise.resolve([
{
id: IdMap.getId("store"), id: IdMap.getId("store"),
name: "Medusa", name: "Medusa",
currencies: [{ code: "dkk" }], currencies: [{ code: "dkk" }],
}), },
]),
}) })
const currencyRepository = MockRepository({ const currencyRepository = MockRepository({
@@ -134,7 +138,7 @@ describe("StoreService", () => {
it("successfully adds currency", async () => { it("successfully adds currency", async () => {
await storeService.addCurrency("sek") await storeService.addCurrency("sek")
expect(storeRepository.findOne).toHaveBeenCalledTimes(1) expect(storeRepository.find).toHaveBeenCalledTimes(1)
expect(storeRepository.save).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledTimes(1)
expect(storeRepository.save).toHaveBeenCalledWith({ expect(storeRepository.save).toHaveBeenCalledWith({
@@ -155,18 +159,20 @@ describe("StoreService", () => {
"Currency already added" "Currency already added"
) )
expect(storeRepository.findOne).toHaveBeenCalledTimes(1) expect(storeRepository.find).toHaveBeenCalledTimes(1)
}) })
}) })
describe("removeCurrency", () => { describe("removeCurrency", () => {
const storeRepository = MockRepository({ const storeRepository = MockRepository({
findOne: () => find: () =>
Promise.resolve({ Promise.resolve([
{
id: IdMap.getId("store"), id: IdMap.getId("store"),
name: "Medusa", name: "Medusa",
currencies: [{ code: "dkk" }], currencies: [{ code: "dkk" }],
}), },
]),
}) })
const currencyRepository = MockRepository({ const currencyRepository = MockRepository({
@@ -195,7 +201,7 @@ describe("StoreService", () => {
it("successfully removes currency", async () => { it("successfully removes currency", async () => {
await storeService.removeCurrency("dkk") await storeService.removeCurrency("dkk")
expect(storeRepository.findOne).toHaveBeenCalledTimes(1) expect(storeRepository.find).toHaveBeenCalledTimes(1)
expect(storeRepository.save).toHaveBeenCalledTimes(1) expect(storeRepository.save).toHaveBeenCalledTimes(1)
expect(storeRepository.save).toHaveBeenCalledWith({ expect(storeRepository.save).toHaveBeenCalledWith({
@@ -280,7 +280,9 @@ describe("SwapService", () => {
}, },
items: { items: {
variant: { variant: {
product: true, product: {
profiles: true,
},
}, },
}, },
swaps: { swaps: {
@@ -11,7 +11,7 @@ const eventBusService = {
describe("UserService", () => { describe("UserService", () => {
describe("retrieve", () => { describe("retrieve", () => {
const userRepository = MockRepository({ const userRepository = MockRepository({
findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), find: () => Promise.resolve([{ id: IdMap.getId("ironman") }]),
}) })
const userService = new UserService({ const userService = new UserService({
manager: MockManager, manager: MockManager,
@@ -25,8 +25,8 @@ describe("UserService", () => {
it("successfully retrieves a user", async () => { it("successfully retrieves a user", async () => {
const result = await userService.retrieve(IdMap.getId("ironman")) const result = await userService.retrieve(IdMap.getId("ironman"))
expect(userRepository.findOne).toHaveBeenCalledTimes(1) expect(userRepository.find).toHaveBeenCalledTimes(1)
expect(userRepository.findOne).toHaveBeenCalledWith({ expect(userRepository.find).toHaveBeenCalledWith({
where: { id: IdMap.getId("ironman") }, where: { id: IdMap.getId("ironman") },
}) })
@@ -78,7 +78,7 @@ describe("UserService", () => {
describe("update", () => { describe("update", () => {
const userRepository = MockRepository({ const userRepository = MockRepository({
findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), find: () => Promise.resolve([{ id: IdMap.getId("ironman") }]),
}) })
const userService = new UserService({ const userService = new UserService({
manager: MockManager, manager: MockManager,
@@ -159,8 +159,8 @@ describe("UserService", () => {
describe("generateResetPasswordToken", () => { describe("generateResetPasswordToken", () => {
const userRepository = MockRepository({ const userRepository = MockRepository({
findOne: () => find: () =>
Promise.resolve({ id: IdMap.getId("ironman"), password_hash: "lol" }), Promise.resolve([{ id: IdMap.getId("ironman"), password_hash: "lol" }]),
}) })
const userService = new UserService({ const userService = new UserService({
+16 -50
View File
@@ -480,12 +480,7 @@ class CartService extends TransactionBaseService {
return await this.atomicPhase_( return await this.atomicPhase_(
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: ["items.variant.product.profiles", "payment_sessions"],
"items",
"items.variant",
"items.variant.product",
"payment_sessions",
],
}) })
const lineItem = cart.items.find((item) => item.id === lineItemId) const lineItem = cart.items.find((item) => item.id === lineItemId)
@@ -518,9 +513,7 @@ class CartService extends TransactionBaseService {
const result = await this.retrieve(cartId, { const result = await this.retrieve(cartId, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"region", "region",
@@ -719,9 +712,7 @@ class CartService extends TransactionBaseService {
cart = await this.retrieve(cart.id, { cart = await this.retrieve(cart.id, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"region", "region",
@@ -899,9 +890,7 @@ class CartService extends TransactionBaseService {
cart = await this.retrieve(cart.id, { cart = await this.retrieve(cart.id, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"region", "region",
@@ -978,9 +967,7 @@ class CartService extends TransactionBaseService {
const updatedCart = await this.retrieve(cartId, { const updatedCart = await this.retrieve(cartId, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"region", "region",
@@ -1054,9 +1041,7 @@ class CartService extends TransactionBaseService {
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
const cartRepo = transactionManager.withRepository(this.cartRepository_) const cartRepo = transactionManager.withRepository(this.cartRepository_)
const relations = [ const relations = [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"shipping_address", "shipping_address",
@@ -1076,7 +1061,7 @@ class CartService extends TransactionBaseService {
) && ) &&
data.sales_channel_id data.sales_channel_id
) { ) {
relations.push("items.variant", "items.variant.product") relations.push("items.variant.product.profiles")
} }
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
@@ -1523,9 +1508,7 @@ class CartService extends TransactionBaseService {
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"region", "region",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
@@ -1622,12 +1605,7 @@ class CartService extends TransactionBaseService {
) )
const cart = await this.retrieveWithTotals(cartId, { const cart = await this.retrieveWithTotals(cartId, {
relations: [ relations: ["payment_sessions", "items.variant.product.profiles"],
"payment_sessions",
"items",
"items.variant",
"items.variant.product",
],
}) })
// If cart total is 0, we don't perform anything payment related // 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, { const cart = await this.retrieveWithTotals(cartId, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"customer", "customer",
"region", "region",
"region.payment_providers", "region.payment_providers",
@@ -1823,9 +1799,7 @@ class CartService extends TransactionBaseService {
cartId, cartId,
{ {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"items.adjustments", "items.adjustments",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
@@ -2107,9 +2081,7 @@ class CartService extends TransactionBaseService {
relations: [ relations: [
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"payment_sessions", "payment_sessions",
], ],
}) })
@@ -2292,7 +2264,7 @@ class CartService extends TransactionBaseService {
cart.items = await lineItemServiceTx.list( cart.items = await lineItemServiceTx.list(
{ id: cart.items.map((i) => i.id) }, { 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) => { async (transactionManager: EntityManager) => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
relations: [ relations: [
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"payment_sessions", "payment_sessions",
@@ -2537,9 +2507,7 @@ class CartService extends TransactionBaseService {
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"gift_cards", "gift_cards",
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"items.adjustments", "items.adjustments",
"region", "region",
"region.tax_rates", "region.tax_rates",
@@ -2809,9 +2777,7 @@ class CartService extends TransactionBaseService {
private getTotalsRelations(config: FindConfig<Cart>): string[] { private getTotalsRelations(config: FindConfig<Cart>): string[] {
const relationSet = new Set(config.relations) const relationSet = new Set(config.relations)
relationSet.add("items") relationSet.add("items.variant.product.profiles")
relationSet.add("items.variant")
relationSet.add("items.variant.product")
relationSet.add("items.tax_lines") relationSet.add("items.tax_lines")
relationSet.add("items.adjustments") relationSet.add("items.adjustments")
relationSet.add("gift_cards") relationSet.add("gift_cards")
+2 -4
View File
@@ -430,7 +430,7 @@ export default class ClaimService extends TransactionBaseService {
id: result.additional_items.map((i) => i.id), 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) => { async (transactionManager: EntityManager) => {
const claim = await this.retrieve(id, { const claim = await this.retrieve(id, {
relations: [ relations: [
"additional_items",
"additional_items.tax_lines", "additional_items.tax_lines",
"additional_items.variant", "additional_items.variant.product.profiles",
"additional_items.variant.product",
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"shipping_methods.tax_lines", "shipping_methods.tax_lines",
+1 -3
View File
@@ -384,9 +384,7 @@ class DraftOrderService extends TransactionBaseService {
relations: [ relations: [
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"payment_sessions", "payment_sessions",
], ],
}) })
+1 -1
View File
@@ -575,7 +575,7 @@ export default class OrderEditService extends TransactionBaseService {
let lineItem = await lineItemServiceTx.create(lineItemData) let lineItem = await lineItemServiceTx.create(lineItemData)
lineItem = await lineItemServiceTx.retrieve(lineItem.id, { lineItem = await lineItemServiceTx.retrieve(lineItem.id, {
relations: ["variant", "variant.product"], relations: ["variant.product.profiles"],
}) })
await this.refreshAdjustments(orderEditId) await this.refreshAdjustments(orderEditId)
+3 -9
View File
@@ -342,11 +342,9 @@ class OrderService extends TransactionBaseService {
const totalsToSelect = select.filter((v) => totalFields.includes(v)) const totalsToSelect = select.filter((v) => totalFields.includes(v))
if (totalsToSelect.length > 0) { if (totalsToSelect.length > 0) {
const relationSet = new Set(relations) const relationSet = new Set(relations)
relationSet.add("items")
relationSet.add("items.tax_lines") relationSet.add("items.tax_lines")
relationSet.add("items.adjustments") relationSet.add("items.adjustments")
relationSet.add("items.variant") relationSet.add("items.variant.product.profiles")
relationSet.add("items.variant.product")
relationSet.add("swaps") relationSet.add("swaps")
relationSet.add("swaps.additional_items") relationSet.add("swaps.additional_items")
relationSet.add("swaps.additional_items.tax_lines") relationSet.add("swaps.additional_items.tax_lines")
@@ -1034,9 +1032,7 @@ class OrderService extends TransactionBaseService {
relations: [ relations: [
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
], ],
}) })
const { shipping_methods } = order const { shipping_methods } = order
@@ -1403,10 +1399,8 @@ class OrderService extends TransactionBaseService {
"billing_address", "billing_address",
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"items",
"items.adjustments", "items.adjustments",
"items.variant", "items.variant.product.profiles",
"items.variant.product",
"payments", "payments",
], ],
}) })
@@ -3,14 +3,14 @@ import {
ICacheService, ICacheService,
IEventBusService, IEventBusService,
IInventoryService, IInventoryService,
IStockLocationService,
InventoryItemDTO, InventoryItemDTO,
InventoryLevelDTO, InventoryLevelDTO,
IStockLocationService,
ReservationItemDTO, ReservationItemDTO,
ReserveQuantityContext, ReserveQuantityContext,
} from "@medusajs/types" } from "@medusajs/types"
import { LineItem, Product, ProductVariant } from "../models" 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 { PricedProduct, PricedVariant } from "../types/pricing"
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item" import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
@@ -303,9 +303,14 @@ class ProductVariantInventoryService extends TransactionBaseService {
// Verify that variant exists // Verify that variant exists
const variants = await this.productVariantService_ const variants = await this.productVariantService_
.withTransaction(this.activeManager_) .withTransaction(this.activeManager_)
.list({ .list(
{
id: data.map((d) => d.variantId), id: data.map((d) => d.variantId),
}) },
{
select: ["id"],
}
)
const foundVariantIds = new Set(variants.map((v) => v.id)) const foundVariantIds = new Set(variants.map((v) => v.id))
const requestedVariantIds = new Set(data.map((v) => v.variantId)) const requestedVariantIds = new Set(data.map((v) => v.variantId))
+26 -5
View File
@@ -1,5 +1,5 @@
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager, In } from "typeorm"
import { ProductVariantService, SearchService } from "." import { ProductVariantService, SearchService } from "."
import { TransactionBaseService } from "../interfaces" import { TransactionBaseService } from "../interfaces"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
@@ -11,6 +11,7 @@ import {
ProductType, ProductType,
ProductVariant, ProductVariant,
SalesChannel, SalesChannel,
ShippingProfile,
} from "../models" } from "../models"
import { ImageRepository } from "../repositories/image" import { ImageRepository } from "../repositories/image"
import { import {
@@ -446,6 +447,10 @@ class ProductService extends TransactionBaseService {
let product = productRepo.create(rest) let product = productRepo.create(rest)
if (rest.profile_id) {
product.profiles = [{ id: rest.profile_id! }] as ShippingProfile[]
}
if (images?.length) { if (images?.length) {
product.images = await imageRepo.upsertImages(images) product.images = await imageRepo.upsertImages(images)
} }
@@ -563,6 +568,10 @@ class ProductService extends TransactionBaseService {
...rest ...rest
} = update } = update
if (rest.profile_id) {
product.profiles = [{ id: rest.profile_id! }] as ShippingProfile[]
}
if (!product.thumbnail && !update.thumbnail && images?.length) { if (!product.thumbnail && !update.thumbnail && images?.length) {
product.thumbnail = images[0] 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 productIds ID or IDs of the products to update
* @param profileId Shipping profile ID to update the shipping options with * @param profileId Shipping profile ID to update the shipping options with
* @returns updated shipping options * @returns updated products
*/ */
async updateShippingProfile( async updateShippingProfile(
productIds: string | string[], productIds: string | string[],
profileId: string profileId: string | null
): Promise<Product[]> { ): Promise<Product[]> {
return await this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager) => {
const productRepo = manager.withRepository(this.productRepository_) const productRepo = manager.withRepository(this.productRepository_)
const ids = isString(productIds) ? [productIds] : productIds 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_ await this.eventBus_
.withTransaction(manager) .withTransaction(manager)
+3 -1
View File
@@ -530,7 +530,9 @@ class ReturnService extends TransactionBaseService {
{ {
id: returnOrder.items.map(({ item_id }) => item_id), 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) => { returnData.items = returnOrder.items.map((item) => {
@@ -290,23 +290,16 @@ class ShippingProfileService extends TransactionBaseService {
this.shippingProfileRepository_ this.shippingProfileRepository_
) )
let profile = await this.retrieve(profileId, { const profile = await this.retrieve(profileId)
relations: [
"products",
"products.profile",
"shipping_options",
"shipping_options.profile",
],
})
const { metadata, products, shipping_options, ...rest } = update const { metadata, products, shipping_options, ...rest } = update
if (products) { if (products) {
profile = await this.addProduct(profile.id, products) await this.addProduct(profile.id, products)
} }
if (shipping_options) { if (shipping_options) {
profile = await this.addShippingOption(profile.id, shipping_options) await this.addShippingOption(profile.id, shipping_options)
} }
if (metadata) { 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<ShippingProfile> {
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 profileId - the profile to add the products to.
* @param productId - the ID of the product or multiple products to add. * @param productId - the ID of the product or multiple products to add.
* @return the result of update * @return the result of update
*/ */
async addProduct( async addProducts(
profileId: string, profileId: string,
productId: string | string[] productId: string | string[]
): Promise<ShippingProfile> { ): Promise<ShippingProfile> {
@@ -364,14 +367,27 @@ class ShippingProfileService extends TransactionBaseService {
profileId profileId
) )
return await this.retrieve(profileId, { return await this.retrieve(profileId)
relations: [
"products",
"products.profile",
"shipping_options",
"shipping_options.profile",
],
}) })
}
/**
* 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<ShippingProfile | void> {
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, { return await this.retrieve(profileId, {
relations: [ relations: ["products.profiles", "shipping_options.profile"],
"products",
"products.profile",
"shipping_options",
"shipping_options.profile",
],
}) })
}) })
} }
+3 -3
View File
@@ -88,13 +88,13 @@ class StoreService extends TransactionBaseService {
}, },
config 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") throw new MedusaError(MedusaError.Types.NOT_FOUND, "Store does not exist")
} }
return store return stores[0]
} }
protected getDefaultCurrency_(code: string): Partial<Currency> { protected getDefaultCurrency_(code: string): Partial<Currency> {
+2 -7
View File
@@ -584,10 +584,7 @@ class SwapService extends TransactionBaseService {
const swap = await this.retrieve(swapId, { const swap = await this.retrieve(swapId, {
relations: [ relations: [
"order", "order.items.variant.product.profiles",
"order.items",
"order.items.variant",
"order.items.variant.product",
"order.swaps", "order.swaps",
"order.swaps.additional_items", "order.swaps.additional_items",
"order.discounts", "order.discounts",
@@ -934,10 +931,8 @@ class SwapService extends TransactionBaseService {
relations: [ relations: [
"payment", "payment",
"shipping_address", "shipping_address",
"additional_items",
"additional_items.tax_lines", "additional_items.tax_lines",
"additional_items.variant", "additional_items.variant.product.profiles",
"additional_items.variant.product",
"shipping_methods", "shipping_methods",
"shipping_methods.shipping_option", "shipping_methods.shipping_option",
"shipping_methods.tax_lines", "shipping_methods.tax_lines",
+3 -3
View File
@@ -85,16 +85,16 @@ class UserService extends TransactionBaseService {
const userRepo = this.activeManager_.withRepository(this.userRepository_) const userRepo = this.activeManager_.withRepository(this.userRepository_)
const query = buildQuery({ id: userId }, config) 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( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
`User with id: ${userId} was not found` `User with id: ${userId} was not found`
) )
} }
return user return users[0]
} }
/** /**
@@ -189,9 +189,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
"discounts", "discounts",
"discounts.rule", "discounts.rule",
"gift_cards", "gift_cards",
"items", "items.variant.product.profiles",
"items.variant",
"items.variant.product",
"items.adjustments", "items.adjustments",
"region", "region",
"region.tax_rates", "region.tax_rates",
@@ -295,7 +293,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
"region", "region",
"payment", "payment",
"payment_sessions", "payment_sessions",
"items.variant.product", "items.variant.product.profiles",
], ],
}) })
@@ -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<DistributedTransaction> {
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
}
@@ -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<ProductTypes.ProductDTO[]> => {
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)
}
@@ -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)[]
}
@@ -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[]
@@ -4,6 +4,7 @@ import {
ProductTypes, ProductTypes,
} from "@medusajs/types" } from "@medusajs/types"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { ProductVariantInventoryService } from "../../services"
export async function attachInventoryItems({ export async function attachInventoryItems({
container, container,
@@ -17,18 +18,13 @@ export async function attachInventoryItems({
inventoryItem: InventoryItemDTO inventoryItem: InventoryItemDTO
}[] }[]
}) { }) {
const productVariantInventoryService = container const productVariantInventoryService: ProductVariantInventoryService =
.resolve("productVariantInventoryService") container.resolve("productVariantInventoryService").withTransaction(manager)
.withTransaction(manager)
return await Promise.all( const inventoryData = data.map(({ variant, inventoryItem }) => ({
data variantId: variant.id,
.filter((d) => d) inventoryItemId: inventoryItem.id,
.map(async ({ variant, inventoryItem }) => { }))
return await productVariantInventoryService.attachInventoryItem(
variant.id, return await productVariantInventoryService.attachInventoryItem(inventoryData)
inventoryItem.id
)
})
)
} }
@@ -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<ProductHandle, SalesChannelId[]>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const salesChannelService: SalesChannelService = container.resolve(
"salesChannelService"
)
const salesChannelServiceTx = salesChannelService.withTransaction(manager)
const salesChannelIdProductIdsMap = new Map<ProductHandle, SalesChannelId[]>()
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
)
}
)
)
}
@@ -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<ProductHandle, ShippingProfileId>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const shippingProfileService: ShippingProfileService = container.resolve(
"shippingProfileService"
)
const shippingProfileServiceTx =
shippingProfileService.withTransaction(manager)
const profileIdProductIdsMap = new Map<ShippingProfileId, ProductHandle[]>()
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)
}
)
)
}
@@ -1,12 +1,16 @@
import { MedusaContainer, ProductTypes } from "@medusajs/types" import { MedusaContainer, ProductTypes } from "@medusajs/types"
export async function removeProducts({ export type CreateProductsData = ProductTypes.CreateProductDTO[]
export async function createProducts({
container, container,
data, data,
}: { }: {
container: MedusaContainer container: MedusaContainer
data: ProductTypes.ProductDTO[] data: CreateProductsData
}): Promise<ProductTypes.ProductDTO[]> { }): Promise<ProductTypes.ProductDTO[]> {
const productModuleService = container.resolve("productModuleService") const productModuleService: ProductTypes.IProductModuleService =
return await productModuleService.softDelete(data.map((p) => p.id)) container.resolve("productModuleService")
return await productModuleService.create(data)
} }
@@ -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
)
})
)
}
@@ -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<ProductHandle, SalesChannelId[]>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const salesChannelService: SalesChannelService = container.resolve(
"salesChannelService"
)
const salesChannelServiceTx = salesChannelService.withTransaction(manager)
const salesChannelIdProductIdsMap = new Map<ProductHandle, SalesChannelId[]>()
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
)
}
)
)
}
@@ -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<ProductHandle, ShippingProfileId>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const shippingProfileService: ShippingProfileService = container.resolve(
"shippingProfileService"
)
const shippingProfileServiceTx =
shippingProfileService.withTransaction(manager)
const profileIdProductIdsMap = new Map<ShippingProfileId, ProductHandle[]>()
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
)
}
)
)
}
@@ -3,3 +3,10 @@ export * from "./remove-products"
export * from "./create-inventory-items" export * from "./create-inventory-items"
export * from "./remove-inventory-items" export * from "./remove-inventory-items"
export * from "./attach-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"
@@ -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<ProductHandle, ShippingProfileId>
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
productsHandleVariantsIndexPricesMap: Map<
ProductHandle,
VariantIndexAndPrices[]
>
}
export async function prepareCreateProductsData({
container,
manager,
data,
}: {
container: MedusaContainer
manager: EntityManager
data: CreateProductsInputData
}): Promise<CreateProductsPreparedData> {
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,
}
}
@@ -15,12 +15,8 @@ export async function removeInventoryItems({
const inventoryService = container.resolve("inventoryService") const inventoryService = container.resolve("inventoryService")
const context = { transactionManager: manager } const context = { transactionManager: manager }
return await Promise.all(
data.map(async ({ inventoryItem }) => {
return await inventoryService!.deleteInventoryItem( return await inventoryService!.deleteInventoryItem(
inventoryItem.id, data.map(({ inventoryItem }) => inventoryItem.id),
context context
) )
})
)
} }
@@ -1,12 +1,13 @@
import { MedusaContainer, ProductTypes } from "@medusajs/types" import { MedusaContainer, ProductTypes } from "@medusajs/types"
export async function createProducts({ export async function removeProducts({
container, container,
data, data,
}: { }: {
container: MedusaContainer container: MedusaContainer
data: ProductTypes.CreateProductDTO[] data: ProductTypes.ProductDTO[]
}) { }) {
const productModuleService = container.resolve("productModuleService") const productModuleService: ProductTypes.IProductModuleService =
return await productModuleService.create(data) container.resolve("productModuleService")
return await productModuleService.softDelete(data.map((p) => p.id))
} }
@@ -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<string, ProductTypes.ProductDTO>(
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)
}
@@ -845,8 +845,8 @@
"nullable": false, "nullable": false,
"mappedType": "text" "mappedType": "text"
}, },
"product_image_id": { "image_id": {
"name": "product_image_id", "name": "image_id",
"type": "text", "type": "text",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
@@ -860,7 +860,10 @@
"indexes": [ "indexes": [
{ {
"keyName": "product_images_pkey", "keyName": "product_images_pkey",
"columnNames": ["product_id", "product_image_id"], "columnNames": [
"product_id",
"image_id"
],
"composite": true, "composite": true,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -877,9 +880,11 @@
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"product_images_product_image_id_foreign": { "product_images_image_id_foreign": {
"constraintName": "product_images_product_image_id_foreign", "constraintName": "product_images_image_id_foreign",
"columnNames": ["product_image_id"], "columnNames": [
"image_id"
],
"localTableName": "public.product_images", "localTableName": "public.product_images",
"referencedColumnNames": ["id"], "referencedColumnNames": ["id"],
"referencedTableName": "public.image", "referencedTableName": "public.image",
@@ -1119,6 +1124,15 @@
"default": "0", "default": "0",
"mappedType": "decimal" "mappedType": "decimal"
}, },
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "timestamptz", "type": "timestamptz",
@@ -1148,15 +1162,6 @@
"nullable": true, "nullable": true,
"length": 6, "length": 6,
"mappedType": "datetime" "mappedType": "datetime"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
} }
}, },
"name": "product_variant", "name": "product_variant",
@@ -1,6 +1,6 @@
import { Migration } from "@mikro-orm/migrations" import { Migration } from "@mikro-orm/migrations"
export class Migration20230710091208 extends Migration { export class Migration20230719100648 extends Migration {
async up(): Promise<void> { async up(): Promise<void> {
this.addSql( 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"));' '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( 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( this.addSql(
@@ -78,7 +78,7 @@ export class Migration20230710091208 extends Migration {
) )
this.addSql( 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( this.addSql(
'create index "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");' '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;' '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( 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( this.addSql(
@@ -32,7 +32,7 @@ class ProductOptionValue {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
value: string value: string
@Property({ persist: false }) @Property({ columnType: "text", nullable: true })
option_id!: string option_id!: string
@ManyToOne(() => ProductOption, { @ManyToOne(() => ProductOption, {
@@ -41,7 +41,7 @@ class ProductOptionValue {
}) })
option: ProductOption option: ProductOption
@Property({ persist: false }) @Property({ columnType: "text", nullable: true })
variant_id!: string variant_id!: string
@ManyToOne(() => ProductVariant, { @ManyToOne(() => ProductVariant, {
@@ -29,7 +29,7 @@ class ProductOption {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
title: string title: string
@Property({ persist: false }) @Property({ columnType: "text", nullable: true })
product_id!: string product_id!: string
@ManyToOne(() => Product, { @ManyToOne(() => Product, {
@@ -105,7 +105,7 @@ class ProductVariant {
@Property({ columnType: "numeric", nullable: true, default: 0 }) @Property({ columnType: "numeric", nullable: true, default: 0 })
variant_rank?: number | null variant_rank?: number | null
@Property({ persist: false }) @Property({ columnType: "text", nullable: true })
product_id!: string product_id!: string
@Property({ onCreate: () => new Date(), columnType: "timestamptz" }) @Property({ onCreate: () => new Date(), columnType: "timestamptz" })
+4 -2
View File
@@ -100,7 +100,7 @@ class Product {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
material?: string | null material?: string | null
@Property({ persist: false }) @Property({ columnType: "text", nullable: true })
collection_id!: string collection_id!: string
@ManyToOne(() => ProductCollection, { @ManyToOne(() => ProductCollection, {
@@ -109,7 +109,7 @@ class Product {
}) })
collection!: ProductCollection collection!: ProductCollection
@Property({ persist: false }) @Property({ columnType: "text", nullable: true })
type_id!: string type_id!: string
@ManyToOne(() => ProductType, { @ManyToOne(() => ProductType, {
@@ -132,6 +132,8 @@ class Product {
pivotTable: "product_images", pivotTable: "product_images",
index: "IDX_product_image_id", index: "IDX_product_image_id",
cascade: ["soft-remove"] as any, cascade: ["soft-remove"] as any,
joinColumn: "product_id",
inverseJoinColumn: "image_id",
}) })
images = new Collection<ProductImage>(this) images = new Collection<ProductImage>(this)
@@ -1,5 +1,3 @@
import * as ProductModels from "@models"
import { ModuleExports } from "@medusajs/types" import { ModuleExports } from "@medusajs/types"
import { ProductModuleService } from "@services" import { ProductModuleService } from "@services"
import loadConnection from "./loaders/connection" import loadConnection from "./loaders/connection"
@@ -7,10 +5,8 @@ import loadContainer from "./loaders/container"
const service = ProductModuleService const service = ProductModuleService
const loaders = [loadContainer, loadConnection] as any const loaders = [loadContainer, loadConnection] as any
const models = Object.values(ProductModels)
export const moduleDefinition: ModuleExports = { export const moduleDefinition: ModuleExports = {
service, service,
loaders, loaders,
models,
} }
+6 -18
View File
@@ -6,7 +6,6 @@ import {
MedusaContext, MedusaContext,
} from "@medusajs/utils" } from "@medusajs/utils"
import { serialize } from "@mikro-orm/core" 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? // 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) return await task(transaction)
} }
const forkedManager = this.manager_.fork()
const options = {} const options = {}
if (transaction) {
Object.assign(options, { ctx: transaction })
}
if (isolationLevel) { if (isolationLevel) {
Object.assign(options, { isolationLevel }) Object.assign(options, { isolationLevel })
} }
if (transaction) { return await (this.manager_ as SqlEntityManager).transactional(task, options)
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
}
} }
const updateDeletedAtRecursively = async <T extends object = any>( const updateDeletedAtRecursively = async <T extends object = any>(
@@ -126,7 +126,6 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
@MedusaContext() @MedusaContext()
{ transactionManager: manager }: Context = {} { transactionManager: manager }: Context = {}
): Promise<Product[]> { ): Promise<Product[]> {
console.log((this as any).prototype)
const products = data.map((product) => { const products = data.map((product) => {
return (manager as SqlEntityManager).create(Product, product) return (manager as SqlEntityManager).create(Product, product)
}) })
@@ -349,12 +349,12 @@ export default class ProductModuleService<
} }
if (isDefined(productData.type)) { if (isDefined(productData.type)) {
productData.type_id = ( productData.type = (
await this.productTypeService_.upsert( await this.productTypeService_.upsert(
[productData.type as ProductTypes.CreateProductTypeDTO], [productData.type as ProductTypes.CreateProductTypeDTO],
sharedContext sharedContext
) )
)?.[0]!.id )?.[0]!
} }
return productData as CreateProductOnlyDTO return productData as CreateProductOnlyDTO
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more