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", () => {
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { flatten, groupBy, map, merge } from "lodash"
|
||||
import {
|
||||
Brackets,
|
||||
EntityRepository,
|
||||
FindOperator,
|
||||
In,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import { Brackets, EntityRepository, FindOperator, In, Repository, } from "typeorm"
|
||||
import { PriceList, Product, SalesChannel } from "../models"
|
||||
import {
|
||||
ExtendedFindConfig,
|
||||
Selector,
|
||||
WithRequiredProperty,
|
||||
} from "../types/common"
|
||||
import { ExtendedFindConfig, Selector, WithRequiredProperty, } from "../types/common"
|
||||
import { applyOrdering } from "../utils/repository"
|
||||
|
||||
export type ProductSelector = Omit<Selector<Product>, "tags"> & {
|
||||
tags: FindOperator<string[]>
|
||||
@@ -45,6 +36,8 @@ export class ProductRepository extends Repository<Product> {
|
||||
optionsWithoutRelations: FindWithoutRelationsOptions,
|
||||
shouldCount = false
|
||||
): Promise<[Product[], number]> {
|
||||
const productAlias = "product"
|
||||
|
||||
const tags = optionsWithoutRelations?.where?.tags
|
||||
delete optionsWithoutRelations?.where?.tags
|
||||
|
||||
@@ -58,8 +51,8 @@ export class ProductRepository extends Repository<Product> {
|
||||
optionsWithoutRelations?.where?.discount_condition_id
|
||||
delete optionsWithoutRelations?.where?.discount_condition_id
|
||||
|
||||
const qb = this.createQueryBuilder("product")
|
||||
.select(["product.id"])
|
||||
const qb = this.createQueryBuilder(productAlias)
|
||||
.select([`${productAlias}.id`])
|
||||
.skip(optionsWithoutRelations.skip)
|
||||
.take(optionsWithoutRelations.take)
|
||||
|
||||
@@ -67,38 +60,26 @@ export class ProductRepository extends Repository<Product> {
|
||||
qb.where(optionsWithoutRelations.where)
|
||||
}
|
||||
|
||||
if (optionsWithoutRelations.order) {
|
||||
const toSelect: string[] = []
|
||||
const parsed = Object.entries(optionsWithoutRelations.order).reduce(
|
||||
(acc, [k, v]) => {
|
||||
const key = `product.${k}`
|
||||
toSelect.push(key)
|
||||
acc[key] = v
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
qb.addSelect(toSelect)
|
||||
qb.orderBy(parsed)
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
qb.leftJoin("product.tags", "tags").andWhere(`tags.id IN (:...tag_ids)`, {
|
||||
tag_ids: tags.value,
|
||||
})
|
||||
qb.leftJoin(`${productAlias}.tags`, "tags").andWhere(
|
||||
`tags.id IN (:...tag_ids)`,
|
||||
{
|
||||
tag_ids: tags.value,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (price_lists) {
|
||||
qb.leftJoin("product.variants", "variants")
|
||||
.leftJoin("variants.prices", "ma")
|
||||
.andWhere("ma.price_list_id IN (:...price_list_ids)", {
|
||||
qb.leftJoin(`${productAlias}.variants`, "variants")
|
||||
.leftJoin("variants.prices", "prices")
|
||||
.andWhere("prices.price_list_id IN (:...price_list_ids)", {
|
||||
price_list_ids: price_lists.value,
|
||||
})
|
||||
}
|
||||
|
||||
if (sales_channels) {
|
||||
qb.innerJoin(
|
||||
"product.sales_channels",
|
||||
`${productAlias}.sales_channels`,
|
||||
"sales_channels",
|
||||
"sales_channels.id IN (:...sales_channels_ids)",
|
||||
{ sales_channels_ids: sales_channels.value }
|
||||
@@ -109,11 +90,20 @@ export class ProductRepository extends Repository<Product> {
|
||||
qb.innerJoin(
|
||||
"discount_condition_product",
|
||||
"dc_product",
|
||||
`dc_product.product_id = product.id AND dc_product.condition_id = :dcId`,
|
||||
`dc_product.product_id = ${productAlias}.id AND dc_product.condition_id = :dcId`,
|
||||
{ dcId: discount_condition_id }
|
||||
)
|
||||
}
|
||||
|
||||
const joinedWithPriceLists = !!price_lists
|
||||
applyOrdering({
|
||||
repository: this,
|
||||
order: optionsWithoutRelations.order ?? {},
|
||||
qb,
|
||||
alias: productAlias,
|
||||
shouldJoin: (relation) => relation !== "prices" || !joinedWithPriceLists,
|
||||
})
|
||||
|
||||
if (optionsWithoutRelations.withDeleted) {
|
||||
qb.withDeleted()
|
||||
}
|
||||
@@ -151,7 +141,8 @@ export class ProductRepository extends Repository<Product> {
|
||||
entityIds: string[],
|
||||
groupedRelations: { [toplevel: string]: string[] },
|
||||
withDeleted = false,
|
||||
select: (keyof Product)[] = []
|
||||
select: (keyof Product)[] = [],
|
||||
order: { [column: string]: "ASC" | "DESC" } = {}
|
||||
): Promise<Product[]> {
|
||||
const entitiesIdsWithRelations = await Promise.all(
|
||||
Object.entries(groupedRelations).map(async ([toplevel, rels]) => {
|
||||
@@ -162,15 +153,13 @@ export class ProductRepository extends Repository<Product> {
|
||||
}
|
||||
|
||||
if (toplevel === "variants") {
|
||||
querybuilder = querybuilder
|
||||
.leftJoinAndSelect(
|
||||
`products.${toplevel}`,
|
||||
toplevel,
|
||||
"variants.deleted_at IS NULL"
|
||||
)
|
||||
.orderBy({
|
||||
"variants.variant_rank": "ASC",
|
||||
})
|
||||
querybuilder = querybuilder.leftJoinAndSelect(
|
||||
`products.${toplevel}`,
|
||||
toplevel,
|
||||
"variants.deleted_at IS NULL"
|
||||
)
|
||||
|
||||
order["variants.variant_rank"] = "ASC"
|
||||
} else {
|
||||
querybuilder = querybuilder.leftJoinAndSelect(
|
||||
`products.${toplevel}`,
|
||||
@@ -251,12 +240,14 @@ export class ProductRepository extends Repository<Product> {
|
||||
entitiesIds,
|
||||
groupedRelations,
|
||||
idsOrOptionsWithoutRelations.withDeleted,
|
||||
idsOrOptionsWithoutRelations.select
|
||||
idsOrOptionsWithoutRelations.select,
|
||||
idsOrOptionsWithoutRelations.order
|
||||
)
|
||||
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
const entitiesToReturn =
|
||||
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
||||
const entitiesAndRelations = groupBy(entitiesIdsWithRelations, "id")
|
||||
const entitiesToReturn = map(entitiesIds, (id) =>
|
||||
merge({}, ...entitiesAndRelations[id])
|
||||
)
|
||||
|
||||
return [entitiesToReturn, count]
|
||||
}
|
||||
@@ -353,6 +344,12 @@ export class ProductRepository extends Repository<Product> {
|
||||
options: FindWithoutRelationsOptions = { where: {} },
|
||||
relations: string[] = []
|
||||
): Promise<[Product[], number]> {
|
||||
const productAlias = "product"
|
||||
const pricesAlias = "prices"
|
||||
const variantsAlias = "variants"
|
||||
const collectionAlias = "collection"
|
||||
const tagsAlias = "tags"
|
||||
|
||||
const tags = options.where.tags
|
||||
delete options.where.tags
|
||||
|
||||
@@ -367,18 +364,18 @@ export class ProductRepository extends Repository<Product> {
|
||||
|
||||
const cleanedOptions = this._cleanOptions(options)
|
||||
|
||||
let qb = this.createQueryBuilder("product")
|
||||
.leftJoinAndSelect("product.variants", "variant")
|
||||
.leftJoinAndSelect("product.collection", "collection")
|
||||
.select(["product.id"])
|
||||
let qb = this.createQueryBuilder(`${productAlias}`)
|
||||
.leftJoinAndSelect(`${productAlias}.variants`, variantsAlias)
|
||||
.leftJoinAndSelect(`${productAlias}.collection`, `${collectionAlias}`)
|
||||
.select([`${productAlias}.id`])
|
||||
.where(cleanedOptions.where)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`product.description ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`product.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`collection.title ILIKE :q`, { q: `%${q}%` })
|
||||
qb.where(`${productAlias}.description ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`${productAlias}.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`${variantsAlias}.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`${variantsAlias}.sku ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`${collectionAlias}.title ILIKE :q`, { q: `%${q}%` })
|
||||
})
|
||||
)
|
||||
.skip(cleanedOptions.skip)
|
||||
@@ -388,47 +385,72 @@ export class ProductRepository extends Repository<Product> {
|
||||
qb.innerJoin(
|
||||
"discount_condition_product",
|
||||
"dc_product",
|
||||
`dc_product.product_id = product.id AND dc_product.condition_id = :dcId`,
|
||||
`dc_product.product_id = ${productAlias}.id AND dc_product.condition_id = :dcId`,
|
||||
{ dcId: discount_condition_id }
|
||||
)
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
qb.leftJoin("product.tags", "tags").andWhere(`tags.id IN (:...tag_ids)`, {
|
||||
tag_ids: tags.value,
|
||||
})
|
||||
qb.leftJoin(`${productAlias}.tags`, tagsAlias).andWhere(
|
||||
`${tagsAlias}.id IN (:...tag_ids)`,
|
||||
{
|
||||
tag_ids: tags.value,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (price_lists) {
|
||||
qb.leftJoin("product.variants", "variants")
|
||||
.leftJoin("variants.prices", "ma")
|
||||
.andWhere("ma.price_list_id IN (:...price_list_ids)", {
|
||||
const variantPricesAlias = `${variantsAlias}_prices`
|
||||
qb.leftJoin(`${productAlias}.variants`, variantPricesAlias)
|
||||
.leftJoin(`${variantPricesAlias}.prices`, pricesAlias)
|
||||
.andWhere(`${pricesAlias}.price_list_id IN (:...price_list_ids)`, {
|
||||
price_list_ids: price_lists.value,
|
||||
})
|
||||
}
|
||||
|
||||
if (sales_channels) {
|
||||
qb.innerJoin(
|
||||
"product.sales_channels",
|
||||
`${productAlias}.sales_channels`,
|
||||
"sales_channels",
|
||||
"sales_channels.id IN (:...sales_channels_ids)",
|
||||
{ sales_channels_ids: sales_channels.value }
|
||||
)
|
||||
}
|
||||
|
||||
const joinedWithTags = !!tags
|
||||
const joinedWithPriceLists = !!price_lists
|
||||
applyOrdering({
|
||||
repository: this,
|
||||
order: options.order ?? {},
|
||||
qb,
|
||||
alias: productAlias,
|
||||
shouldJoin: (relation) =>
|
||||
relation !== variantsAlias &&
|
||||
(relation !== pricesAlias || !joinedWithPriceLists) &&
|
||||
(relation !== tagsAlias || !joinedWithTags),
|
||||
})
|
||||
|
||||
if (cleanedOptions.withDeleted) {
|
||||
qb = qb.withDeleted()
|
||||
}
|
||||
|
||||
const [results, count] = await qb.getManyAndCount()
|
||||
const orderedResultsSet = new Set(results.map((p) => p.id))
|
||||
|
||||
const products = await this.findWithRelations(
|
||||
relations,
|
||||
results.map((r) => r.id),
|
||||
[...orderedResultsSet],
|
||||
cleanedOptions.withDeleted
|
||||
)
|
||||
const productsMap = new Map(products.map((p) => [p.id, p]))
|
||||
|
||||
return [products, count]
|
||||
// Looping through the orderedResultsSet in order to maintain the original order and assign the data returned by findWithRelations
|
||||
const orderedProducts: Product[] = []
|
||||
orderedResultsSet.forEach((id) => {
|
||||
orderedProducts.push(productsMap.get(id)!)
|
||||
})
|
||||
|
||||
return [orderedProducts, count]
|
||||
}
|
||||
|
||||
public async isProductInSalesChannels(
|
||||
|
||||
@@ -3,10 +3,7 @@ import { IdMap, MockManager } from "medusa-test-utils"
|
||||
import { User } from "../../../../models"
|
||||
import { BatchJobStatus } from "../../../../types/batch-job"
|
||||
import { productsToExport } from "../../../__fixtures__/product-export-data"
|
||||
import {
|
||||
AdminPostBatchesReq,
|
||||
defaultAdminProductRelations,
|
||||
} from "../../../../api"
|
||||
import { AdminPostBatchesReq, defaultAdminProductRelations, } from "../../../../api"
|
||||
import { ProductExportBatchJob } from "../../../batch-jobs/product/types"
|
||||
import { Request } from "express"
|
||||
import { FlagRouter } from "../../../../utils/flag-router"
|
||||
|
||||
@@ -50,7 +50,7 @@ export function getListConfig<TModel extends BaseEntity>(
|
||||
expand?: string[],
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
order?: { [k: symbol]: "DESC" | "ASC" }
|
||||
order: { [k: string | symbol]: "DESC" | "ASC" } = {}
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (isDefined(fields)) {
|
||||
@@ -66,8 +66,10 @@ export function getListConfig<TModel extends BaseEntity>(
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
const orderBy: Record<string, "DESC" | "ASC"> = order ?? {
|
||||
created_at: "DESC",
|
||||
const orderBy = order
|
||||
|
||||
if (!Object.keys(order).length) {
|
||||
orderBy["created_at"] = "DESC"
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { flatten, groupBy, map, merge } from "lodash"
|
||||
import { Repository, SelectQueryBuilder } from "typeorm"
|
||||
import { EntityMetadata, Repository, SelectQueryBuilder } from "typeorm"
|
||||
import { FindWithoutRelationsOptions } from "../repositories/customer-group"
|
||||
|
||||
// TODO: All the utilities except applyOrdering needs to be re worked depending on the outcome of the product repository
|
||||
|
||||
/**
|
||||
* Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs.
|
||||
* Allow to query the relations for the specified entity ids
|
||||
@@ -163,3 +165,80 @@ export function mergeEntitiesWithRelations<T>(
|
||||
merge({}, ...entityAndRelations)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the appropriate order depending on the requirements
|
||||
* @param repository
|
||||
* @param order The field on which to apply the order (e.g { "variants.prices.amount": "DESC" })
|
||||
* @param qb
|
||||
* @param alias
|
||||
* @param shouldJoin In case a join is already applied elsewhere and therefore you want to avoid to re joining the data in that case you can return false for specific relations
|
||||
*/
|
||||
export function applyOrdering<T>({
|
||||
repository,
|
||||
order,
|
||||
qb,
|
||||
alias,
|
||||
shouldJoin,
|
||||
}: {
|
||||
repository: Repository<T>
|
||||
order: Record<string, "ASC" | "DESC">
|
||||
qb: SelectQueryBuilder<T>
|
||||
alias: string
|
||||
shouldJoin: (relation: string) => boolean
|
||||
}) {
|
||||
const toSelect: string[] = []
|
||||
|
||||
const parsed = Object.entries(order).reduce(
|
||||
(acc, [orderPath, orderDirection]) => {
|
||||
// If the orderPath (e.g variants.prices.amount) includes a point it means that it is to access
|
||||
// a child relation of an unknown depth
|
||||
if (orderPath.includes(".")) {
|
||||
// We are spliting the path and separating the relations from the property to order. (e.g relations ["variants", "prices"] and property "amount"
|
||||
const relationsToJoin = orderPath.split(".")
|
||||
const propToOrder = relationsToJoin.pop()
|
||||
|
||||
// For each relation we will retrieve the metadata in order to use the right property name from the relation registered in the entity.
|
||||
// Each time we will return the child (i.e the relation) and the inverse metadata (corresponding to the child metadata from the parent point of view)
|
||||
// In order for the next child to know its parent
|
||||
relationsToJoin.reduce(
|
||||
([parent, parentMetadata], child) => {
|
||||
// Find the relation metadata from the parent entity
|
||||
const relationMetadata = (
|
||||
parentMetadata as EntityMetadata
|
||||
).relations.find(
|
||||
(relationMetadata) => relationMetadata.propertyName === child
|
||||
)
|
||||
|
||||
// The consumer can refuse to apply a join on a relation if the join has already been applied before calling this util
|
||||
const shouldApplyJoin = shouldJoin(child)
|
||||
if (shouldApplyJoin) {
|
||||
qb.leftJoin(`${parent}.${relationMetadata!.propertyPath}`, child)
|
||||
}
|
||||
|
||||
// Return the child relation to be the parent for the next one, as well as the metadata corresponding the child in order
|
||||
// to find the next relation metadata for the next child
|
||||
return [child, relationMetadata!.inverseEntityMetadata]
|
||||
},
|
||||
[alias, repository.metadata]
|
||||
)
|
||||
|
||||
// The key for variants.prices.amount will be "prices.amount" since we are ordering on the join added to its parent "variants" in this example
|
||||
const key = `${
|
||||
relationsToJoin[relationsToJoin.length - 1]
|
||||
}.${propToOrder}`
|
||||
acc[key] = orderDirection
|
||||
toSelect.push(key)
|
||||
return acc
|
||||
}
|
||||
|
||||
const key = `${alias}.${orderPath}`
|
||||
toSelect.push(key)
|
||||
acc[key] = orderDirection
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
qb.addSelect(toSelect)
|
||||
qb.orderBy(parsed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user