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:
1 parent
2e384842d5
commit
a81227fa74
3 files changed
+363
-7
No files matched your search
@@ -44,7 +44,7 @@ export default async (req, res) => {
|
|||||||
const selector: FilterableProductVariantProps = {}
|
const selector: FilterableProductVariantProps = {}
|
||||||
|
|
||||||
if ("q" in req.query) {
|
if ("q" in req.query) {
|
||||||
selector.q = req.query.q
|
selector.q = q
|
||||||
}
|
}
|
||||||
|
|
||||||
const listConfig: FindConfig<ProductVariant> = {
|
const listConfig: FindConfig<ProductVariant> = {
|
||||||
@@ -54,9 +54,12 @@ export default async (req, res) => {
|
|||||||
take: limit,
|
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 {
|
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"
|
import { ProductVariant } from "../models/product-variant"
|
||||||
|
|
||||||
|
export type FindWithRelationsOptions = FindManyOptions<ProductVariant> & {
|
||||||
|
order?: OrderByCondition
|
||||||
|
withDeleted?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
@EntityRepository(ProductVariant)
|
@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 { MedusaError } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
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 { MoneyAmount } from "../models/money-amount"
|
||||||
import { Product } from "../models/product"
|
import { Product } from "../models/product"
|
||||||
import { ProductOptionValue } from "../models/product-option-value"
|
import { ProductOptionValue } from "../models/product-option-value"
|
||||||
@@ -8,7 +14,10 @@ import { ProductVariant } from "../models/product-variant"
|
|||||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||||
import { ProductRepository } from "../repositories/product"
|
import { ProductRepository } from "../repositories/product"
|
||||||
import { ProductOptionValueRepository } from "../repositories/product-option-value"
|
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 EventBusService from "../services/event-bus"
|
||||||
import RegionService from "../services/region"
|
import RegionService from "../services/region"
|
||||||
import { FindConfig } from "../types/common"
|
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 {FilterableProductVariantProps} selector - the query object for find
|
||||||
* @param {FindConfig<ProductVariant>} config - query config object for variant retrieval
|
* @param {FindConfig<ProductVariant>} config - query config object for variant retrieval
|
||||||
@@ -648,6 +687,87 @@ class ProductVariantService extends BaseService {
|
|||||||
|
|
||||||
return updated
|
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
|
export default ProductVariantService
|
||||||
Reference in new issue
Block a user