Skip to content

Commit 923a869

Browse files
chore(api,types): allow filtering on variant sku/barcode/ean/upc on both /store and /admin apis (#14826)
from both /admin and /store endpoints. Covers both /admin/products and /admin/products/:id/variants ## Summary **What** — What changes are introduced in this PR? add ability to filter products by variant `sku`/`ean`/`upc`/`barcode` in addition to `option` **Why** — Why are these changes relevant or necessary? for /store/products These are real-world identities that a storefront might encounter from barcodes, qr-codes or other entities. for /admin/products it already had an option to filter by 'ean'/'upc' and 'barcode' but in larger setups, the SKU would be dictated by the ERP or Inventory system, so it makes it easier to react to environment events/webhooks to find the product/variant to deal with. for /admin/product/:id/variant it can now filter the skus by `sku=12356` note: for /store, the `variants[sku]` already works, but was not reflected into the type system, so it didn't work/was suggested on the js-sdk. **How** — How have these changes been implemented? Updated validators and types **Testing** — How have these changes been tested, or how can the reviewer test the feature? Unittests provided for both /store and /admin --- ## Examples Provide examples or code snippets that demonstrate how this feature works, or how it can be used in practice. This helps with documentation and ensures maintainers can quickly understand and verify the change. /store/products ```ts const productResponse = await sdk.store.products.list({ variants: { sku: 'SKU1234567' } }); ``` /admin/products ```ts const productResponse = await sdk.admin.products.list({ variants: { sku: 'SKU1234567' } }); ``` /admin/products/:id/variants ``` const productResponse = await sdk.admin.productVariants.list({ sku: 'SKU1234567' }); ``` --- ## Checklist Please ensure the following before requesting a review: - [X] I have added a **changeset** for this PR - Every non-breaking change should be marked as a **patch** - To add a changeset, run `yarn changeset` and follow the prompts - [X] The changes are covered by relevant **tests** - [X] I have verified the code works as intended locally - [X] I have linked the related issue(s) if applicable --- ## Additional Context Add any additional context, related issues, or references that might help the reviewer understand this PR. Co-authored-by: Nicolas Gorga <62995075+NicolasGorga@users.noreply.github.com>
1 parent a99483b commit 923a869

File tree

9 files changed

+220
-2
lines changed

9 files changed

+220
-2
lines changed

.changeset/rich-nights-stand.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"integration-tests-http": patch
3+
"@medusajs/types": patch
4+
"@medusajs/medusa": patch
5+
---
6+
7+
chore(medusa,types): allow filtering on variant sku/barcode/ean/upc on both /store and /admin apis

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,71 @@ medusaIntegrationTestRunner({
984984

985985

986986

987+
it("returns a list of products filtered by variants[sku]", async () => {
988+
const productWithSku = await api.post(
989+
"/admin/products",
990+
getProductFixture({
991+
title: "Product with SKU",
992+
shipping_profile_id: shippingProfile.id,
993+
variants: [
994+
{
995+
title: "Test variant",
996+
sku: "SKU1234567890123",
997+
prices: [{ currency_code: "usd", amount: 100 }],
998+
options: {
999+
size: "large",
1000+
color: "green",
1001+
},
1002+
},
1003+
],
1004+
}),
1005+
adminHeaders
1006+
)
1007+
1008+
await api.post(
1009+
"/admin/products",
1010+
getProductFixture({
1011+
title: "Product with different SKU",
1012+
shipping_profile_id: shippingProfile.id,
1013+
variants: [
1014+
{
1015+
title: "Test variant 2",
1016+
sku: "SKU9876543210987",
1017+
prices: [{ currency_code: "usd", amount: 150 }],
1018+
options: {
1019+
size: "large",
1020+
color: "green",
1021+
},
1022+
},
1023+
],
1024+
}),
1025+
adminHeaders
1026+
)
1027+
1028+
const response = await api
1029+
.get("/admin/products?variants[sku]=SKU1234567890123", adminHeaders)
1030+
.catch((err) => {
1031+
console.log(err)
1032+
})
1033+
1034+
expect(response.status).toEqual(200)
1035+
expect(response.data.products).toHaveLength(1)
1036+
expect(response.data.products).toEqual(
1037+
expect.arrayContaining([
1038+
expect.objectContaining({
1039+
id: productWithSku.data.product.id,
1040+
title: "Product with SKU",
1041+
variants: expect.arrayContaining([
1042+
expect.objectContaining({
1043+
sku: "SKU1234567890123",
1044+
}),
1045+
]),
1046+
}),
1047+
])
1048+
)
1049+
})
1050+
1051+
9871052
it("returns a list of products filtered by variants[ean]", async () => {
9881053
const productWithEan = await api.post(
9891054
"/admin/products",
@@ -1415,6 +1480,47 @@ medusaIntegrationTestRunner({
14151480
])
14161481
})
14171482

1483+
it('should get product variants filtered by sku', async () => {
1484+
const payload = {
1485+
title: "Test product - 1",
1486+
handle: "test-1",
1487+
options: [{ title: "size", values: ["x", "l"] }],
1488+
shipping_profile_id: shippingProfile.id,
1489+
variants: [
1490+
{
1491+
title: "Custom inventory 1",
1492+
prices: [{ currency_code: "usd", amount: 100 }],
1493+
options: { size: "x" },
1494+
sku: "sku-123",
1495+
},
1496+
{
1497+
title: "Custom inventory 2",
1498+
prices: [{ currency_code: "usd", amount: 100 }],
1499+
options: { size: "l" },
1500+
sku: "sku-456",
1501+
},
1502+
],
1503+
}
1504+
1505+
const product = (
1506+
await api.post(`/admin/products`, payload, adminHeaders)
1507+
).data.product
1508+
1509+
const variants = (
1510+
await api.get(
1511+
`/admin/products/${product.id}/variants?sku=sku-123`,
1512+
adminHeaders
1513+
)
1514+
).data.variants
1515+
1516+
expect(variants).toEqual([
1517+
expect.objectContaining({
1518+
title: "Custom inventory 1",
1519+
product_id: product.id,
1520+
}),
1521+
])
1522+
});
1523+
14181524
it("should get product variants filtered by manage_inventory", async () => {
14191525
const payload = {
14201526
title: "Test product - 1",

integration-tests/http/__tests__/product/store/product.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,10 @@ medusaIntegrationTestRunner({
532532
{
533533
title: "test variant 1",
534534
manage_inventory: true,
535+
sku: 'test-sku',
536+
ean: 'test-ean',
537+
upc: 'test-upc',
538+
barcode: 'test-barcode',
535539
options: {
536540
size: "large",
537541
color: "green",
@@ -892,6 +896,65 @@ medusaIntegrationTestRunner({
892896
])
893897
})
894898

899+
900+
901+
902+
it("can filter products by ean", async () => {
903+
const response = await api.get(
904+
`/store/products?variants[ean]=${product.variants[0].ean}`,
905+
storeHeaders
906+
)
907+
908+
expect(response.status).toEqual(200)
909+
expect(response.data.count).toEqual(1)
910+
expect(response.data.products).toEqual([
911+
expect.objectContaining({ id: product.id }),
912+
])
913+
})
914+
915+
it("can filter products by upc", async () => {
916+
const response = await api.get(
917+
`/store/products?variants[upc]=${product.variants[0].upc}`,
918+
storeHeaders
919+
)
920+
921+
expect(response.status).toEqual(200)
922+
expect(response.data.count).toEqual(1)
923+
expect(response.data.products).toEqual([
924+
expect.objectContaining({ id: product.id }),
925+
])
926+
})
927+
928+
it("can filter products by barcode", async () => {
929+
const response = await api.get(
930+
`/store/products?variants[barcode]=${product.variants[0].barcode}`,
931+
storeHeaders
932+
)
933+
expect(response.status).toEqual(200)
934+
expect(response.data.count).toEqual(1)
935+
expect(response.data.products).toEqual([
936+
expect.objectContaining({ id: product.id }),
937+
])
938+
})
939+
940+
941+
it("can filter products by sku", async () => {
942+
const response = await api.get(
943+
`/store/products?variants[sku]=${product.variants[0].sku}`,
944+
storeHeaders
945+
)
946+
947+
expect(response.status).toEqual(200)
948+
expect(response.data.count).toEqual(1)
949+
expect(response.data.products).toEqual([
950+
expect.objectContaining({ id: product.id }),
951+
])
952+
})
953+
954+
955+
956+
957+
895958
it("returns a list of products with one of the given handles", async () => {
896959
const response = await api.get(
897960
`/store/products?handle[]=${product.handle}&handle[]=${product2.handle}`,

packages/core/types/src/http/product/admin/queries.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export interface AdminProductVariantParams
2424
* out of stock.
2525
*/
2626
allow_backorder?: boolean
27+
/**
28+
* Filter by sku(s).
29+
*/
30+
sku?: string | string[]
2731
/**
2832
* Filter by variant ean(s).
2933
*/
@@ -66,6 +70,12 @@ export interface AdminProductExportParams extends Omit<AdminProductListParams, "
6670
id?: string[]
6771
}
6872
variants?: {
73+
74+
sku?: string | string[] | OperatorMap<string | string[]>
75+
ean?: string | string[] | OperatorMap<string | string[]>
76+
upc?: string | string[] | OperatorMap<string | string[]>
77+
barcode?: string | string[] | OperatorMap<string | string[]>
78+
6979
options?: {
7080
value?: string
7181
option_id?: string

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,11 @@ export interface BaseProductVariantParams
446446
options?: {
447447
value: string
448448
option_id: string
449-
}
449+
},
450+
sku?: string | string[]
451+
ean?: string | string[]
452+
upc?: string | string[]
453+
barcode?: string | string[]
450454
created_at?: OperatorMap<string>
451455
updated_at?: OperatorMap<string>
452456
deleted_at?: OperatorMap<string>

packages/core/types/src/http/product/store/queries.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface StoreProductListParams
4747
/**
4848
* Filter by the product's variants.
4949
*/
50-
variants?: Pick<StoreProductVariantParams, "options">
50+
variants?: Pick<StoreProductVariantParams, "options" | "sku" | "ean" | "upc" | "barcode">
5151
/**
5252
* The locale code in BCP 47 format. Information of the
5353
* product and related entities will be localized based on the provided locale.

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,12 @@ export interface FilterableProductProps
739739
* Filters on a product's variant properties.
740740
*/
741741
variants?: {
742+
743+
sku?: string | string[] | OperatorMap<string | string[]>
744+
ean?: string | string[] | OperatorMap<string | string[]>
745+
upc?: string | string[] | OperatorMap<string | string[]>
746+
barcode?: string | string[] | OperatorMap<string | string[]>
747+
742748
options?: {
743749
value?: string
744750
option_id?: string
@@ -931,6 +937,23 @@ export interface FilterableProductVariantProps
931937
* The SKUs to filter product variants by.
932938
*/
933939
sku?: string | string[] | OperatorMap<string | string[]>
940+
941+
/**
942+
* The EANs to filter product variants by.
943+
*/
944+
ean?: string | string[] | OperatorMap<string | string[]>
945+
946+
/**
947+
* The UPCs to filter product variants by.
948+
*/
949+
upc?: string | string[] | OperatorMap<string | string[]>
950+
951+
/**
952+
* The barcodes to filter product variants by.
953+
*/
954+
barcode?: string | string[] | OperatorMap<string | string[]>
955+
956+
934957
/**
935958
* Filter the product variants by their associated products' IDs.
936959
*/

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const AdminGetProductVariantsParamsFields = z.object({
1010
id: z.union([z.string(), z.array(z.string())]).optional(),
1111
manage_inventory: booleanString().optional(),
1212
allow_backorder: booleanString().optional(),
13+
sku: z.union([z.string(), z.array(z.string())]).optional(),
1314
ean: z.union([z.string(), z.array(z.string())]).optional(),
1415
upc: z.union([z.string(), z.array(z.string())]).optional(),
1516
barcode: z.union([z.string(), z.array(z.string())]).optional(),

packages/medusa/src/api/store/products/validators.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export const StoreGetProductVariantsParamsFields = z.object({
2929
q: z.string().optional(),
3030
id: z.union([z.string(), z.array(z.string())]).optional(),
3131
sku: z.union([z.string(), z.array(z.string())]).optional(),
32+
ean: z.union([z.string(), z.array(z.string())]).optional(),
33+
upc: z.union([z.string(), z.array(z.string())]).optional(),
34+
barcode: z.union([z.string(), z.array(z.string())]).optional(),
35+
3236
options: z
3337
.object({ value: z.string().optional(), option_id: z.string().optional() })
3438
.optional(),

0 commit comments

Comments
 (0)