fix: Allow filtering products by variant options in store (#7784)
This commit is contained in:
1 parent
ee35379e21
commit
944051a951
6 files changed
+243
-505
No files matched your search
@@ -51,499 +51,6 @@ describe("/store/products", () => {
|
|||||||
medusaProcess.kill()
|
medusaProcess.kill()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("GET /store/products", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const defaultSalesChannel = await simpleSalesChannelFactory(
|
|
||||||
dbConnection,
|
|
||||||
{
|
|
||||||
id: "sales-channel",
|
|
||||||
is_default: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
await productSeeder(dbConnection, defaultSalesChannel)
|
|
||||||
await adminSeeder(dbConnection)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
const db = useDb()
|
|
||||||
await db.teardown()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by id ASC", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?order=id")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(5)
|
|
||||||
expect(response.data.products[0].id).toEqual(giftCardId)
|
|
||||||
expect(response.data.products[1].id).toEqual(testProductId)
|
|
||||||
expect(response.data.products[2].id).toEqual(testProductId1)
|
|
||||||
expect(response.data.products[3].id).toEqual(testProductFilteringId1)
|
|
||||||
expect(response.data.products[4].id).toEqual(testProductFilteringId2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by id DESC", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?order=-id")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(5)
|
|
||||||
expect(response.data.products[0].id).toEqual(testProductFilteringId2)
|
|
||||||
expect(response.data.products[1].id).toEqual(testProductFilteringId1)
|
|
||||||
expect(response.data.products[2].id).toEqual(testProductId1)
|
|
||||||
expect(response.data.products[3].id).toEqual(testProductId)
|
|
||||||
expect(response.data.products[4].id).toEqual(giftCardId)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by variants title DESC", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?order=-variants.title")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(5)
|
|
||||||
|
|
||||||
const testProductIndex = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId)
|
|
||||||
)
|
|
||||||
const testProduct1Index = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId1)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Since they have the same variant titles for rank 2, the order is not guaranteed
|
|
||||||
expect([3, 4]).toContain(testProductIndex)
|
|
||||||
expect([3, 4]).toContain(testProduct1Index)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by variants title ASC", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?order=variants.title")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(5)
|
|
||||||
|
|
||||||
const testProductIndex = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId)
|
|
||||||
)
|
|
||||||
const testProduct1Index = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId1)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(testProductIndex).toBe(0)
|
|
||||||
expect(testProduct1Index).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by variants prices DESC", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
await simpleProductFactory(dbConnection, {
|
|
||||||
id: testProductId2,
|
|
||||||
status: "published",
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
id: "test_variant_5",
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency: "usd",
|
|
||||||
amount: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
let response = await api.get(
|
|
||||||
"/store/products?order=-variants.prices.amount"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update amount to unsure order, same amount will add randomness in the result with the same amounts
|
|
||||||
const productToUpdate = response.data.products.find(
|
|
||||||
(p) => p.id === testProductId
|
|
||||||
)
|
|
||||||
const priceToUpdate = productToUpdate.variants[0].prices[0]
|
|
||||||
const priceData = {
|
|
||||||
id: priceToUpdate.id,
|
|
||||||
currency_code: priceToUpdate.currency_code,
|
|
||||||
amount: 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${testProductId}/variants/${productToUpdate.variants[0].id}`,
|
|
||||||
{ prices: [priceData] },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await api.get("/store/products?order=-variants.prices.amount")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(6)
|
|
||||||
|
|
||||||
const testProductIndex = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId)
|
|
||||||
)
|
|
||||||
const testProduct1Index = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId1)
|
|
||||||
)
|
|
||||||
const testProduct2Index = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId2)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(testProduct2Index).toBe(3) // 200
|
|
||||||
expect(testProductIndex).toBe(4) // 120
|
|
||||||
expect(testProduct1Index).toBe(5) // 100
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by variants prices ASC", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
await simpleProductFactory(dbConnection, {
|
|
||||||
id: testProductId2,
|
|
||||||
status: "published",
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
id: "test_variant_5",
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency: "usd",
|
|
||||||
amount: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
let response = await api.get(
|
|
||||||
"/store/products?order=variants.prices.amount"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update amount to unsure order, same amount will add randomness in the result with the same amounts
|
|
||||||
const productToUpdate = response.data.products.find(
|
|
||||||
(p) => p.id === testProductId1
|
|
||||||
)
|
|
||||||
const priceToUpdate = productToUpdate.variants[0].prices[0]
|
|
||||||
const priceData = {
|
|
||||||
id: priceToUpdate.id,
|
|
||||||
currency_code: priceToUpdate.currency_code,
|
|
||||||
amount: 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${testProductId1}/variants/${productToUpdate.variants[0].id}`,
|
|
||||||
{ prices: [priceData] },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await api.get("/store/products?order=variants.prices.amount")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(6)
|
|
||||||
|
|
||||||
const testProductIndex = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId)
|
|
||||||
)
|
|
||||||
const testProduct1Index = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === testProductId1)
|
|
||||||
)
|
|
||||||
const testProduct2Index = response.data.products.indexOf(
|
|
||||||
response.data.products.find((p) => p.id === "test-product2")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(testProductIndex).toBe(0) // 100
|
|
||||||
expect(testProduct1Index).toBe(1) // 120
|
|
||||||
expect(testProduct2Index).toBe(2) // 200
|
|
||||||
})
|
|
||||||
|
|
||||||
it("products contain only fields defined with `fields` param", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?fields=handle")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
|
|
||||||
expect(Object.keys(response.data.products[0])).toHaveLength(10)
|
|
||||||
expect(Object.keys(response.data.products[0])).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
"id",
|
|
||||||
"created_at",
|
|
||||||
|
|
||||||
// fields
|
|
||||||
"handle",
|
|
||||||
// relations
|
|
||||||
"variants",
|
|
||||||
"options",
|
|
||||||
"images",
|
|
||||||
"tags",
|
|
||||||
"collection",
|
|
||||||
"type",
|
|
||||||
"profiles",
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by id ASC and filtered with free text search", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?q=filtering&order=id")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(2)
|
|
||||||
|
|
||||||
expect(response.data.products).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId1,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId2,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of ordered products by id DESC and filtered with free text search", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get("/store/products?q=filtering&order=-id")
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(2)
|
|
||||||
|
|
||||||
expect(response.data.products).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId2,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId1,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of products in collection", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const notExpected = [
|
|
||||||
expect.objectContaining({ collection_id: "test-collection" }),
|
|
||||||
expect.objectContaining({ collection_id: "test-collection1" }),
|
|
||||||
]
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/store/products?collection_id[]=test-collection2")
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(1)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId2,
|
|
||||||
collection_id: "test-collection2",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const notExpect of notExpected) {
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.not.arrayContaining([notExpect])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of products with a given tag", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const notExpected = [expect.objectContaining({ id: "tag4" })]
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/store/products?tags[]=tag3")
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(1)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId1,
|
|
||||||
collection_id: "test-collection1",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const notExpect of notExpected) {
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.not.arrayContaining([notExpect])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns gift card product", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/store/products?is_giftcard=true")
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products.length).toEqual(1)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: giftCardId,
|
|
||||||
is_giftcard: true,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns non gift card products", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/store/products?is_giftcard=false")
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.not.arrayContaining([
|
|
||||||
expect.objectContaining({ is_giftcard: true }),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns product with tag", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const notExpected = [expect.objectContaining({ id: "tag4" })]
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/store/products?tags[]=tag3")
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(1)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId1,
|
|
||||||
collection_id: "test-collection1",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const notExpect of notExpected) {
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.not.arrayContaining([notExpect])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of products in with a given handle", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const notExpected = [
|
|
||||||
expect.objectContaining({ handle: testProductFilteringId1 }),
|
|
||||||
]
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/store/products?handle=test-product_filtering_2")
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(1)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId2,
|
|
||||||
handle: testProductFilteringId2,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const notExpect of notExpected) {
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.not.arrayContaining([notExpect])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("works when filtering by type_id", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get(
|
|
||||||
`/store/products?type_id[]=test-type&fields=id,title,type_id`
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products).toHaveLength(5)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type_id: "test-type",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns only published products", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const notExpected = [
|
|
||||||
expect.objectContaining({ status: "proposed" }),
|
|
||||||
expect.objectContaining({ status: "draft" }),
|
|
||||||
expect.objectContaining({ status: "rejected" }),
|
|
||||||
]
|
|
||||||
|
|
||||||
const response = await api.get("/store/products").catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.products.length).toEqual(5)
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductId1,
|
|
||||||
collection_id: "test-collection",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductId,
|
|
||||||
collection_id: "test-collection",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId2,
|
|
||||||
collection_id: "test-collection2",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: testProductFilteringId1,
|
|
||||||
collection_id: "test-collection1",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: giftCardId,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const notExpect of notExpected) {
|
|
||||||
expect(response.data.products).toEqual(
|
|
||||||
expect.not.arrayContaining([notExpect])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("list params", () => {
|
describe("list params", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await productSeeder(dbConnection)
|
await productSeeder(dbConnection)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ medusaIntegrationTestRunner({
|
|||||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||||
let store
|
let store
|
||||||
let appContainer
|
let appContainer
|
||||||
|
let collection
|
||||||
let product
|
let product
|
||||||
let product1
|
let product1
|
||||||
let product2
|
let product2
|
||||||
@@ -487,13 +488,31 @@ medusaIntegrationTestRunner({
|
|||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.inventory_item
|
).data.inventory_item
|
||||||
|
|
||||||
|
collection = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/collections",
|
||||||
|
{ title: "base-collection" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.collection
|
||||||
;[product, [variant]] = await createProducts({
|
;[product, [variant]] = await createProducts({
|
||||||
title: "test product 1",
|
title: "test product 1",
|
||||||
|
collection_id: collection.id,
|
||||||
status: ProductStatus.PUBLISHED,
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [
|
||||||
|
{ title: "size", values: ["large", "small"] },
|
||||||
|
{ title: "color", values: ["green"] },
|
||||||
|
],
|
||||||
|
tags: [{ value: "tag1" }],
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "test variant 1",
|
title: "test variant 1",
|
||||||
manage_inventory: true,
|
manage_inventory: true,
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
inventory_items: [
|
inventory_items: [
|
||||||
{
|
{
|
||||||
inventory_item_id: inventoryItem1.id,
|
inventory_item_id: inventoryItem1.id,
|
||||||
@@ -511,8 +530,20 @@ medusaIntegrationTestRunner({
|
|||||||
;[product2, [variant2]] = await createProducts({
|
;[product2, [variant2]] = await createProducts({
|
||||||
title: "test product 2 uniquely",
|
title: "test product 2 uniquely",
|
||||||
status: ProductStatus.PUBLISHED,
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [
|
||||||
|
{ title: "size", values: ["large", "small"] },
|
||||||
|
{ title: "material", values: ["cotton", "polyester"] },
|
||||||
|
],
|
||||||
variants: [
|
variants: [
|
||||||
{ title: "test variant 2", manage_inventory: false, prices: [] },
|
{
|
||||||
|
title: "test variant 2",
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
material: "cotton",
|
||||||
|
},
|
||||||
|
manage_inventory: false,
|
||||||
|
prices: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
;[product3, [variant3]] = await createProducts({
|
;[product3, [variant3]] = await createProducts({
|
||||||
@@ -618,6 +649,138 @@ medusaIntegrationTestRunner({
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("returns a list of ordered products by id ASC", async () => {
|
||||||
|
const response = await api.get("/store/products?order=id")
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
[product.id, product2.id, product3.id]
|
||||||
|
.sort((p1, p2) => p1.localeCompare(p2))
|
||||||
|
.map((id) => expect.objectContaining({ id }))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a list of ordered products by id DESC", async () => {
|
||||||
|
const response = await api.get("/store/products?order=-id")
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
[product.id, product2.id, product3.id]
|
||||||
|
.sort((p1, p2) => p2.localeCompare(p1))
|
||||||
|
.map((id) => expect.objectContaining({ id }))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: This doesn't work currently, but worked in v1
|
||||||
|
it.skip("returns a list of ordered products by variants title DESC", async () => {
|
||||||
|
const response = await api.get("/store/products?order=-variants.title")
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: product3.id }),
|
||||||
|
expect.objectContaining({ id: product2.id }),
|
||||||
|
expect.objectContaining({ id: product.id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: This doesn't work currently, but worked in v1
|
||||||
|
it.skip("returns a list of ordered products by variants title ASC", async () => {
|
||||||
|
const response = await api.get("/store/products?order=variants.title")
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: product3.id }),
|
||||||
|
expect.objectContaining({ id: product2.id }),
|
||||||
|
expect.objectContaining({ id: product.id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: This doesn't work currently, but worked in v1
|
||||||
|
it.skip("returns a list of ordered products by variants prices DESC", async () => {
|
||||||
|
let response = await api.get(
|
||||||
|
"/store/products?order=-variants.prices.amount"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: This doesn't work currently, but worked in v1
|
||||||
|
it.skip("returns a list of ordered products by variants prices ASC", async () => {})
|
||||||
|
|
||||||
|
// BREAKING: It seems `id` and `created_at` is always returned, even if not in the fields params
|
||||||
|
it("products contain only fields defined with `fields` param", async () => {
|
||||||
|
const response = await api.get("/store/products?fields=handle")
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(Object.keys(response.data.products[0])).toEqual([
|
||||||
|
"handle",
|
||||||
|
"created_at",
|
||||||
|
"id",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a list of products in collection", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products?collection_id[]=${collection.id}`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: product.id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a list of products with a given tag", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products?tags[]=${product.tags[0].id}`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: product.id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Not implemented yet
|
||||||
|
it.skip("returns gift card product", async () => {
|
||||||
|
const response = await api
|
||||||
|
.get("/store/products?is_giftcard=true")
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Not implemented yet
|
||||||
|
it.skip("returns non gift card products", async () => {
|
||||||
|
const response = await api
|
||||||
|
.get("/store/products?is_giftcard=false")
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a list of products in with a given handle", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products?handle=${product.handle}`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: product.id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a list of products filtered by variant options", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products?variants.options[option_id]=${product.options[1].id}&variants.options[value]=large`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: product.id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
describe("with publishable keys", () => {
|
describe("with publishable keys", () => {
|
||||||
let salesChannel1
|
let salesChannel1
|
||||||
let salesChannel2
|
let salesChannel2
|
||||||
|
|||||||
@@ -702,6 +702,12 @@ export interface FilterableProductProps
|
|||||||
*/
|
*/
|
||||||
value?: string[]
|
value?: string[]
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Filters on a product's variant properties.
|
||||||
|
*/
|
||||||
|
variants?: {
|
||||||
|
options: { value: string; option_id: string }
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Filter a product by the ID of the associated type
|
* Filter a product by the ID of the associated type
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const StoreGetProductVariantsParams = createFindParams({
|
|||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
status: ProductStatusEnum.array().optional(),
|
status: ProductStatusEnum.array().optional(),
|
||||||
|
options: z.object({ value: z.string(), option_id: z.string() }).optional(),
|
||||||
created_at: createOperatorMap().optional(),
|
created_at: createOperatorMap().optional(),
|
||||||
updated_at: createOperatorMap().optional(),
|
updated_at: createOperatorMap().optional(),
|
||||||
deleted_at: createOperatorMap().optional(),
|
deleted_at: createOperatorMap().optional(),
|
||||||
@@ -39,7 +40,16 @@ export const StoreGetProductsParams = createFindParams({
|
|||||||
.object({
|
.object({
|
||||||
region_id: z.string().optional(),
|
region_id: z.string().optional(),
|
||||||
currency_code: z.string().optional(),
|
currency_code: z.string().optional(),
|
||||||
variants: StoreGetProductVariantsParams.optional(),
|
variants: z
|
||||||
|
.object({
|
||||||
|
status: ProductStatusEnum.array().optional(),
|
||||||
|
options: z
|
||||||
|
.object({ value: z.string(), option_id: z.string() })
|
||||||
|
.optional(),
|
||||||
|
$and: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
|
$or: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
$and: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
$and: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
$or: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
$or: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,18 +9,41 @@ import {
|
|||||||
prepareRetrieveQuery,
|
prepareRetrieveQuery,
|
||||||
} from "../../utils/get-query-config"
|
} from "../../utils/get-query-config"
|
||||||
import { zodValidator } from "./zod-helper"
|
import { zodValidator } from "./zod-helper"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize an input query, especially from array like query params to an array type
|
* Normalize an input query, especially from array like query params to an array type
|
||||||
* e.g: /admin/orders/?fields[]=id,status,cart_id becomes { fields: ["id", "status", "cart_id"] }
|
* e.g: /admin/orders/?fields[]=id,status,cart_id becomes { fields: ["id", "status", "cart_id"] }
|
||||||
|
*
|
||||||
|
* We only support up to 2 levels of depth for query params in order to have a somewhat readable query param, and limit possible performance issues
|
||||||
*/
|
*/
|
||||||
const normalizeQuery = (req: MedusaRequest) => {
|
const normalizeQuery = (req: MedusaRequest) => {
|
||||||
return Object.entries(req.query).reduce((acc, [key, val]) => {
|
return Object.entries(req.query).reduce((acc, [key, val]) => {
|
||||||
if (Array.isArray(val) && val.length === 1) {
|
let normalizedValue = val
|
||||||
acc[key] = (val as string[])[0].split(",")
|
if (Array.isArray(val) && val.length === 1 && typeof val[0] === "string") {
|
||||||
} else {
|
normalizedValue = val[0].split(",")
|
||||||
acc[key] = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key.includes(".")) {
|
||||||
|
const [parent, child, ...others] = key.split(".")
|
||||||
|
if (others.length > 0) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
`Key accessor more than 2 levels deep: ${key}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acc[parent]) {
|
||||||
|
acc[parent] = {}
|
||||||
|
}
|
||||||
|
acc[parent] = {
|
||||||
|
...acc[parent],
|
||||||
|
[child]: normalizedValue,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc[key] = normalizedValue
|
||||||
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-6
@@ -803,6 +803,8 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("list", function () {
|
describe("list", function () {
|
||||||
|
let productOneData
|
||||||
|
let productTwoData
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const collections = await createCollections(
|
const collections = await createCollections(
|
||||||
MikroOrmWrapper.forkManager(),
|
MikroOrmWrapper.forkManager(),
|
||||||
@@ -812,16 +814,20 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
productCollectionOne = collections[0]
|
productCollectionOne = collections[0]
|
||||||
productCollectionTwo = collections[1]
|
productCollectionTwo = collections[1]
|
||||||
|
|
||||||
const productOneData = buildProductAndRelationsData({
|
const resp = await service.createProducts([
|
||||||
|
buildProductAndRelationsData({
|
||||||
collection_id: productCollectionOne.id,
|
collection_id: productCollectionOne.id,
|
||||||
})
|
options: [{ title: "size", values: ["large", "small"] }],
|
||||||
|
variants: [{ title: "variant 1", options: { size: "small" } }],
|
||||||
const productTwoData = buildProductAndRelationsData({
|
}),
|
||||||
|
buildProductAndRelationsData({
|
||||||
collection_id: productCollectionTwo.id,
|
collection_id: productCollectionTwo.id,
|
||||||
tags: [],
|
tags: [],
|
||||||
})
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
await service.createProducts([productOneData, productTwoData])
|
productOneData = resp[0]
|
||||||
|
productTwoData = resp[1]
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return a list of products scoped by collection id", async () => {
|
it("should return a list of products scoped by collection id", async () => {
|
||||||
@@ -843,6 +849,29 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should return a list of products scoped by variant options", async () => {
|
||||||
|
const productsWithVariants = await service.listProducts(
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
options: {
|
||||||
|
option_id: productOneData.options[0].id,
|
||||||
|
value: "small",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relations: ["variants", "variants.options"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(productsWithVariants).toHaveLength(1)
|
||||||
|
expect(productsWithVariants).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: productOneData.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("should return empty array when querying for a collection that doesnt exist", async () => {
|
it("should return empty array when querying for a collection that doesnt exist", async () => {
|
||||||
const products = await service.listProducts(
|
const products = await service.listProducts(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user