fix: storefront product filtering (#1189)

* fix: allow multiple ids in list + expand, fields param

* fix: add filtering by title

* fix: adds integration test

* fix: adds integration test of product variant filtering

* fix: integration tests

* fix: unit tests

* fix: refactor query param parsing
This commit is contained in:
Sebastian Rindom
2022-03-17 23:28:15 +01:00
committed by GitHub
parent 4be991c156
commit e3655b53f7
12 changed files with 324 additions and 28 deletions
@@ -33,6 +33,8 @@ describe("GET /admin/products/:id", () => {
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
@@ -51,6 +53,7 @@ describe("GET /admin/products/:id", () => {
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
],
relations: [
@@ -73,6 +73,8 @@ export const defaultAdminProductFields = [
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
@@ -91,6 +93,7 @@ export const defaultAdminProductFields = [
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
]
@@ -98,6 +101,8 @@ export const allowedAdminProductFields = [
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
@@ -116,6 +121,7 @@ export const allowedAdminProductFields = [
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
]
@@ -8,11 +8,11 @@ import {
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 { IsType } from "../../../../utils/validators/is-type"
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
/**
@@ -64,6 +64,8 @@ export default async (req, res) => {
const validated = await validator(StoreGetProductsParams, req.query)
const filterableFields: StoreGetProductsParams = omit(validated, [
"fields",
"expand",
"limit",
"offset",
])
@@ -71,8 +73,23 @@ export default async (req, res) => {
// get only published products for store endpoint
filterableFields["status"] = ["published"]
let includeFields: string[] = []
if (validated.fields) {
const set = new Set(validated.fields.split(","))
set.add("id")
includeFields = [...set]
}
let expandFields: string[] = []
if (validated.expand) {
expandFields = validated.expand.split(",")
}
const listConfig = {
relations: defaultStoreProductsRelations,
select: includeFields.length ? includeFields : undefined,
relations: expandFields.length
? expandFields
: defaultStoreProductsRelations,
skip: validated.offset,
take: validated.limit,
}
@@ -91,6 +108,14 @@ export default async (req, res) => {
}
export class StoreGetProductsPaginationParams {
@IsString()
@IsOptional()
fields?: string
@IsString()
@IsOptional()
expand?: string
@IsNumber()
@IsOptional()
@Type(() => Number)
@@ -103,9 +128,9 @@ export class StoreGetProductsPaginationParams {
}
export class StoreGetProductsParams extends StoreGetProductsPaginationParams {
@IsString()
@IsOptional()
id?: string
@IsType([String, [String]])
id?: string | string[]
@IsString()
@IsOptional()
@@ -1,8 +1,12 @@
import { Type } from "class-transformer"
import { omit } from "lodash"
import { IsInt, IsOptional, IsString } from "class-validator"
import { defaultStoreVariantRelations } from "."
import { FilterableProductVariantProps } from "../../../../types/product-variant"
import ProductVariantService from "../../../../services/product-variant"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
import { NumericalComparisonOperator } from "../../../../types/common"
/**
* @oas [get] /variants
@@ -29,17 +33,14 @@ import { validator } from "../../../../utils/validator"
* $ref: "#/components/schemas/product_variant"
*/
export default async (req, res) => {
const { limit, offset, expand, ids } = await validator(
StoreGetVariantsParams,
req.query
)
const validated = await validator(StoreGetVariantsParams, req.query)
const { expand, offset, limit } = validated
let expandFields: string[] = []
if (expand) {
expandFields = expand.split(",")
}
let selector = {}
const listConfig = {
relations: expandFields.length
? expandFields
@@ -48,14 +49,21 @@ export default async (req, res) => {
take: limit,
}
if (ids) {
selector = { id: ids.split(",") }
const filterableFields: FilterableProductVariantProps = omit(validated, [
"ids",
"limit",
"offset",
"expand",
])
if (validated.ids) {
filterableFields.id = validated.ids.split(",")
}
const variantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const variants = await variantService.list(selector, listConfig)
const variants = await variantService.list(filterableFields, listConfig)
res.json({ variants })
}
@@ -78,4 +86,16 @@ export class StoreGetVariantsParams {
@IsOptional()
@IsString()
ids?: string
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
@IsOptional()
@IsType([String, [String]])
title?: string | string[]
@IsOptional()
@IsType([Number, NumericalComparisonOperator])
inventory_quantity?: number | NumericalComparisonOperator
}
+14 -11
View File
@@ -13,6 +13,7 @@ import { Product } from "../models/product"
type DefaultWithoutRelations = Omit<FindManyOptions<Product>, "relations">
type CustomOptions = {
select?: DefaultWithoutRelations["select"]
where?: DefaultWithoutRelations["where"] & {
tags?: FindOperator<ProductTag>
}
@@ -77,9 +78,7 @@ export class ProductRepository extends Repository<Product> {
return [entities, count]
}
private getGroupedRelations(
relations: Array<keyof Product>
): {
private getGroupedRelations(relations: Array<keyof Product>): {
[toplevel: string]: string[]
} {
const groupedRelations: { [toplevel: string]: string[] } = {}
@@ -98,12 +97,17 @@ export class ProductRepository extends Repository<Product> {
private async queryProductsWithIds(
entityIds: string[],
groupedRelations: { [toplevel: string]: string[] },
withDeleted = false
withDeleted = false,
select: (keyof Product)[] = []
): Promise<Product[]> {
const entitiesIdsWithRelations = await Promise.all(
Object.entries(groupedRelations).map(([toplevel, rels]) => {
let querybuilder = this.createQueryBuilder("products")
if (select && select.length) {
querybuilder.select(select.map((f) => `products.${f}`))
}
if (toplevel === "variants") {
querybuilder = querybuilder
.leftJoinAndSelect(
@@ -193,13 +197,13 @@ export class ProductRepository extends Repository<Product> {
const entitiesIdsWithRelations = await this.queryProductsWithIds(
entitiesIds,
groupedRelations,
idsOrOptionsWithoutRelations.withDeleted
idsOrOptionsWithoutRelations.withDeleted,
idsOrOptionsWithoutRelations.select
)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesToReturn = this.mergeEntitiesWithRelations(
entitiesAndRelations
)
const entitiesToReturn =
this.mergeEntitiesWithRelations(entitiesAndRelations)
return [entitiesToReturn, count]
}
@@ -240,9 +244,8 @@ export class ProductRepository extends Repository<Product> {
)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesToReturn = this.mergeEntitiesWithRelations(
entitiesAndRelations
)
const entitiesToReturn =
this.mergeEntitiesWithRelations(entitiesAndRelations)
return entitiesToReturn
}
+3 -1
View File
@@ -327,7 +327,9 @@ class ProductService extends BaseService {
return existing.id
}
const created = productTypeRepository.create(type)
const created = productTypeRepository.create({
value: type.value,
})
const result = await productTypeRepository.save(created)
return result.id
+4 -4
View File
@@ -70,8 +70,8 @@ export class FilterableProductVariantProps {
@IsType([String, [String], StringComparisonOperator])
id?: string | string[] | StringComparisonOperator
@IsString()
title?: string
@IsType([String, [String]])
title?: string | string[]
@IsType([String, [String]])
product_id?: string | string[]
@@ -88,8 +88,8 @@ export class FilterableProductVariantProps {
@IsType([String])
upc?: string
@IsNumber()
inventory_quantity?: number
@IsType([Number, NumericalComparisonOperator])
inventory_quantity?: number | NumericalComparisonOperator
@IsBoolean()
allow_backorder?: boolean