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:
Sebastian Rindom
2022-02-02 19:29:06 +01:00
committed by GitHub
parent 2e384842d5
commit a81227fa74
3 changed files with 363 additions and 7 deletions

View File

@@ -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 {

View File

@@ -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]
}
}

View File

@@ -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