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:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user