Fix/count variants (#1020)
* fix: adds listAndCount to product variants * fix: adds list and count with relations * lint fix * Update packages/medusa/src/services/product-variant.ts Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * Update packages/medusa/src/services/product-variant.ts Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -44,7 +44,7 @@ export default async (req, res) => {
|
||||
const selector: FilterableProductVariantProps = {}
|
||||
|
||||
if ("q" in req.query) {
|
||||
selector.q = req.query.q
|
||||
selector.q = q
|
||||
}
|
||||
|
||||
const listConfig: FindConfig<ProductVariant> = {
|
||||
@@ -54,9 +54,12 @@ export default async (req, res) => {
|
||||
take: limit,
|
||||
}
|
||||
|
||||
const variants = await variantService.list(selector, listConfig)
|
||||
const [variants, count] = await variantService.listAndCount(
|
||||
selector,
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.json({ variants, count: variants.length, offset, limit })
|
||||
res.json({ variants, count, offset, limit })
|
||||
}
|
||||
|
||||
export class AdminGetVariantsParams {
|
||||
|
||||
@@ -1,5 +1,238 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import {
|
||||
EntityRepository,
|
||||
FindConditions,
|
||||
FindManyOptions,
|
||||
FindOperator,
|
||||
OrderByCondition,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import { flatten, groupBy, map, merge } from "lodash"
|
||||
import { ProductVariant } from "../models/product-variant"
|
||||
|
||||
export type FindWithRelationsOptions = FindManyOptions<ProductVariant> & {
|
||||
order?: OrderByCondition
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
@EntityRepository(ProductVariant)
|
||||
export class ProductVariantRepository extends Repository<ProductVariant> {}
|
||||
export class ProductVariantRepository extends Repository<ProductVariant> {
|
||||
private mergeEntitiesWithRelations(
|
||||
entitiesAndRelations: Array<Partial<ProductVariant>>
|
||||
): ProductVariant[] {
|
||||
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
||||
return map(entitiesAndRelationsById, (entityAndRelations) =>
|
||||
merge({}, ...entityAndRelations)
|
||||
)
|
||||
}
|
||||
|
||||
private async queryProductsVariants(
|
||||
optionsWithoutRelations: FindWithRelationsOptions,
|
||||
shouldCount = false
|
||||
): Promise<[ProductVariant[], number]> {
|
||||
let qb = this.createQueryBuilder("pv")
|
||||
.select(["pv.id"])
|
||||
.skip(optionsWithoutRelations.skip)
|
||||
.take(optionsWithoutRelations.take)
|
||||
|
||||
qb = optionsWithoutRelations.where
|
||||
? qb.where(optionsWithoutRelations.where)
|
||||
: qb
|
||||
|
||||
qb = optionsWithoutRelations.order
|
||||
? qb.orderBy(optionsWithoutRelations.order)
|
||||
: qb
|
||||
|
||||
if (optionsWithoutRelations.withDeleted) {
|
||||
qb = qb.withDeleted()
|
||||
}
|
||||
|
||||
let entities: ProductVariant[]
|
||||
let count = 0
|
||||
if (shouldCount) {
|
||||
const result = await qb.getManyAndCount()
|
||||
entities = result[0]
|
||||
count = result[1]
|
||||
} else {
|
||||
entities = await qb.getMany()
|
||||
}
|
||||
|
||||
return [entities, count]
|
||||
}
|
||||
|
||||
private getGroupedRelations(relations: Array<keyof ProductVariant>): {
|
||||
[toplevel: string]: string[]
|
||||
} {
|
||||
const groupedRelations: { [toplevel: string]: string[] } = {}
|
||||
for (const rel of relations) {
|
||||
const [topLevel] = rel.split(".")
|
||||
if (groupedRelations[topLevel]) {
|
||||
groupedRelations[topLevel].push(rel)
|
||||
} else {
|
||||
groupedRelations[topLevel] = [rel]
|
||||
}
|
||||
}
|
||||
|
||||
return groupedRelations
|
||||
}
|
||||
|
||||
private async queryProductVariantsWithIds(
|
||||
entityIds: string[],
|
||||
groupedRelations: { [toplevel: string]: string[] },
|
||||
withDeleted = false
|
||||
): Promise<ProductVariant[]> {
|
||||
const entitiesIdsWithRelations = await Promise.all(
|
||||
Object.entries(groupedRelations).map(([toplevel, rels]) => {
|
||||
let querybuilder = this.createQueryBuilder("pv")
|
||||
querybuilder = querybuilder.leftJoinAndSelect(
|
||||
`pv.${toplevel}`,
|
||||
toplevel
|
||||
)
|
||||
|
||||
for (const rel of rels) {
|
||||
const [_, rest] = rel.split(".")
|
||||
if (!rest) {
|
||||
continue
|
||||
}
|
||||
// Regex matches all '.' except the rightmost
|
||||
querybuilder = querybuilder.leftJoinAndSelect(
|
||||
rel.replace(/\.(?=[^.]*\.)/g, "__"),
|
||||
rel.replace(".", "__")
|
||||
)
|
||||
}
|
||||
|
||||
if (withDeleted) {
|
||||
querybuilder = querybuilder
|
||||
.where("pv.id IN (:...entitiesIds)", {
|
||||
entitiesIds: entityIds,
|
||||
})
|
||||
.withDeleted()
|
||||
} else {
|
||||
querybuilder = querybuilder.where(
|
||||
"pv.deleted_at IS NULL AND pv.id IN (:...entitiesIds)",
|
||||
{
|
||||
entitiesIds: entityIds,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return querybuilder.getMany()
|
||||
})
|
||||
).then(flatten)
|
||||
|
||||
return entitiesIdsWithRelations
|
||||
}
|
||||
|
||||
public async findWithRelationsAndCount(
|
||||
relations: string[] = [],
|
||||
idsOrOptionsWithoutRelations: FindWithRelationsOptions | string[] = {
|
||||
where: {},
|
||||
},
|
||||
withDeleted?: boolean
|
||||
): Promise<[ProductVariant[], number]> {
|
||||
let count: number
|
||||
let entities: ProductVariant[]
|
||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||
entities = await this.findByIds(idsOrOptionsWithoutRelations, {
|
||||
withDeleted: withDeleted ?? false,
|
||||
})
|
||||
count = entities.length
|
||||
} else {
|
||||
const result = await this.queryProductsVariants(
|
||||
idsOrOptionsWithoutRelations,
|
||||
true
|
||||
)
|
||||
entities = result[0]
|
||||
count = result[1]
|
||||
}
|
||||
const entitiesIds = entities.map(({ id }) => id)
|
||||
|
||||
if (entitiesIds.length === 0) {
|
||||
// no need to continue
|
||||
return [[], count]
|
||||
}
|
||||
|
||||
if (relations.length === 0) {
|
||||
const toReturn = await this.findByIds(
|
||||
entitiesIds,
|
||||
idsOrOptionsWithoutRelations as FindConditions<ProductVariant>
|
||||
)
|
||||
return [toReturn, toReturn.length]
|
||||
}
|
||||
|
||||
const groupedRelations = this.getGroupedRelations(
|
||||
relations as (keyof ProductVariant)[]
|
||||
)
|
||||
const entitiesIdsWithRelations = await this.queryProductVariantsWithIds(
|
||||
entitiesIds,
|
||||
groupedRelations,
|
||||
withDeleted
|
||||
)
|
||||
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
const entitiesToReturn =
|
||||
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
||||
|
||||
return [entitiesToReturn, count]
|
||||
}
|
||||
|
||||
public async findWithRelations(
|
||||
relations: string[] = [],
|
||||
idsOrOptionsWithoutRelations: FindWithRelationsOptions | string[] = {},
|
||||
withDeleted = false
|
||||
): Promise<ProductVariant[]> {
|
||||
let entities: ProductVariant[]
|
||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||
entities = await this.findByIds(idsOrOptionsWithoutRelations, {
|
||||
withDeleted,
|
||||
})
|
||||
} else {
|
||||
const result = await this.queryProductsVariants(
|
||||
idsOrOptionsWithoutRelations,
|
||||
false
|
||||
)
|
||||
entities = result[0]
|
||||
}
|
||||
const entitiesIds = entities.map(({ id }) => id)
|
||||
|
||||
if (entitiesIds.length === 0) {
|
||||
// no need to continue
|
||||
return []
|
||||
}
|
||||
|
||||
if (relations.length === 0) {
|
||||
return await this.findByIds(
|
||||
entitiesIds,
|
||||
idsOrOptionsWithoutRelations as FindConditions<ProductVariant>
|
||||
)
|
||||
}
|
||||
|
||||
const groupedRelations = this.getGroupedRelations(
|
||||
relations as (keyof ProductVariant)[]
|
||||
)
|
||||
const entitiesIdsWithRelations = await this.queryProductVariantsWithIds(
|
||||
entitiesIds,
|
||||
groupedRelations,
|
||||
withDeleted
|
||||
)
|
||||
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
const entitiesToReturn =
|
||||
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
||||
|
||||
return entitiesToReturn
|
||||
}
|
||||
|
||||
public async findOneWithRelations(
|
||||
relations: Array<keyof ProductVariant> = [],
|
||||
optionsWithoutRelations: FindWithRelationsOptions = { where: {} }
|
||||
): Promise<ProductVariant> {
|
||||
// Limit 1
|
||||
optionsWithoutRelations.take = 1
|
||||
|
||||
const result = await this.findWithRelations(
|
||||
relations,
|
||||
optionsWithoutRelations
|
||||
)
|
||||
return result[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager, ILike, IsNull, SelectQueryBuilder } from "typeorm"
|
||||
import {
|
||||
Brackets,
|
||||
EntityManager,
|
||||
ILike,
|
||||
IsNull,
|
||||
SelectQueryBuilder,
|
||||
} from "typeorm"
|
||||
import { MoneyAmount } from "../models/money-amount"
|
||||
import { Product } from "../models/product"
|
||||
import { ProductOptionValue } from "../models/product-option-value"
|
||||
@@ -8,7 +14,10 @@ import { ProductVariant } from "../models/product-variant"
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
import { ProductRepository } from "../repositories/product"
|
||||
import { ProductOptionValueRepository } from "../repositories/product-option-value"
|
||||
import { ProductVariantRepository } from "../repositories/product-variant"
|
||||
import {
|
||||
FindWithRelationsOptions,
|
||||
ProductVariantRepository,
|
||||
} from "../repositories/product-variant"
|
||||
import EventBusService from "../services/event-bus"
|
||||
import RegionService from "../services/region"
|
||||
import { FindConfig } from "../types/common"
|
||||
@@ -542,6 +551,36 @@ class ProductVariantService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} selector - the query object for find
|
||||
* @param {FindConfig<ProductVariant>} config - query config object for variant retrieval
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async listAndCount(
|
||||
selector: FilterableProductVariantProps,
|
||||
config: FindConfig<ProductVariant> = { relations: [], skip: 0, take: 20 }
|
||||
): Promise<[ProductVariant[], number]> {
|
||||
const variantRepo = this.manager_.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
|
||||
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||
|
||||
if (q) {
|
||||
const qb = this.getFreeTextQueryBuilder_(variantRepo, query, q)
|
||||
const [raw, count] = await qb.getManyAndCount()
|
||||
|
||||
const variants = await variantRepo.findWithRelations(
|
||||
relations,
|
||||
raw.map((i) => i.id),
|
||||
query.withDeleted ?? false
|
||||
)
|
||||
return [variants, count]
|
||||
}
|
||||
|
||||
return await variantRepo.findWithRelationsAndCount(relations, query)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FilterableProductVariantProps} selector - the query object for find
|
||||
* @param {FindConfig<ProductVariant>} config - query config object for variant retrieval
|
||||
@@ -648,6 +687,87 @@ class ProductVariantService extends BaseService {
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query object to be used for list queries.
|
||||
* @param {object} selector - the selector to create the query from
|
||||
* @param {object} config - the config to use for the query
|
||||
* @return {object} an object containing the query, relations and free-text
|
||||
* search param.
|
||||
*/
|
||||
prepareListQuery_(
|
||||
selector: FilterableProductVariantProps,
|
||||
config: FindConfig<ProductVariant>
|
||||
): { query: FindWithRelationsOptions; relations: string[]; q?: string } {
|
||||
let q: string | undefined
|
||||
if (typeof selector.q !== "undefined") {
|
||||
q = selector.q
|
||||
delete selector.q
|
||||
}
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
query.relations = config.relations
|
||||
}
|
||||
|
||||
if (config.select && config.select.length > 0) {
|
||||
query.select = config.select
|
||||
}
|
||||
|
||||
const rels = query.relations
|
||||
delete query.relations
|
||||
|
||||
return {
|
||||
query,
|
||||
relations: rels,
|
||||
q,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists variants based on the provided parameters and includes the count of
|
||||
* variants that match the query.
|
||||
* @param {object} variantRepo - the variant repository
|
||||
* @param {object} query - object that defines the scope for what should be returned
|
||||
* @param {object} q - free text query
|
||||
* @return {Promise<[ProductVariant[], number]>} an array containing the products as the first element and the total
|
||||
* count of products that matches the query as the second element.
|
||||
*/
|
||||
getFreeTextQueryBuilder_(
|
||||
variantRepo: ProductVariantRepository,
|
||||
query: FindWithRelationsOptions,
|
||||
q?: string
|
||||
): SelectQueryBuilder<ProductVariant> {
|
||||
const where = query.where
|
||||
|
||||
if (typeof where === "object") {
|
||||
if ("title" in where) {
|
||||
delete where.title
|
||||
}
|
||||
}
|
||||
|
||||
let qb = variantRepo
|
||||
.createQueryBuilder("pv")
|
||||
.take(query.take)
|
||||
.skip(Math.max(query.skip ?? 0, 0))
|
||||
.leftJoinAndSelect("pv.product", "product")
|
||||
.select(["pv.id"])
|
||||
.where(where!)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`product.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`pv.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`pv.sku ILIKE :q`, { q: `%${q}%` })
|
||||
})
|
||||
)
|
||||
|
||||
if (query.withDeleted) {
|
||||
qb = qb.withDeleted()
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariantService
|
||||
|
||||
Reference in New Issue
Block a user