feat(medusa): Ordering products on retrieval (#2815)

**What**

Move to transformQuery which adds a default ordering and also allows to order the product list from the store API

**How**
Among other things, fix the product repo to allow ordering by either a key from the product or a key from a relation

FIXES CORE-911
FIXES CORE-901
This commit is contained in:
Adrien de Peretti
2022-12-22 10:33:53 +00:00
committed by GitHub
parent 5ae319004f
commit 463f83ffdd
17 changed files with 1080 additions and 1927 deletions
@@ -157,6 +157,7 @@ import { FilterableProductProps } from "../../../../types/product"
* - (query) limit=50 {integer} Limit the number of products returned.
* - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result.
* - (query) fields {string} (Comma separated) Which fields should be included in each product of the result.
* - (query) order {string} the field used to order the products.
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
@@ -258,4 +259,8 @@ export class AdminGetProductsParams extends FilterableProductProps {
@IsString()
@IsOptional()
fields?: string
@IsString()
@IsOptional()
order?: string
}
@@ -23,6 +23,10 @@ describe("GET /store/products", () => {
relations: defaultStoreProductsRelations,
skip: 0,
take: 100,
select: undefined,
order: {
created_at: "DESC",
},
}
)
})
@@ -50,6 +54,10 @@ describe("GET /store/products", () => {
relations: defaultStoreProductsRelations,
skip: 0,
take: 100,
order: {
created_at: "DESC",
},
select: undefined,
}
)
})
@@ -2,13 +2,14 @@ import { RequestHandler, Router } from "express"
import "reflect-metadata"
import { Product } from "../../../.."
import middlewares from "../../../middlewares"
import middlewares, { transformQuery } from "../../../middlewares"
import { FlagRouter } from "../../../../utils/flag-router"
import { PaginatedResponse } from "../../../../types/common"
import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params"
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import { validateProductSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-product-sales-channel-association"
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
import { StoreGetProductsParams } from "./list-products"
const route = Router()
@@ -24,7 +25,14 @@ export default (app, featureFlagRouter: FlagRouter) => {
route.use("/:id", validateProductSalesChannelAssociation)
}
route.get("/", middlewares.wrap(require("./list-products").default))
route.get(
"/",
transformQuery(StoreGetProductsParams, {
defaultRelations: defaultStoreProductsRelations,
isList: true,
}),
middlewares.wrap(require("./list-products").default)
)
route.get("/:id", middlewares.wrap(require("./get-product").default))
route.post("/search", middlewares.wrap(require("./search").default))
@@ -7,21 +7,16 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { omit, pickBy } from "lodash"
import {
CartService,
ProductService,
RegionService,
} from "../../../../services"
import { isDefined } from "medusa-core-utils"
import { defaultStoreProductsRelations } from "."
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { Product } from "../../../../models"
import PricingService from "../../../../services/pricing"
import { DateComparisonOperator } from "../../../../types/common"
import { PriceSelectionParams } from "../../../../types/price-selection"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { validator } from "../../../../utils/validator"
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
import { IsType } from "../../../../utils/validators/is-type"
import { FlagRouter } from "../../../../utils/flag-router"
@@ -133,6 +128,7 @@ import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/pub
* - (query) limit=100 {integer} Limit the number of products returned.
* - (query) expand {string} (Comma separated) Which fields should be expanded in each order of the result.
* - (query) fields {string} (Comma separated) Which fields should be included in each order of the result.
* - (query) order {string} the field used to order the products.
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
@@ -196,59 +192,34 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
const regionService: RegionService = req.scope.resolve("regionService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const validated = await validator(StoreGetProductsParams, req.query)
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
if (req.publishableApiKeyScopes?.sales_channel_id.length) {
validated.sales_channel_id =
validated.sales_channel_id ||
req.publishableApiKeyScopes.sales_channel_id
}
}
const filterableFields: StoreGetProductsParams = omit(validated, [
"fields",
"expand",
"limit",
"offset",
"cart_id",
"region_id",
"currency_code",
])
const validated = req.validatedQuery as StoreGetProductsParams
let {
cart_id,
region_id: regionId,
currency_code: currencyCode,
...filterableFields
} = req.filterableFields
const listConfig = req.listConfig
// get only published products for store endpoint
filterableFields["status"] = ["published"]
let includeFields: (keyof Product)[] = []
if (validated.fields) {
const set = new Set(validated.fields.split(",")) as Set<keyof Product>
set.add("id")
includeFields = [...set]
}
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
if (req.publishableApiKeyScopes?.sales_channel_id.length) {
filterableFields.sales_channel_id =
filterableFields.sales_channel_id ||
req.publishableApiKeyScopes.sales_channel_id
let expandFields: string[] = []
if (validated.expand) {
expandFields = validated.expand.split(",")
}
const listConfig = {
select: includeFields.length ? includeFields : undefined,
relations: expandFields.length
? expandFields
: defaultStoreProductsRelations,
skip: validated.offset,
take: validated.limit,
listConfig.relations.push("sales_channels")
}
}
const [rawProducts, count] = await productService.listAndCount(
pickBy(filterableFields, (val) => isDefined(val)),
filterableFields,
listConfig
)
let regionId = validated.region_id
let currencyCode = validated.currency_code
if (validated.cart_id) {
const cart = await cartService.retrieve(validated.cart_id, {
select: ["id", "region_id"],
@@ -261,7 +232,7 @@ export default async (req, res) => {
}
const products = await pricingService.setProductPrices(rawProducts, {
cart_id: validated.cart_id,
cart_id: cart_id,
region_id: regionId,
currency_code: currencyCode,
customer_id: req.user?.customer_id,
@@ -294,6 +265,10 @@ export class StoreGetProductsPaginationParams extends PriceSelectionParams {
@IsOptional()
@Type(() => Number)
limit?: number = 100
@IsString()
@IsOptional()
order?: string
}
export class StoreGetProductsParams extends StoreGetProductsPaginationParams {
@@ -1,6 +1,5 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("List variants", () => {
describe("list variants successfull", () => {