fix(medusa): fetching a product without a category with categories filed passed (#13020)
**What** - adding `categories` to `fields` param would return 404 for an existing product if the product was not associated with a category --- CLOSES CMRC-1046
This commit is contained in:
1 parent
e413cfefc2
commit
71818e43cb
6 files changed
+216
-10
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): fetching a product without a category with categories filed passed
|
||||||
@@ -787,8 +787,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// TODO: This doesn't work currently, but worked in v1
|
// TODO: This doesn't work currently, but worked in v1
|
||||||
it.skip("returns a list of ordered products by variants title DESC", async () => {
|
it.skip("returns a list of ordered products by variants title DESC", async () => {})
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by variant title ASC", async () => {
|
it("returns a list of ordered products by variant title ASC", async () => {
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
@@ -1840,6 +1839,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("GET /store/products/:id", () => {
|
describe("GET /store/products/:id", () => {
|
||||||
|
let defaultSalesChannel
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
;[product, [variant]] = await createProducts({
|
;[product, [variant]] = await createProducts({
|
||||||
title: "test product 1",
|
title: "test product 1",
|
||||||
@@ -1868,7 +1868,7 @@ medusaIntegrationTestRunner({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultSalesChannel = await createSalesChannel(
|
defaultSalesChannel = await createSalesChannel(
|
||||||
{ name: "default sales channel" },
|
{ name: "default sales channel" },
|
||||||
[product.id]
|
[product.id]
|
||||||
)
|
)
|
||||||
@@ -1928,6 +1928,184 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should retrieve product withhout category if the categories field is passed", async () => {
|
||||||
|
const [product] = await createProducts({
|
||||||
|
title: "test category prod",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "test category variant",
|
||||||
|
options: { size: "large" },
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${defaultSalesChannel.id}/products`,
|
||||||
|
{ add: [product.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products/${product.id}?fields=*categories`,
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
categories: [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should retrieve product with category", async () => {
|
||||||
|
const [product] = await createProducts({
|
||||||
|
title: "test category prod",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "test category variant",
|
||||||
|
options: { size: "large" },
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const category = await createCategory(
|
||||||
|
{ name: "test", is_internal: false, is_active: true },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${defaultSalesChannel.id}/products`,
|
||||||
|
{ add: [product.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products/${product.id}?fields=*categories`,
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
categories: [expect.objectContaining({ id: category.id })],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return product without internal category", async () => {
|
||||||
|
const [product] = await createProducts({
|
||||||
|
title: "test category prod",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "test category variant",
|
||||||
|
options: { size: "large" },
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const category = await createCategory(
|
||||||
|
{ name: "test", is_internal: true, is_active: true },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${defaultSalesChannel.id}/products`,
|
||||||
|
{ add: [product.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products/${product.id}?fields=*categories`,
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
categories: [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return product without internal category (multicategory example)", async () => {
|
||||||
|
const [product] = await createProducts({
|
||||||
|
title: "test category prod",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "test category variant",
|
||||||
|
options: { size: "large" },
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const categoryInternal = await createCategory(
|
||||||
|
{ name: "test", is_internal: true, is_active: true },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const categoryPublic = await createCategory(
|
||||||
|
{ name: "test_public", is_internal: false, is_active: true },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${defaultSalesChannel.id}/products`,
|
||||||
|
{ add: [product.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products/${product.id}?fields=*categories`,
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product.categories.length).toEqual(1)
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
categories: [expect.objectContaining({ id: categoryPublic.id })],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: There are 2 problems that need to be solved to enable this test
|
// TODO: There are 2 problems that need to be solved to enable this test
|
||||||
// 1. When adding product to another category, the product is being removed from earlier assigned categories
|
// 1. When adding product to another category, the product is being removed from earlier assigned categories
|
||||||
// 2. MikroORM seems to be doing a join strategy to load relationships, we need to send a separate query to fetch relationships
|
// 2. MikroORM seems to be doing a join strategy to load relationships, we need to send a separate query to fetch relationships
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { isPresent, MedusaError } from "@medusajs/framework/utils"
|
|||||||
import { MedusaResponse } from "@medusajs/framework/http"
|
import { MedusaResponse } from "@medusajs/framework/http"
|
||||||
import { wrapVariantsWithInventoryQuantityForSalesChannel } from "../../../utils/middlewares"
|
import { wrapVariantsWithInventoryQuantityForSalesChannel } from "../../../utils/middlewares"
|
||||||
import {
|
import {
|
||||||
|
filterOutInternalProductCategories,
|
||||||
refetchProduct,
|
refetchProduct,
|
||||||
RequestWithContext,
|
RequestWithContext,
|
||||||
wrapProductsWithTaxPrices,
|
wrapProductsWithTaxPrices,
|
||||||
@@ -33,6 +34,14 @@ export const GET = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const includesCategoriesField = req.queryConfig.fields.some((field) =>
|
||||||
|
field.startsWith("categories")
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!req.queryConfig.fields.includes("categories.is_internal")) {
|
||||||
|
req.queryConfig.fields.push("categories.is_internal")
|
||||||
|
}
|
||||||
|
|
||||||
const product = await refetchProduct(
|
const product = await refetchProduct(
|
||||||
filters,
|
filters,
|
||||||
req.scope,
|
req.scope,
|
||||||
@@ -53,6 +62,10 @@ export const GET = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includesCategoriesField) {
|
||||||
|
filterOutInternalProductCategories([product])
|
||||||
|
}
|
||||||
|
|
||||||
await wrapProductsWithTaxPrices(req, [product])
|
await wrapProductsWithTaxPrices(req, [product])
|
||||||
res.json({ product })
|
res.json({ product })
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,22 @@ export const refetchProduct = async (
|
|||||||
return await refetchEntity("product", idOrFilter, scope, fields)
|
return await refetchEntity("product", idOrFilter, scope, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const filterOutInternalProductCategories = (
|
||||||
|
products: HttpTypes.StoreProduct[]
|
||||||
|
) => {
|
||||||
|
return products.forEach((product: HttpTypes.StoreProduct) => {
|
||||||
|
if (!product.categories) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
product.categories = product.categories.filter(
|
||||||
|
(category) =>
|
||||||
|
!(category as HttpTypes.StoreProductCategory & { is_internal: boolean })
|
||||||
|
.is_internal
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const wrapProductsWithTaxPrices = async <T>(
|
export const wrapProductsWithTaxPrices = async <T>(
|
||||||
req: RequestWithContext<T>,
|
req: RequestWithContext<T>,
|
||||||
products: HttpTypes.StoreProduct[]
|
products: HttpTypes.StoreProduct[]
|
||||||
|
|||||||
@@ -93,13 +93,6 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
}),
|
}),
|
||||||
applyDefaultFilters({
|
applyDefaultFilters({
|
||||||
status: ProductStatus.PUBLISHED,
|
status: ProductStatus.PUBLISHED,
|
||||||
categories: (_filters, fields: string[]) => {
|
|
||||||
if (!fields.some((field) => field.startsWith("categories"))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return { is_internal: false, is_active: true }
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
normalizeDataForContext(),
|
normalizeDataForContext(),
|
||||||
setPricingContext(),
|
setPricingContext(),
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export default class ProductModuleService
|
|||||||
this.getProductFindConfig_(config),
|
this.getProductFindConfig_(config),
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
const serializedProducts = await this.baseRepository_.serialize<
|
const serializedProducts = await this.baseRepository_.serialize<
|
||||||
ProductTypes.ProductDTO[]
|
ProductTypes.ProductDTO[]
|
||||||
>(products)
|
>(products)
|
||||||
|
|||||||
Reference in new issue
Block a user