feat: expand store product filtering (#973)
This commit is contained in:
@@ -4,9 +4,10 @@ const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
|
||||
const productSeeder = require("../../helpers/product-seeder")
|
||||
const productSeeder = require("../../helpers/store-product-seeder")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("/store/products", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
@@ -23,6 +24,204 @@ describe("/store/products", () => {
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("GET /store/products", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await productSeeder(dbConnection)
|
||||
await adminSeeder(dbConnection)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
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).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-product_filtering_2",
|
||||
collection_id: "test-collection2",
|
||||
}),
|
||||
])
|
||||
|
||||
for (const notExpect of notExpected) {
|
||||
expect(response.data.products).toEqual(
|
||||
expect.not.arrayContaining([notExpect])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("returns a list of products in 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).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-product_filtering_1",
|
||||
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.objectContaining({
|
||||
id: "giftcard",
|
||||
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).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-product_filtering_1",
|
||||
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: "test-product_filtering_1" }),
|
||||
]
|
||||
|
||||
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).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-product_filtering_2",
|
||||
handle: "test-product_filtering_2",
|
||||
}),
|
||||
])
|
||||
|
||||
for (const notExpect of notExpected) {
|
||||
expect(response.data.products).toEqual(
|
||||
expect.not.arrayContaining([notExpect])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "giftcard",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-product_filtering_1",
|
||||
collection_id: "test-collection1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-product_filtering_2",
|
||||
collection_id: "test-collection2",
|
||||
}),
|
||||
])
|
||||
|
||||
for (const notExpect of notExpected) {
|
||||
expect(response.data.products).toEqual(
|
||||
expect.not.arrayContaining([notExpect])
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("/store/products/:id", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
|
||||
287
integration-tests/api/helpers/store-product-seeder.js
Normal file
287
integration-tests/api/helpers/store-product-seeder.js
Normal file
@@ -0,0 +1,287 @@
|
||||
const {
|
||||
ProductCollection,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductOption,
|
||||
Region,
|
||||
Product,
|
||||
ShippingProfile,
|
||||
ProductVariant,
|
||||
Image,
|
||||
} = require("@medusajs/medusa")
|
||||
|
||||
module.exports = async (connection, data = {}) => {
|
||||
const manager = connection.manager
|
||||
|
||||
const defaultProfile = await manager.findOne(ShippingProfile, {
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const coll = manager.create(ProductCollection, {
|
||||
id: "test-collection",
|
||||
handle: "test-collection",
|
||||
title: "Test collection",
|
||||
})
|
||||
|
||||
await manager.save(coll)
|
||||
|
||||
const coll1 = manager.create(ProductCollection, {
|
||||
id: "test-collection1",
|
||||
handle: "test-collection1",
|
||||
title: "Test collection 1",
|
||||
})
|
||||
|
||||
await manager.save(coll1)
|
||||
|
||||
const coll2 = manager.create(ProductCollection, {
|
||||
id: "test-collection2",
|
||||
handle: "test-collection2",
|
||||
title: "Test collection 2",
|
||||
})
|
||||
|
||||
await manager.save(coll2)
|
||||
|
||||
const tag = manager.create(ProductTag, {
|
||||
id: "tag1",
|
||||
value: "123",
|
||||
})
|
||||
|
||||
await manager.save(tag)
|
||||
|
||||
const tag3 = manager.create(ProductTag, {
|
||||
id: "tag3",
|
||||
value: "123",
|
||||
})
|
||||
|
||||
await manager.save(tag3)
|
||||
|
||||
const tag4 = manager.create(ProductTag, {
|
||||
id: "tag4",
|
||||
value: "123",
|
||||
})
|
||||
|
||||
await manager.save(tag4)
|
||||
|
||||
const type = manager.create(ProductType, {
|
||||
id: "test-type",
|
||||
value: "test-type",
|
||||
})
|
||||
|
||||
await manager.save(type)
|
||||
|
||||
const image = manager.create(Image, {
|
||||
id: "test-image",
|
||||
url: "test-image.png",
|
||||
})
|
||||
|
||||
await manager.save(image)
|
||||
|
||||
await manager.insert(Region, {
|
||||
id: "test-region",
|
||||
name: "Test Region",
|
||||
currency_code: "usd",
|
||||
tax_rate: 0,
|
||||
})
|
||||
|
||||
const p = manager.create(Product, {
|
||||
id: "test-product",
|
||||
handle: "test-product",
|
||||
title: "Test product",
|
||||
profile_id: defaultProfile.id,
|
||||
description: "test-product-description",
|
||||
collection_id: "test-collection",
|
||||
type: { id: "test-type", value: "test-type" },
|
||||
tags: [
|
||||
{ id: "tag1", value: "123" },
|
||||
{ tag: "tag2", value: "456" },
|
||||
],
|
||||
})
|
||||
|
||||
p.images = [image]
|
||||
|
||||
await manager.save(p)
|
||||
|
||||
await manager.save(ProductOption, {
|
||||
id: "test-option",
|
||||
title: "test-option",
|
||||
product_id: "test-product",
|
||||
})
|
||||
|
||||
const variant1 = await manager.create(ProductVariant, {
|
||||
id: "test-variant",
|
||||
inventory_quantity: 10,
|
||||
title: "Test variant",
|
||||
variant_rank: 0,
|
||||
sku: "test-sku",
|
||||
ean: "test-ean",
|
||||
upc: "test-upc",
|
||||
barcode: "test-barcode",
|
||||
product_id: "test-product",
|
||||
prices: [{ id: "test-price", currency_code: "usd", amount: 100 }],
|
||||
options: [
|
||||
{
|
||||
id: "test-variant-option",
|
||||
value: "Default variant",
|
||||
option_id: "test-option",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await manager.save(variant1)
|
||||
|
||||
const variant2 = await manager.create(ProductVariant, {
|
||||
id: "test-variant_1",
|
||||
inventory_quantity: 10,
|
||||
title: "Test variant rank (1)",
|
||||
variant_rank: 2,
|
||||
sku: "test-sku1",
|
||||
ean: "test-ean1",
|
||||
upc: "test-upc1",
|
||||
barcode: "test-barcode 1",
|
||||
product_id: "test-product",
|
||||
prices: [{ id: "test-price1", currency_code: "usd", amount: 100 }],
|
||||
options: [
|
||||
{
|
||||
id: "test-variant-option-1",
|
||||
value: "Default variant 1",
|
||||
option_id: "test-option",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await manager.save(variant2)
|
||||
|
||||
const variant3 = await manager.create(ProductVariant, {
|
||||
id: "test-variant_2",
|
||||
inventory_quantity: 10,
|
||||
title: "Test variant rank (2)",
|
||||
variant_rank: 1,
|
||||
sku: "test-sku2",
|
||||
ean: "test-ean2",
|
||||
upc: "test-upc2",
|
||||
product_id: "test-product",
|
||||
prices: [{ id: "test-price2", currency_code: "usd", amount: 100 }],
|
||||
options: [
|
||||
{
|
||||
id: "test-variant-option-2",
|
||||
value: "Default variant 2",
|
||||
option_id: "test-option",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await manager.save(variant3)
|
||||
|
||||
const p1 = manager.create(Product, {
|
||||
id: "test-product1",
|
||||
handle: "test-product1",
|
||||
title: "Test product1",
|
||||
profile_id: defaultProfile.id,
|
||||
description: "test-product-description1",
|
||||
collection_id: "test-collection",
|
||||
type: { id: "test-type", value: "test-type" },
|
||||
tags: [
|
||||
{ id: "tag1", value: "123" },
|
||||
{ tag: "tag2", value: "456" },
|
||||
],
|
||||
})
|
||||
|
||||
await manager.save(p1)
|
||||
|
||||
const variant4 = await manager.create(ProductVariant, {
|
||||
id: "test-variant_3",
|
||||
inventory_quantity: 10,
|
||||
title: "Test variant rank (2)",
|
||||
variant_rank: 1,
|
||||
sku: "test-sku3",
|
||||
ean: "test-ean3",
|
||||
upc: "test-upc3",
|
||||
product_id: "test-product1",
|
||||
prices: [{ id: "test-price3", currency_code: "usd", amount: 100 }],
|
||||
options: [
|
||||
{
|
||||
id: "test-variant-option-3",
|
||||
value: "Default variant 3",
|
||||
option_id: "test-option",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await manager.save(variant4)
|
||||
|
||||
const variant5 = await manager.create(ProductVariant, {
|
||||
id: "test-variant_4",
|
||||
inventory_quantity: 10,
|
||||
title: "Test variant rank (2)",
|
||||
variant_rank: 0,
|
||||
sku: "test-sku4",
|
||||
ean: "test-ean4",
|
||||
upc: "test-upc4",
|
||||
product_id: "test-product1",
|
||||
prices: [{ id: "test-price4", currency_code: "usd", amount: 100 }],
|
||||
options: [
|
||||
{
|
||||
id: "test-variant-option-4",
|
||||
value: "Default variant 4",
|
||||
option_id: "test-option",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await manager.save(variant5)
|
||||
|
||||
const product1 = manager.create(Product, {
|
||||
id: "test-product_filtering_1",
|
||||
handle: "test-product_filtering_1",
|
||||
title: "Test product filtering 1",
|
||||
profile_id: defaultProfile.id,
|
||||
description: "test-product-description",
|
||||
type: { id: "test-type", value: "test-type" },
|
||||
collection_id: "test-collection1",
|
||||
status: "published",
|
||||
tags: [{ id: "tag3", value: "123" }],
|
||||
})
|
||||
|
||||
await manager.save(product1)
|
||||
|
||||
const product2 = manager.create(Product, {
|
||||
id: "test-product_filtering_2",
|
||||
handle: "test-product_filtering_2",
|
||||
title: "Test product filtering 2",
|
||||
profile_id: defaultProfile.id,
|
||||
description: "test-product-description",
|
||||
type: { id: "test-type", value: "test-type" },
|
||||
collection_id: "test-collection2",
|
||||
status: "published",
|
||||
tags: [{ id: "tag4", value: "1234" }],
|
||||
})
|
||||
|
||||
await manager.save(product2)
|
||||
|
||||
const product3 = manager.create(Product, {
|
||||
id: "test-product_filtering_3",
|
||||
handle: "test-product_filtering_3",
|
||||
title: "Test product filtering 3",
|
||||
profile_id: defaultProfile.id,
|
||||
description: "test-product-description",
|
||||
type: { id: "test-type", value: "test-type" },
|
||||
collection_id: "test-collection1",
|
||||
status: "draft",
|
||||
tags: [{ id: "tag4", value: "1234" }],
|
||||
})
|
||||
|
||||
await manager.save(product3)
|
||||
|
||||
const gift_card = manager.create(Product, {
|
||||
id: "giftcard",
|
||||
handle: "giftcard",
|
||||
is_giftcard: true,
|
||||
title: "giftcard",
|
||||
profile_id: defaultProfile.id,
|
||||
description: "test-product-description",
|
||||
type: { id: "test-type", value: "test-type" },
|
||||
status: "published",
|
||||
})
|
||||
|
||||
await manager.save(gift_card)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { InviteServiceMock } from "../../../../../services/__mocks__/invite"
|
||||
import { UserRole } from "../../../../../types/user"
|
||||
@@ -9,9 +10,9 @@ describe("POST /invites", () => {
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/admin/invites`, {
|
||||
payload: {
|
||||
role: "",
|
||||
role: "admin",
|
||||
},
|
||||
session: {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: "test_user",
|
||||
},
|
||||
@@ -25,6 +26,10 @@ describe("POST /invites", () => {
|
||||
|
||||
it("throws when role is empty", () => {
|
||||
expect(subject.error).toBeTruthy()
|
||||
expect(subject.error.text).toEqual(
|
||||
`{"type":"invalid_data","message":"user must be an email"}`
|
||||
)
|
||||
expect(subject.error.status).toEqual(400)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,9 +42,9 @@ describe("POST /invites", () => {
|
||||
role: "admin",
|
||||
user: "lebron@james.com",
|
||||
},
|
||||
session: {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: "test_user",
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
import { Type } from "class-transformer"
|
||||
import { IsBoolean, IsInt, IsNumber, IsOptional } from "class-validator"
|
||||
import { Transform, Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { omit, pickBy, identity } from "lodash"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { defaultStoreProductsRelations } from "."
|
||||
import { ProductService } from "../../../../services"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
|
||||
/**
|
||||
* @oas [get] /products
|
||||
* operationId: GetProducts
|
||||
* summary: List Products
|
||||
* description: "Retrieves a list of Products."
|
||||
* parameters:
|
||||
* - (query) q {string} Query used for searching products.
|
||||
* - (query) id {string} Id of the product to search for.
|
||||
* - (query) collection_id {string[]} Collection ids to search for.
|
||||
* - (query) tags {string[]} Tags to search for.
|
||||
* - (query) title {string} to search for.
|
||||
* - (query) description {string} to search for.
|
||||
* - (query) handle {string} to search for.
|
||||
* - (query) is_giftcard {string} Search for giftcards using is_giftcard=true.
|
||||
* - (query) type {string} to search for.
|
||||
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting products was created, i.e. less than, greater than etc.
|
||||
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting products was updated, i.e. less than, greater than etc.
|
||||
* - (query) deleted_at {DateComparisonOperator} Date comparison for when resulting products was deleted, i.e. less than, greater than etc.
|
||||
* - (query) offset {string} How many products to skip in the result.
|
||||
* - (query) limit {string} Limit the number of products returned.
|
||||
* tags:
|
||||
* - Product
|
||||
* responses:
|
||||
@@ -37,13 +63,13 @@ export default async (req, res) => {
|
||||
|
||||
const validated = await validator(StoreGetProductsParams, req.query)
|
||||
|
||||
const selector = {}
|
||||
const filterableFields: StoreGetProductsParams = omit(validated, [
|
||||
"limit",
|
||||
"offset",
|
||||
])
|
||||
|
||||
if (validated.is_giftcard && validated.is_giftcard === true) {
|
||||
selector["is_giftcard"] = validated.is_giftcard
|
||||
}
|
||||
|
||||
selector["status"] = ["published"]
|
||||
// get only published products for store endpoint
|
||||
filterableFields["status"] = ["published"]
|
||||
|
||||
const listConfig = {
|
||||
relations: defaultStoreProductsRelations,
|
||||
@@ -52,7 +78,7 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
const [products, count] = await productService.listAndCount(
|
||||
selector,
|
||||
pickBy(filterableFields, (val) => typeof val !== "undefined"),
|
||||
listConfig
|
||||
)
|
||||
|
||||
@@ -64,17 +90,68 @@ export default async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
export class StoreGetProductsParams {
|
||||
@IsInt()
|
||||
export class StoreGetProductsPaginationParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit = 100
|
||||
offset?: number = 0
|
||||
|
||||
@IsInt()
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset = 0
|
||||
limit?: number = 100
|
||||
}
|
||||
|
||||
export class StoreGetProductsParams extends StoreGetProductsPaginationParams {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
q?: string
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
collection_id?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
tags?: string[]
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
title?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
handle?: string
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
|
||||
is_giftcard?: boolean
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
type?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
is_giftcard?: boolean
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => DateComparisonOperator)
|
||||
deleted_at?: DateComparisonOperator
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user