feat: expand store product filtering (#973)

This commit is contained in:
Philip Korsholm
2022-01-11 16:06:16 +01:00
committed by GitHub
parent 6dbd8d318d
commit f61eaeec12
4 changed files with 590 additions and 22 deletions

View File

@@ -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 {

View 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)
}

View File

@@ -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"),
},
},
})

View File

@@ -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
}