Skip to content

Commit 2872ea5

Browse files
committed
feat(product-category): add external_id to product-category
It is available on both /admin and /store, and can be used as filter. fix: add missing snapshot test: fix race condition in test chore: clean up test-cases. Remove dead code chore: cleanup tests
1 parent d07f707 commit 2872ea5

File tree

15 files changed

+335
-1
lines changed

15 files changed

+335
-1
lines changed

.changeset/yummy-teams-matter.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@medusajs/product": patch
3+
"integration-tests-http": patch
4+
"@medusajs/types": patch
5+
"@medusajs/medusa": patch
6+
---
7+
8+
add external_id to product-category

integration-tests/http/__tests__/product-category/admin/product-category.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ medusaIntegrationTestRunner({
5353
name: "category-0",
5454
parent_category_id: productCategoryParent.id,
5555
rank: 0,
56+
external_id: "ext-id-0",
5657
description: "category-0",
5758
},
5859
adminHeaders
@@ -110,6 +111,7 @@ medusaIntegrationTestRunner({
110111
id: productCategory.id,
111112
name: productCategory.name,
112113
handle: productCategory.handle,
114+
external_id: productCategory.external_id,
113115
parent_category_id: productCategoryParent.id,
114116
category_children: [
115117
expect.objectContaining({
@@ -226,6 +228,7 @@ medusaIntegrationTestRunner({
226228
"/admin/product-categories",
227229
{
228230
name: "sweater",
231+
external_id: "ext-id-sweater",
229232
parent_category_id: productCategoryParent.id,
230233
is_internal: true,
231234
},
@@ -365,6 +368,8 @@ medusaIntegrationTestRunner({
365368
expect(response.data.product_categories).toHaveLength(7) // created in beforeEach
366369
})
367370

371+
372+
368373
it("gets list of product category with immediate children and parents", async () => {
369374
// BREAKING: To get the children tree, the query param include_descendants_tree must be used
370375
const path = `/admin/product-categories?limit=7`
@@ -638,6 +643,18 @@ medusaIntegrationTestRunner({
638643
)
639644
})
640645

646+
it('filters based on external id', async () => {
647+
648+
const response = await api.get(
649+
`/admin/product-categories?external_id=${productCategory.external_id}`,
650+
adminHeaders
651+
)
652+
653+
expect(response.status).toEqual(200)
654+
expect(response.data.product_categories).toHaveLength(1)
655+
expect(response.data.product_categories[0].id).toEqual(productCategory.id)
656+
});
657+
641658
it("filters based on parent category", async () => {
642659
const response = await api.get(
643660
`/admin/product-categories?parent_category_id=${productCategoryParent.id}&limit=7`,
@@ -839,6 +856,46 @@ medusaIntegrationTestRunner({
839856
)
840857
})
841858

859+
it("successfully creates a product category with an external id", async () => {
860+
const payload = {
861+
name: "test",
862+
handle: "test",
863+
is_internal: true,
864+
parent_category_id: productCategory.id,
865+
description: "test",
866+
external_id: "ext-id-0",
867+
}
868+
869+
const response = await api.post(
870+
`/admin/product-categories`,
871+
payload,
872+
adminHeaders
873+
)
874+
875+
expect(response.status).toEqual(200)
876+
expect(response.data).toEqual(
877+
expect.objectContaining({
878+
product_category: expect.objectContaining({
879+
name: payload.name,
880+
description: payload.description,
881+
handle: payload.handle,
882+
is_internal: payload.is_internal,
883+
is_active: false,
884+
external_id: payload.external_id,
885+
created_at: expect.any(String),
886+
updated_at: expect.any(String),
887+
parent_category: expect.objectContaining({
888+
id: productCategory.id,
889+
}),
890+
category_children: [],
891+
rank: 0,
892+
}),
893+
})
894+
)
895+
})
896+
897+
898+
842899
it("successfully creates a product category with a rank", async () => {
843900
const payload = {
844901
name: "test",
@@ -1202,6 +1259,7 @@ medusaIntegrationTestRunner({
12021259
handle: "test",
12031260
is_internal: true,
12041261
is_active: true,
1262+
external_id: "ext-id-2",
12051263
parent_category_id: productCategory.id,
12061264
},
12071265
adminHeaders
@@ -1220,6 +1278,7 @@ medusaIntegrationTestRunner({
12201278
parent_category_id: productCategory.id,
12211279
category_children: [],
12221280
rank: 1,
1281+
external_id: "ext-id-2",
12231282
}),
12241283
})
12251284
)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
2+
import { IStoreModuleService } from "@medusajs/types"
3+
import {
4+
Modules
5+
} from "@medusajs/utils"
6+
import qs from "qs"
7+
import {
8+
adminHeaders,
9+
createAdminUser,
10+
generatePublishableKey,
11+
generateStoreHeaders,
12+
} from "../../../../helpers/create-admin-user"
13+
import { createAuthenticatedCustomer } from "../../../../modules/helpers/create-authenticated-customer"
14+
15+
jest.setTimeout(30000)
16+
17+
medusaIntegrationTestRunner({
18+
testSuite: ({ dbConnection, api, getContainer }) => {
19+
let store
20+
let appContainer
21+
let productCategory1
22+
let productCategory2
23+
let storeHeaders
24+
let publishableKey
25+
let storeHeadersWithCustomer
26+
let customer
27+
28+
const createCategory = async (data, productIds) => {
29+
const response = await api.post(
30+
"/admin/product-categories",
31+
data,
32+
adminHeaders
33+
)
34+
35+
await api.post(
36+
`/admin/product-categories/${response.data.product_category.id}/products`,
37+
{ add: productIds },
38+
adminHeaders
39+
)
40+
41+
const response2 = await api.get(
42+
`/admin/product-categories/${response.data.product_category.id}?fields=*products`,
43+
adminHeaders
44+
)
45+
46+
return response2.data.product_category
47+
}
48+
49+
50+
beforeEach(async () => {
51+
appContainer = getContainer()
52+
publishableKey = await generatePublishableKey(appContainer)
53+
storeHeaders = generateStoreHeaders({ publishableKey })
54+
await createAdminUser(dbConnection, adminHeaders, appContainer)
55+
const result = await createAuthenticatedCustomer(api, storeHeaders, {
56+
first_name: "tony",
57+
last_name: "stark",
58+
email: "tony@stark-industries.com",
59+
})
60+
61+
customer = result.customer
62+
storeHeadersWithCustomer = {
63+
headers: {
64+
...storeHeaders.headers,
65+
authorization: `Bearer ${result.jwt}`,
66+
},
67+
}
68+
69+
const storeModule: IStoreModuleService = appContainer.resolve(
70+
Modules.STORE
71+
)
72+
// A default store is created when the app is started, so we want to delete that one and create one specifically for our tests.
73+
const defaultId = (await api.get("/admin/stores", adminHeaders)).data
74+
.stores?.[0]?.id
75+
if (defaultId) {
76+
await storeModule.deleteStores(defaultId)
77+
}
78+
79+
store = await storeModule.createStores({
80+
name: "New store",
81+
supported_currencies: [
82+
{ currency_code: "usd", is_default: true },
83+
{ currency_code: "dkk" },
84+
],
85+
})
86+
87+
})
88+
89+
describe("Get product-categories based on publishable key", () => {
90+
91+
beforeEach(async () => {
92+
93+
productCategory1 = await createCategory(
94+
{ name: "test", is_internal: false, is_active: true, external_id: "ext-id-1" },
95+
[]
96+
)
97+
98+
99+
productCategory2 = await createCategory(
100+
{ name: "test2", is_internal: false, is_active: true, handle: 'test-2', external_id: "ext-id-2" },
101+
[]
102+
)
103+
})
104+
105+
it("returns the external_id on id lookups", async () => {
106+
const response = await api.get(
107+
`/store/product-categories/${productCategory1.id}`,
108+
storeHeaders
109+
)
110+
111+
expect(response.status).toBe(200)
112+
expect(response.data.product_category).toEqual(
113+
expect.objectContaining({
114+
id: productCategory1.id,
115+
external_id: "ext-id-1",
116+
})
117+
)
118+
})
119+
120+
it("it filters on the external_id field", async () => {
121+
122+
123+
124+
const searchParam = qs.stringify({
125+
external_id: "ext-id-2",
126+
})
127+
128+
const response = await api.get(
129+
`/store/product-categories?${searchParam}`,
130+
storeHeaders
131+
)
132+
133+
expect(response.status).toBe(200)
134+
expect(response.data.product_categories).toEqual(
135+
expect.arrayContaining([
136+
expect.objectContaining({
137+
id: productCategory2.id,
138+
external_id: "ext-id-2",
139+
}),
140+
])
141+
)
142+
})
143+
})
144+
},
145+
})

packages/core/types/src/http/product-category/admin/payloads.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export interface AdminCreateProductCategory {
3030
* The category's ranking among its sibling categories.
3131
*/
3232
rank?: number
33+
/**
34+
* An external identifier for the product category.
35+
*/
36+
external_id?: string | null
3337
/**
3438
* Key-value pairs of custom data.
3539
*/
@@ -68,6 +72,10 @@ export interface AdminUpdateProductCategory {
6872
* The category's ranking among its sibling categories.
6973
*/
7074
rank?: number
75+
/**
76+
* An external identifier for the product category.
77+
*/
78+
external_id?: string | null
7179
/**
7280
* Key-value pairs of custom data.
7381
*/

packages/core/types/src/http/product-category/common.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export interface BaseProductCategory {
3232
* The category's ranking among sibling categories.
3333
*/
3434
rank: number | null
35+
/**
36+
* An external identifier for the product category.
37+
*/
38+
external_id: string | null
3539
/**
3640
* The ID of the category's parent.
3741
*/
@@ -93,6 +97,10 @@ export interface BaseProductCategoryListParams
9397
* Filter by the category's handle(s).
9498
*/
9599
handle?: string | string[]
100+
/**
101+
* Filter by the category's external ID(s).
102+
*/
103+
external_id?: string | string[] | null
96104
/**
97105
* Filter by whether the category is active.
98106
*/

packages/core/types/src/product/common.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ export interface ProductCategoryDTO {
310310
* The ranking of the product category among sibling categories.
311311
*/
312312
rank: number
313+
/**
314+
* An external identifier for the product category, such as an ID from a third-party system.
315+
*/
316+
external_id: string | null
313317
/**
314318
* The ranking of the product category among sibling categories.
315319
*/
@@ -384,6 +388,10 @@ export interface CreateProductCategoryDTO {
384388
* The ID of the parent product category, if it has any.
385389
*/
386390
parent_category_id?: string | null
391+
/**
392+
* An external identifier for the product category, such as an ID from a third-party system.
393+
*/
394+
external_id?: string | null
387395
/**
388396
* Holds custom data in key-value pairs.
389397
*/
@@ -431,6 +439,10 @@ export interface UpdateProductCategoryDTO {
431439
* The ID of the parent product category, if it has any.
432440
*/
433441
parent_category_id?: string | null
442+
/**
443+
* An external identifier for the product category, such as an ID from a third-party system.
444+
*/
445+
external_id?: string | null
434446
/**
435447
* Holds custom data in key-value pairs.
436448
*/
@@ -989,6 +1001,10 @@ export interface FilterableProductCategoryProps
9891001
* Filter product categories by whether they're internal.
9901002
*/
9911003
is_internal?: boolean
1004+
/**
1005+
* Filter product categories by external ID.
1006+
*/
1007+
external_id?: string | string[] | null
9921008
/**
9931009
* Whether to include children of retrieved product categories.
9941010
*/

packages/medusa/src/api/admin/product-categories/query-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const defaults = [
1010
"is_active",
1111
"is_internal",
1212
"rank",
13+
"external_id",
1314
"parent_category_id",
1415
"created_at",
1516
"updated_at",
@@ -26,6 +27,7 @@ export const allowed = [
2627
"is_active",
2728
"is_internal",
2829
"rank",
30+
"external_id",
2931
"parent_category_id",
3032
"created_at",
3133
"updated_at",

packages/medusa/src/api/admin/product-categories/validators.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const AdminProductCategoriesParamsFields = z.object({
2626
description: z.union([z.string(), z.array(z.string())]).optional(),
2727
handle: z.union([z.string(), z.array(z.string())]).optional(),
2828
parent_category_id: z.union([z.string(), z.array(z.string())]).optional(),
29+
external_id: z.union([z.string(), z.array(z.string()), z.null()]).optional(),
2930
include_ancestors_tree: booleanString().optional(),
3031
include_descendants_tree: booleanString().optional(),
3132
is_internal: booleanString().optional(),
@@ -53,6 +54,7 @@ export const CreateProductCategory = z
5354
is_internal: z.boolean().optional(),
5455
is_active: z.boolean().optional(),
5556
parent_category_id: z.string().nullish(),
57+
external_id: z.string().nullish(),
5658
metadata: z.record(z.unknown()).nullish(),
5759
rank: z.number().nonnegative().optional(),
5860
})
@@ -74,6 +76,7 @@ export const UpdateProductCategory = z
7476
is_internal: z.boolean().optional(),
7577
is_active: z.boolean().optional(),
7678
parent_category_id: z.string().nullish(),
79+
external_id: z.string().nullish(),
7780
metadata: z.record(z.unknown()).nullish(),
7881
rank: z.number().nonnegative().optional(),
7982
})

0 commit comments

Comments
 (0)