fix(medusa): Product repo typeorm issues (#4084)
* fix(medusa): Product repo typeorm issues * chore: fixed category scopes * WIP fix categories * fix product repo to attach categories * fix uni tests * Create eighty-icons-exercise.md * revert package.json * fix change set * last fixes * cleanup iteration * fix repository deep relations joining aliasing * improve response time * improve category test case * fix free texts search * fix repo * centralise repository manipulation into utils and use the utils in the product repo * fix product repo * fix customer group * update changeset * fix customer group * include feedback * fix repo * remove query strategy --------- Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
committed by
GitHub
parent
26963acc0a
commit
9518efccae
5
.changeset/eighty-icons-exercise.md
Normal file
5
.changeset/eighty-icons-exercise.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): Revert product repo to prevent typeorm issues + cleanup and improvements
|
||||
@@ -1,27 +1,14 @@
|
||||
const path = require("path")
|
||||
const { ProductCategory } = require("@medusajs/medusa")
|
||||
const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist")
|
||||
const { IdMap } = require("medusa-test-utils")
|
||||
const {
|
||||
ProductVariant,
|
||||
ProductOptionValue,
|
||||
MoneyAmount,
|
||||
DiscountConditionType,
|
||||
DiscountConditionOperator,
|
||||
} = require("@medusajs/medusa")
|
||||
|
||||
const setupServer = require("../../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||
const productSeeder = require("../../../helpers/product-seeder")
|
||||
const priceListSeeder = require("../../../helpers/price-list-seeder")
|
||||
const {
|
||||
simpleProductFactory,
|
||||
simpleDiscountFactory,
|
||||
simpleProductCategoryFactory,
|
||||
simpleSalesChannelFactory,
|
||||
simpleRegionFactory,
|
||||
} = require("../../../factories")
|
||||
|
||||
const testProductId = "test-product"
|
||||
@@ -124,10 +111,7 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
|
||||
it("returns a list of products in product category without category children", async () => {
|
||||
const api = useApi()
|
||||
const params = `category_id[]=${categoryWithProductId}`
|
||||
const response = await api.get(
|
||||
`/admin/products?${params}`,
|
||||
adminHeaders
|
||||
)
|
||||
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toHaveLength(1)
|
||||
@@ -141,10 +125,7 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
|
||||
it("returns a list of products in product category without category children explicitly set to false", async () => {
|
||||
const api = useApi()
|
||||
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
|
||||
const response = await api.get(
|
||||
`/admin/products?${params}`,
|
||||
adminHeaders
|
||||
)
|
||||
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toHaveLength(1)
|
||||
@@ -159,10 +140,7 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
|
||||
const api = useApi()
|
||||
|
||||
const params = `category_id[]=${categoryWithProductId}&include_category_children=true`
|
||||
const response = await api.get(
|
||||
`/admin/products?${params}`,
|
||||
adminHeaders
|
||||
)
|
||||
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toHaveLength(3)
|
||||
@@ -185,10 +163,7 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
|
||||
const api = useApi()
|
||||
|
||||
const params = `category_id[]=${categoryWithoutProductId}&include_category_children=true`
|
||||
const response = await api.get(
|
||||
`/admin/products?${params}`,
|
||||
adminHeaders
|
||||
)
|
||||
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toHaveLength(0)
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("/store/products", () => {
|
||||
internalCategoryWithProduct = await simpleProductCategoryFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: inactiveCategoryWithProductId,
|
||||
id: internalCategoryWithProductId,
|
||||
name: "inactive category with Product",
|
||||
products: [{ id: testProductFilteringId2 }],
|
||||
parent_category: nestedCategoryWithProduct,
|
||||
@@ -219,6 +219,51 @@ describe("/store/products", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("returns only active and public products with include_category_children when categories are expanded", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const params = `id[]=${testProductFilteringId2}&expand=categories`
|
||||
let response = await api.get(`/store/products?${params}`)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toHaveLength(1)
|
||||
|
||||
expect(response.data.products).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: testProductFilteringId2,
|
||||
categories: [],
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
const category = await simpleProductCategoryFactory(dbConnection, {
|
||||
id: categoryWithProductId,
|
||||
name: "category with Product 2",
|
||||
products: [{ id: response.data.products[0].id }],
|
||||
is_active: true,
|
||||
is_internal: false,
|
||||
})
|
||||
|
||||
response = await api.get(`/store/products?${params}`)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toHaveLength(1)
|
||||
|
||||
expect(response.data.products).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: testProductFilteringId2,
|
||||
categories: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: category.id,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("does not query products with category that are inactive", async () => {
|
||||
const api = useApi()
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
DeleteResult,
|
||||
FindOperator,
|
||||
FindOptionsRelations,
|
||||
In,
|
||||
SelectQueryBuilder,
|
||||
} from "typeorm"
|
||||
import { DeleteResult, FindOperator, FindOptionsRelations, In } from "typeorm"
|
||||
import { CustomerGroup } from "../models"
|
||||
import { ExtendedFindConfig } from "../types/common"
|
||||
import {
|
||||
@@ -15,6 +9,7 @@ import {
|
||||
} from "../utils/repository"
|
||||
import { objectToStringPath } from "@medusajs/utils"
|
||||
import { dataSource } from "../loaders/database"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
export type DefaultWithoutRelations = Omit<
|
||||
ExtendedFindConfig<CustomerGroup>,
|
||||
@@ -71,45 +66,66 @@ export const CustomerGroupRepository = dataSource
|
||||
|
||||
async findWithRelationsAndCount(
|
||||
relations: FindOptionsRelations<CustomerGroup> = {},
|
||||
idsOrOptionsWithoutRelations: FindWithoutRelationsOptions = { where: {} }
|
||||
idsOrOptionsWithoutRelations: string[] | FindWithoutRelationsOptions = {
|
||||
where: {},
|
||||
}
|
||||
): Promise<[CustomerGroup[], number]> {
|
||||
const withDeleted = Array.isArray(idsOrOptionsWithoutRelations)
|
||||
? false
|
||||
: idsOrOptionsWithoutRelations.withDeleted ?? false
|
||||
const isOptionsArray = Array.isArray(idsOrOptionsWithoutRelations)
|
||||
const originalWhere = isOptionsArray
|
||||
? undefined
|
||||
: cloneDeep(idsOrOptionsWithoutRelations.where)
|
||||
const originalOrder: any = isOptionsArray
|
||||
? undefined
|
||||
: { ...idsOrOptionsWithoutRelations.order }
|
||||
const originalSelect = isOptionsArray
|
||||
? undefined
|
||||
: objectToStringPath(idsOrOptionsWithoutRelations.select)
|
||||
const clonedOptions = isOptionsArray
|
||||
? idsOrOptionsWithoutRelations
|
||||
: cloneDeep(idsOrOptionsWithoutRelations)
|
||||
|
||||
let count: number
|
||||
let entities: CustomerGroup[]
|
||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||
entities = await this.find({
|
||||
where: { id: In(idsOrOptionsWithoutRelations) },
|
||||
withDeleted: idsOrOptionsWithoutRelations.withDeleted ?? false,
|
||||
withDeleted,
|
||||
})
|
||||
count = entities.length
|
||||
} else {
|
||||
const customJoinsBuilders: ((
|
||||
qb: SelectQueryBuilder<CustomerGroup>,
|
||||
alias: string
|
||||
) => void)[] = []
|
||||
const discountConditionId = (
|
||||
clonedOptions as FindWithoutRelationsOptions
|
||||
)?.where?.discount_condition_id
|
||||
delete (clonedOptions as FindWithoutRelationsOptions)?.where
|
||||
?.discount_condition_id
|
||||
|
||||
if (idsOrOptionsWithoutRelations?.where?.discount_condition_id) {
|
||||
const discountConditionId =
|
||||
idsOrOptionsWithoutRelations?.where?.discount_condition_id
|
||||
delete idsOrOptionsWithoutRelations?.where?.discount_condition_id
|
||||
const result = await queryEntityWithoutRelations({
|
||||
repository: this,
|
||||
optionsWithoutRelations: clonedOptions as FindWithoutRelationsOptions,
|
||||
shouldCount: true,
|
||||
customJoinBuilders: [
|
||||
async (qb, alias) => {
|
||||
if (discountConditionId) {
|
||||
qb.innerJoin(
|
||||
"discount_condition_customer_group",
|
||||
"dc_cg",
|
||||
`dc_cg.customer_group_id = ${alias}.id AND dc_cg.condition_id = :dcId`,
|
||||
{ dcId: discountConditionId }
|
||||
)
|
||||
|
||||
customJoinsBuilders.push(
|
||||
(qb: SelectQueryBuilder<CustomerGroup>, alias: string) => {
|
||||
qb.innerJoin(
|
||||
"discount_condition_customer_group",
|
||||
"dc_cg",
|
||||
`dc_cg.customer_group_id = ${alias}.id AND dc_cg.condition_id = :dcId`,
|
||||
{ dcId: discountConditionId }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
return {
|
||||
relation: "discount_condition",
|
||||
preventOrderJoin: true,
|
||||
}
|
||||
}
|
||||
|
||||
const result = await queryEntityWithoutRelations(
|
||||
this,
|
||||
idsOrOptionsWithoutRelations,
|
||||
true,
|
||||
customJoinsBuilders
|
||||
)
|
||||
return
|
||||
},
|
||||
],
|
||||
})
|
||||
entities = result[0]
|
||||
count = result[1]
|
||||
}
|
||||
@@ -121,16 +137,21 @@ export const CustomerGroupRepository = dataSource
|
||||
}
|
||||
|
||||
if (Object.keys(relations).length === 0) {
|
||||
const options = { ...idsOrOptionsWithoutRelations }
|
||||
|
||||
// Since we are finding by the ids that have been retrieved above and those ids are already
|
||||
// applying skip/take. Remove those options to avoid getting no results
|
||||
delete options.skip
|
||||
delete options.take
|
||||
if (!Array.isArray(clonedOptions)) {
|
||||
delete clonedOptions.skip
|
||||
delete clonedOptions.take
|
||||
}
|
||||
|
||||
const toReturn = await this.find({
|
||||
...options,
|
||||
where: { id: In(entitiesIds) },
|
||||
...(isOptionsArray
|
||||
? {}
|
||||
: (clonedOptions as FindWithoutRelationsOptions)),
|
||||
where: {
|
||||
id: In(entitiesIds),
|
||||
...(Array.isArray(clonedOptions) ? {} : clonedOptions.where),
|
||||
},
|
||||
})
|
||||
return [toReturn, toReturn.length]
|
||||
}
|
||||
@@ -138,16 +159,13 @@ export const CustomerGroupRepository = dataSource
|
||||
const legacyRelations = objectToStringPath(relations)
|
||||
const groupedRelations = getGroupedRelations(legacyRelations)
|
||||
|
||||
const legacySelect = objectToStringPath(
|
||||
idsOrOptionsWithoutRelations.select
|
||||
)
|
||||
const entitiesIdsWithRelations = await queryEntityWithIds(
|
||||
this,
|
||||
entitiesIds,
|
||||
const entitiesIdsWithRelations = await queryEntityWithIds({
|
||||
repository: this,
|
||||
entityIds: entitiesIds,
|
||||
groupedRelations,
|
||||
idsOrOptionsWithoutRelations.withDeleted,
|
||||
legacySelect
|
||||
)
|
||||
select: originalSelect,
|
||||
withDeleted,
|
||||
})
|
||||
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
const entitiesToReturn =
|
||||
|
||||
@@ -1,21 +1,299 @@
|
||||
import {
|
||||
Brackets,
|
||||
FindOperator,
|
||||
FindOptionsWhere,
|
||||
ILike,
|
||||
In,
|
||||
SelectQueryBuilder,
|
||||
} from "typeorm"
|
||||
import { Product, ProductCategory, ProductVariant } from "../models"
|
||||
import { ExtendedFindConfig } from "../types/common"
|
||||
import { dataSource } from "../loaders/database"
|
||||
import { ProductFilterOptions } from "../types/product"
|
||||
import {
|
||||
isObject,
|
||||
fetchCategoryDescendantsIds,
|
||||
} from "../utils"
|
||||
PriceList,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductTag,
|
||||
SalesChannel,
|
||||
} from "../models"
|
||||
import { dataSource } from "../loaders/database"
|
||||
import { cloneDeep, groupBy, map, merge } from "lodash"
|
||||
import { ExtendedFindConfig } from "../types/common"
|
||||
import {
|
||||
applyOrdering,
|
||||
getGroupedRelations,
|
||||
queryEntityWithIds,
|
||||
queryEntityWithoutRelations,
|
||||
} from "../utils/repository"
|
||||
import { objectToStringPath } from "@medusajs/utils"
|
||||
|
||||
export type DefaultWithoutRelations = Omit<
|
||||
ExtendedFindConfig<Product>,
|
||||
"relations"
|
||||
>
|
||||
|
||||
export type FindWithoutRelationsOptions = DefaultWithoutRelations & {
|
||||
where: DefaultWithoutRelations["where"] & {
|
||||
price_list_id?: FindOperator<PriceList>
|
||||
sales_channel_id?: FindOperator<SalesChannel>
|
||||
category_id?: {
|
||||
value: string[]
|
||||
}
|
||||
categories?: FindOptionsWhere<ProductCategory>
|
||||
tags?: FindOperator<ProductTag>
|
||||
include_category_children?: boolean
|
||||
discount_condition_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ProductRepository = dataSource.getRepository(Product).extend({
|
||||
async queryProducts(
|
||||
optionsWithoutRelations: FindWithoutRelationsOptions,
|
||||
shouldCount = false
|
||||
): Promise<[Product[], number]> {
|
||||
const tags = optionsWithoutRelations?.where?.tags
|
||||
delete optionsWithoutRelations?.where?.tags
|
||||
|
||||
const price_lists = optionsWithoutRelations?.where?.price_list_id
|
||||
delete optionsWithoutRelations?.where?.price_list_id
|
||||
|
||||
const sales_channels = optionsWithoutRelations?.where?.sales_channel_id
|
||||
delete optionsWithoutRelations?.where?.sales_channel_id
|
||||
|
||||
const categoryId = optionsWithoutRelations?.where?.category_id
|
||||
delete optionsWithoutRelations?.where?.category_id
|
||||
|
||||
const categoriesQuery = optionsWithoutRelations.where.categories || {}
|
||||
delete optionsWithoutRelations?.where?.categories
|
||||
|
||||
const include_category_children =
|
||||
optionsWithoutRelations?.where?.include_category_children
|
||||
delete optionsWithoutRelations?.where?.include_category_children
|
||||
|
||||
const discount_condition_id =
|
||||
optionsWithoutRelations?.where?.discount_condition_id
|
||||
delete optionsWithoutRelations?.where?.discount_condition_id
|
||||
|
||||
return queryEntityWithoutRelations<Product>({
|
||||
repository: this,
|
||||
optionsWithoutRelations,
|
||||
shouldCount,
|
||||
customJoinBuilders: [
|
||||
async (qb, alias) => {
|
||||
if (tags) {
|
||||
qb.leftJoin(`${alias}.tags`, "tags").andWhere(
|
||||
`tags.id IN (:...tag_ids)`,
|
||||
{
|
||||
tag_ids: tags.value,
|
||||
}
|
||||
)
|
||||
return { relation: "tags", preventOrderJoin: true }
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
async (qb, alias) => {
|
||||
if (price_lists) {
|
||||
qb.leftJoin(`${alias}.variants`, "variants")
|
||||
.leftJoin("variants.prices", "prices")
|
||||
.andWhere("prices.price_list_id IN (:...price_list_ids)", {
|
||||
price_list_ids: price_lists.value,
|
||||
})
|
||||
return { relation: "prices", preventOrderJoin: true }
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
async (qb, alias) => {
|
||||
if (sales_channels) {
|
||||
qb.innerJoin(
|
||||
`${alias}.sales_channels`,
|
||||
"sales_channels",
|
||||
"sales_channels.id IN (:...sales_channels_ids)",
|
||||
{ sales_channels_ids: sales_channels.value }
|
||||
)
|
||||
return { relation: "sales_channels", preventOrderJoin: true }
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
async (qb, alias) => {
|
||||
let categoryIds: string[] = []
|
||||
if (categoryId) {
|
||||
categoryIds = categoryId?.value
|
||||
|
||||
if (include_category_children) {
|
||||
const categoryRepository =
|
||||
this.manager.getTreeRepository(ProductCategory)
|
||||
const categories = await categoryRepository.find({
|
||||
where: { id: In(categoryIds) },
|
||||
})
|
||||
|
||||
for (const category of categories) {
|
||||
const categoryChildren =
|
||||
await categoryRepository.findDescendantsTree(category)
|
||||
|
||||
const getAllIdsRecursively = (
|
||||
productCategory: ProductCategory
|
||||
) => {
|
||||
let result = [productCategory.id]
|
||||
|
||||
;(productCategory.category_children || []).forEach(
|
||||
(child) => {
|
||||
result = result.concat(getAllIdsRecursively(child))
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
categoryIds = categoryIds.concat(
|
||||
getAllIdsRecursively(categoryChildren)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (categoryIds.length || categoriesQuery) {
|
||||
const joinScope = {}
|
||||
|
||||
if (categoryIds.length) {
|
||||
Object.assign(joinScope, { id: categoryIds })
|
||||
}
|
||||
|
||||
if (categoriesQuery) {
|
||||
Object.assign(joinScope, categoriesQuery)
|
||||
}
|
||||
|
||||
this._applyCategoriesQuery(qb, {
|
||||
alias,
|
||||
categoryAlias: "categories",
|
||||
where: joinScope,
|
||||
joinName: categoryIds.length ? "innerJoin" : "leftJoin",
|
||||
})
|
||||
|
||||
return { relation: "categories", preventOrderJoin: true }
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
async (qb, alias) => {
|
||||
if (discount_condition_id) {
|
||||
qb.innerJoin(
|
||||
"discount_condition_product",
|
||||
"dc_product",
|
||||
`dc_product.product_id = ${alias}.id AND dc_product.condition_id = :dcId`,
|
||||
{ dcId: discount_condition_id }
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
async queryProductsWithIds({
|
||||
entityIds,
|
||||
groupedRelations,
|
||||
withDeleted = false,
|
||||
select = [],
|
||||
order = {},
|
||||
where = {},
|
||||
}: {
|
||||
entityIds: string[]
|
||||
groupedRelations: { [toplevel: string]: string[] }
|
||||
withDeleted?: boolean
|
||||
select?: (keyof Product)[]
|
||||
order?: { [column: string]: "ASC" | "DESC" }
|
||||
where?: FindOptionsWhere<Product>
|
||||
}): Promise<Product[]> {
|
||||
return await queryEntityWithIds({
|
||||
repository: this,
|
||||
entityIds,
|
||||
groupedRelations,
|
||||
withDeleted,
|
||||
select,
|
||||
customJoinBuilders: [
|
||||
(queryBuilder, alias, topLevel) => {
|
||||
if (topLevel === "variants") {
|
||||
queryBuilder.leftJoinAndSelect(
|
||||
`${alias}.${topLevel}`,
|
||||
topLevel,
|
||||
`${topLevel}.deleted_at IS NULL`
|
||||
)
|
||||
|
||||
if (
|
||||
!Object.keys(order!).some((key) => key.startsWith("variants"))
|
||||
) {
|
||||
// variant_rank being select false, apply the filter here directly
|
||||
queryBuilder.addOrderBy(`${topLevel}.variant_rank`, "ASC")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
(queryBuilder, alias, topLevel) => {
|
||||
if (topLevel === "categories") {
|
||||
const joinScope = where!
|
||||
.categories as FindOptionsWhere<ProductCategory>
|
||||
|
||||
this._applyCategoriesQuery(queryBuilder, {
|
||||
alias,
|
||||
categoryAlias: "categories",
|
||||
where: joinScope,
|
||||
joinName: "leftJoinAndSelect",
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
async findWithRelationsAndCount(
|
||||
relations: string[] = [],
|
||||
idsOrOptionsWithoutRelations: FindWithoutRelationsOptions = { where: {} }
|
||||
): Promise<[Product[], number]> {
|
||||
return await this._findWithRelations({
|
||||
relations,
|
||||
idsOrOptionsWithoutRelations,
|
||||
withDeleted: false,
|
||||
shouldCount: true,
|
||||
})
|
||||
},
|
||||
|
||||
async findWithRelations(
|
||||
relations: string[] = [],
|
||||
idsOrOptionsWithoutRelations: FindWithoutRelationsOptions | string[] = {
|
||||
where: {},
|
||||
},
|
||||
withDeleted = false
|
||||
): Promise<Product[]> {
|
||||
const [products] = await this._findWithRelations({
|
||||
relations,
|
||||
idsOrOptionsWithoutRelations,
|
||||
withDeleted,
|
||||
shouldCount: false,
|
||||
})
|
||||
|
||||
return products
|
||||
},
|
||||
|
||||
async findOneWithRelations(
|
||||
relations: string[] = [],
|
||||
optionsWithoutRelations: FindWithoutRelationsOptions = { where: {} }
|
||||
): Promise<Product> {
|
||||
// Limit 1
|
||||
optionsWithoutRelations.take = 1
|
||||
|
||||
const result = await this.findWithRelations(
|
||||
relations,
|
||||
optionsWithoutRelations
|
||||
)
|
||||
return result[0]
|
||||
},
|
||||
|
||||
async bulkAddToCollection(
|
||||
productIds: string[],
|
||||
collectionId: string
|
||||
@@ -42,6 +320,235 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
|
||||
return this.findByIds(productIds)
|
||||
},
|
||||
|
||||
async getFreeTextSearchResultsAndCount(
|
||||
q: string,
|
||||
options: FindWithoutRelationsOptions = { where: {} },
|
||||
relations: string[] = []
|
||||
): Promise<[Product[], number]> {
|
||||
const option_ = cloneDeep(options)
|
||||
|
||||
const productAlias = "product"
|
||||
const pricesAlias = "prices"
|
||||
const variantsAlias = "variants"
|
||||
const collectionAlias = "collection"
|
||||
const tagsAlias = "tags"
|
||||
|
||||
if ("description" in option_.where) {
|
||||
delete option_.where.description
|
||||
}
|
||||
|
||||
if ("title" in option_.where) {
|
||||
delete option_.where.title
|
||||
}
|
||||
|
||||
const tags = option_.where.tags
|
||||
delete option_.where.tags
|
||||
|
||||
const price_lists = option_.where.price_list_id
|
||||
delete option_.where.price_list_id
|
||||
|
||||
const sales_channels = option_.where.sales_channel_id
|
||||
delete option_.where.sales_channel_id
|
||||
|
||||
const discount_condition_id = option_.where.discount_condition_id
|
||||
delete option_.where.discount_condition_id
|
||||
|
||||
const categoriesQuery = option_.where.categories
|
||||
delete option_.where.categories
|
||||
|
||||
let qb = this.createQueryBuilder(`${productAlias}`)
|
||||
.leftJoinAndSelect(`${productAlias}.variants`, variantsAlias)
|
||||
.leftJoinAndSelect(`${productAlias}.collection`, `${collectionAlias}`)
|
||||
.select([`${productAlias}.id`])
|
||||
.where(option_.where)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
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(option_.skip)
|
||||
.take(option_.take)
|
||||
|
||||
if (discount_condition_id) {
|
||||
qb.innerJoin(
|
||||
"discount_condition_product",
|
||||
"dc_product",
|
||||
`dc_product.product_id = ${productAlias}.id AND dc_product.condition_id = :dcId`,
|
||||
{ dcId: discount_condition_id }
|
||||
)
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
qb.leftJoin(`${productAlias}.tags`, tagsAlias).andWhere(
|
||||
`${tagsAlias}.id IN (:...tag_ids)`,
|
||||
{
|
||||
tag_ids: tags.value,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (price_lists) {
|
||||
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(
|
||||
`${productAlias}.sales_channels`,
|
||||
"sales_channels",
|
||||
"sales_channels.id IN (:...sales_channels_ids)",
|
||||
{ sales_channels_ids: sales_channels.value }
|
||||
)
|
||||
}
|
||||
|
||||
if (categoriesQuery) {
|
||||
this._applyCategoriesQuery(qb, {
|
||||
alias: productAlias,
|
||||
categoryAlias: "categories",
|
||||
where: categoriesQuery,
|
||||
joinName: "leftJoin",
|
||||
})
|
||||
}
|
||||
|
||||
const joinedWithTags = !!tags
|
||||
const joinedWithPriceLists = !!price_lists
|
||||
applyOrdering({
|
||||
repository: this,
|
||||
order: (options.order as any) ?? {},
|
||||
qb,
|
||||
alias: productAlias,
|
||||
shouldJoin: (relation) =>
|
||||
relation !== variantsAlias &&
|
||||
(relation !== pricesAlias || !joinedWithPriceLists) &&
|
||||
(relation !== tagsAlias || !joinedWithTags),
|
||||
})
|
||||
|
||||
if (option_.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,
|
||||
[...orderedResultsSet],
|
||||
option_.withDeleted
|
||||
)
|
||||
const productsMap = new Map(products.map((p) => [p.id, p]))
|
||||
|
||||
// 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]
|
||||
},
|
||||
|
||||
async _findWithRelations({
|
||||
relations = [],
|
||||
idsOrOptionsWithoutRelations = {
|
||||
where: {},
|
||||
},
|
||||
withDeleted = false,
|
||||
shouldCount = false,
|
||||
}: {
|
||||
relations: string[]
|
||||
idsOrOptionsWithoutRelations: string[] | FindWithoutRelationsOptions
|
||||
withDeleted: boolean
|
||||
shouldCount: boolean
|
||||
}): Promise<[Product[], number]> {
|
||||
withDeleted = Array.isArray(idsOrOptionsWithoutRelations)
|
||||
? withDeleted
|
||||
: idsOrOptionsWithoutRelations.withDeleted ?? false
|
||||
const isOptionsArray = Array.isArray(idsOrOptionsWithoutRelations)
|
||||
const originalWhere = isOptionsArray
|
||||
? undefined
|
||||
: cloneDeep(idsOrOptionsWithoutRelations.where)
|
||||
const originalOrder: any = isOptionsArray
|
||||
? undefined
|
||||
: { ...idsOrOptionsWithoutRelations.order }
|
||||
const originalSelect = isOptionsArray
|
||||
? undefined
|
||||
: objectToStringPath(idsOrOptionsWithoutRelations.select)
|
||||
const clonedOptions = isOptionsArray
|
||||
? idsOrOptionsWithoutRelations
|
||||
: cloneDeep(idsOrOptionsWithoutRelations)
|
||||
|
||||
let count: number
|
||||
let entities: Product[]
|
||||
|
||||
if (isOptionsArray) {
|
||||
entities = await this.find({
|
||||
where: {
|
||||
id: In(clonedOptions as string[]),
|
||||
},
|
||||
withDeleted,
|
||||
})
|
||||
count = entities.length
|
||||
} else {
|
||||
const result = await this.queryProducts(
|
||||
clonedOptions as FindWithoutRelationsOptions,
|
||||
shouldCount
|
||||
)
|
||||
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) {
|
||||
// Since we are finding by the ids that have been retrieved above and those ids are already
|
||||
// applying skip/take. Remove those options to avoid getting no results
|
||||
if (!Array.isArray(clonedOptions)) {
|
||||
delete clonedOptions.skip
|
||||
delete clonedOptions.take
|
||||
}
|
||||
|
||||
const toReturn = await this.find({
|
||||
...(isOptionsArray
|
||||
? {}
|
||||
: (clonedOptions as FindWithoutRelationsOptions)),
|
||||
where: {
|
||||
id: In(entitiesIds),
|
||||
...(Array.isArray(clonedOptions) ? {} : clonedOptions.where),
|
||||
},
|
||||
})
|
||||
return [toReturn, toReturn.length]
|
||||
}
|
||||
|
||||
const groupedRelations = getGroupedRelations(relations)
|
||||
|
||||
const entitiesIdsWithRelations = await this.queryProductsWithIds({
|
||||
entityIds: entitiesIds,
|
||||
groupedRelations,
|
||||
select: originalSelect,
|
||||
order: originalOrder,
|
||||
where: originalWhere,
|
||||
withDeleted,
|
||||
})
|
||||
|
||||
const entitiesAndRelations = groupBy(entitiesIdsWithRelations, "id")
|
||||
const entitiesToReturn = map(entitiesIds, (id) =>
|
||||
merge({}, ...entitiesAndRelations[id])
|
||||
)
|
||||
|
||||
return [entitiesToReturn, count]
|
||||
},
|
||||
|
||||
async isProductInSalesChannels(
|
||||
id: string,
|
||||
salesChannelIds: string[]
|
||||
@@ -58,7 +565,26 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
|
||||
)
|
||||
},
|
||||
|
||||
async findAndCount(
|
||||
_applyCategoriesQuery(
|
||||
qb: SelectQueryBuilder<Product>,
|
||||
{ alias, categoryAlias, where, joinName }
|
||||
) {
|
||||
const joinWhere = Object.entries(where ?? {})
|
||||
.map(([column, condition]) => {
|
||||
if (Array.isArray(condition)) {
|
||||
return `${categoryAlias}.${column} IN (:...${column})`
|
||||
} else {
|
||||
return `${categoryAlias}.${column} = :${column}`
|
||||
}
|
||||
})
|
||||
.join(" AND ")
|
||||
|
||||
qb[joinName](`${alias}.${categoryAlias}`, categoryAlias, joinWhere, where)
|
||||
|
||||
return qb
|
||||
},
|
||||
|
||||
/* async findAndCount(
|
||||
options: ExtendedFindConfig<Product & ProductFilterOptions>,
|
||||
q?: string
|
||||
): Promise<[Product[], number]> {
|
||||
@@ -293,7 +819,7 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
|
||||
queryBuilder.setFindOptions(options_)
|
||||
|
||||
return queryBuilder
|
||||
},
|
||||
},*/
|
||||
|
||||
/**
|
||||
* Upserts shipping profile for products
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import ProductService from "../product"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
|
||||
@@ -29,7 +29,7 @@ const mockUpsertType = jest.fn().mockImplementation((value) => {
|
||||
describe("ProductService", () => {
|
||||
describe("retrieve", () => {
|
||||
const productRepo = MockRepository({
|
||||
findOne: (query) => {
|
||||
findOneWithRelations: (rels, query) => {
|
||||
if (query.where.id === "test id with variants") {
|
||||
return {
|
||||
id: "test id with variants",
|
||||
@@ -62,8 +62,8 @@ describe("ProductService", () => {
|
||||
it("successfully retrieves a product", async () => {
|
||||
const result = await productService.retrieve(IdMap.getId("ironman"))
|
||||
|
||||
expect(productRepo.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(productRepo.findOne).toHaveBeenCalledWith({
|
||||
expect(productRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
|
||||
expect(productRepo.findOneWithRelations).toHaveBeenCalledWith([], {
|
||||
where: { id: IdMap.getId("ironman") },
|
||||
})
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("ProductService", () => {
|
||||
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
||||
variants: product.variants,
|
||||
}),
|
||||
findOne: () => ({
|
||||
findOneWithRelations: () => ({
|
||||
id: IdMap.getId("ironman"),
|
||||
title: "Suit",
|
||||
options: [],
|
||||
@@ -210,7 +210,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("update", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOne: (query) => {
|
||||
findOneWithRelations: (rels, query) => {
|
||||
if (query.where.id === IdMap.getId("ironman&co")) {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("ironman&co"),
|
||||
@@ -414,7 +414,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("addOption", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOne: (query) =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
options: [{ title: "Color" }],
|
||||
@@ -487,7 +487,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("reorderVariants", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOne: (query) =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }],
|
||||
@@ -546,7 +546,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("updateOption", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOne: (query) =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
options: [
|
||||
@@ -622,7 +622,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("deleteOption", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOne: (query) =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
variants: [
|
||||
|
||||
@@ -13,24 +13,33 @@ import {
|
||||
SalesChannel,
|
||||
} from "../models"
|
||||
import { ImageRepository } from "../repositories/image"
|
||||
import { ProductRepository } from "../repositories/product"
|
||||
import {
|
||||
FindWithoutRelationsOptions,
|
||||
ProductRepository,
|
||||
} from "../repositories/product"
|
||||
import { ProductCategoryRepository } from "../repositories/product-category"
|
||||
import { ProductOptionRepository } from "../repositories/product-option"
|
||||
import { ProductTagRepository } from "../repositories/product-tag"
|
||||
import { ProductTypeRepository } from "../repositories/product-type"
|
||||
import { ProductVariantRepository } from "../repositories/product-variant"
|
||||
import { ExtendedFindConfig, FindConfig, Selector } from "../types/common"
|
||||
import { Selector } from "../types/common"
|
||||
import {
|
||||
CreateProductInput,
|
||||
FilterableProductProps,
|
||||
FindProductConfig,
|
||||
ProductFilterOptions,
|
||||
ProductOptionInput,
|
||||
ProductSelector,
|
||||
UpdateProductInput,
|
||||
} from "../types/product"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import {
|
||||
buildQuery,
|
||||
buildRelationsOrSelect,
|
||||
isString,
|
||||
setMetadata,
|
||||
} from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import EventBusService from "./event-bus"
|
||||
import { objectToStringPath } from "@medusajs/utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -138,7 +147,7 @@ class ProductService extends TransactionBaseService {
|
||||
include_discount_prices: false,
|
||||
}
|
||||
): Promise<[Product[], number]> {
|
||||
const productRepo = this.activeManager_.withRepository(
|
||||
/* const productRepo = this.activeManager_.withRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
|
||||
@@ -147,7 +156,26 @@ class ProductService extends TransactionBaseService {
|
||||
Product & ProductFilterOptions
|
||||
>
|
||||
|
||||
return await productRepo.findAndCount(query, q)
|
||||
return await productRepo.findAndCount(query, q)*/
|
||||
|
||||
/**
|
||||
* TODO: The below code is a temporary fix for the issue with the typeorm idle transaction in query strategy mode
|
||||
*/
|
||||
|
||||
const manager = this.activeManager_
|
||||
const productRepo = manager.withRepository(this.productRepository_)
|
||||
|
||||
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||
|
||||
if (q) {
|
||||
return await productRepo.getFreeTextSearchResultsAndCount(
|
||||
q,
|
||||
query,
|
||||
relations
|
||||
)
|
||||
}
|
||||
|
||||
return await productRepo.findWithRelationsAndCount(relations, query)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,7 +271,7 @@ class ProductService extends TransactionBaseService {
|
||||
include_discount_prices: false, // TODO: this seams to be unused from the repository
|
||||
}
|
||||
): Promise<Product> {
|
||||
const productRepo = this.activeManager_.withRepository(
|
||||
/* const productRepo = this.activeManager_.withRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
const query = buildQuery(selector, config as FindConfig<Product>)
|
||||
@@ -260,6 +288,32 @@ class ProductService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return product*/
|
||||
|
||||
/**
|
||||
* TODO: The below code is a temporary fix for the issue with the typeorm idle transaction in query strategy mode
|
||||
*/
|
||||
const manager = this.activeManager_
|
||||
const productRepo = manager.withRepository(this.productRepository_)
|
||||
|
||||
const { relations, ...query } = buildQuery(selector, config)
|
||||
|
||||
const product = await productRepo.findOneWithRelations(
|
||||
objectToStringPath(relations),
|
||||
query as FindWithoutRelationsOptions
|
||||
)
|
||||
|
||||
if (!product) {
|
||||
const selectorConstraints = Object.entries(selector)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(", ")
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with ${selectorConstraints} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
@@ -888,6 +942,47 @@ class ProductService extends TransactionBaseService {
|
||||
return products
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary method to be used in place we need custom query strategy to prevent typeorm bug
|
||||
* @param selector
|
||||
* @param config
|
||||
* @protected
|
||||
*/
|
||||
protected prepareListQuery_(
|
||||
selector: FilterableProductProps | Selector<Product>,
|
||||
config: FindProductConfig
|
||||
): {
|
||||
q: string
|
||||
relations: (keyof Product)[]
|
||||
query: FindWithoutRelationsOptions
|
||||
} {
|
||||
let q
|
||||
if ("q" in selector) {
|
||||
q = selector.q
|
||||
delete selector.q
|
||||
}
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
query.order = config.order
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
query.relations = buildRelationsOrSelect(config.relations)
|
||||
}
|
||||
|
||||
if (config.select && config.select.length > 0) {
|
||||
query.select = buildRelationsOrSelect(config.select)
|
||||
}
|
||||
|
||||
const rels = objectToStringPath(query.relations)
|
||||
delete query.relations
|
||||
|
||||
return {
|
||||
query: query as FindWithoutRelationsOptions,
|
||||
relations: rels as (keyof Product)[],
|
||||
q,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductService
|
||||
|
||||
@@ -274,7 +274,7 @@ export function addOrderToSelect<TEntity>(
|
||||
* }
|
||||
* @param collection
|
||||
*/
|
||||
function buildRelationsOrSelect<TEntity>(
|
||||
export function buildRelationsOrSelect<TEntity>(
|
||||
collection: string[]
|
||||
): FindOptionsRelations<TEntity> | FindOptionsSelect<TEntity> {
|
||||
const output: FindOptionsRelations<TEntity> | FindOptionsSelect<TEntity> = {}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ExtendedFindConfig } from "../types/common"
|
||||
/**
|
||||
* Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs.
|
||||
* Allow to query the relations for the specified entity ids
|
||||
*
|
||||
* @param repository
|
||||
* @param entityIds
|
||||
* @param groupedRelations
|
||||
@@ -17,18 +18,25 @@ import { ExtendedFindConfig } from "../types/common"
|
||||
* @param select
|
||||
* @param customJoinBuilders
|
||||
*/
|
||||
export async function queryEntityWithIds<T extends ObjectLiteral>(
|
||||
repository: Repository<T>,
|
||||
entityIds: string[],
|
||||
groupedRelations: { [toplevel: string]: string[] },
|
||||
export async function queryEntityWithIds<T extends ObjectLiteral>({
|
||||
repository,
|
||||
entityIds,
|
||||
groupedRelations,
|
||||
withDeleted = false,
|
||||
select: (keyof T)[] = [],
|
||||
customJoinBuilders: ((
|
||||
select = [],
|
||||
customJoinBuilders = [],
|
||||
}: {
|
||||
repository: Repository<T>
|
||||
entityIds: string[]
|
||||
groupedRelations: { [toplevel: string]: string[] }
|
||||
withDeleted?: boolean
|
||||
select?: (keyof T)[]
|
||||
customJoinBuilders?: ((
|
||||
qb: SelectQueryBuilder<T>,
|
||||
alias: string,
|
||||
toplevel: string
|
||||
) => boolean)[] = []
|
||||
): Promise<T[]> {
|
||||
) => boolean)[]
|
||||
}): Promise<T[]> {
|
||||
const alias = repository.metadata.name.toLowerCase()
|
||||
return await Promise.all(
|
||||
Object.entries(groupedRelations).map(async ([toplevel, rels]) => {
|
||||
@@ -58,10 +66,11 @@ export async function queryEntityWithIds<T extends ObjectLiteral>(
|
||||
if (!rest) {
|
||||
continue
|
||||
}
|
||||
// Regex matches all '.' except the rightmost
|
||||
querybuilder = querybuilder.leftJoinAndSelect(
|
||||
// Regex matches all '.' except the rightmost
|
||||
rel.replace(/\.(?=[^.]*\.)/g, "__"),
|
||||
rel.replace(".", "__")
|
||||
// Replace all '.' with '__' to avoid typeorm's automatic aliasing
|
||||
rel.replace(/\./g, "__")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,20 +98,26 @@ export async function queryEntityWithIds<T extends ObjectLiteral>(
|
||||
* Custom query entity without relations, it is part of the creation of a custom findWithRelationsAndCount needs.
|
||||
* Allow to query the entities without taking into account the relations. The relations will be queried separately
|
||||
* using the queryEntityWithIds util
|
||||
*
|
||||
* @param repository
|
||||
* @param optionsWithoutRelations
|
||||
* @param shouldCount
|
||||
* @param customJoinBuilders
|
||||
*/
|
||||
export async function queryEntityWithoutRelations<T extends ObjectLiteral>(
|
||||
repository: Repository<T>,
|
||||
optionsWithoutRelations: Omit<ExtendedFindConfig<T>, "relations">,
|
||||
export async function queryEntityWithoutRelations<T extends ObjectLiteral>({
|
||||
repository,
|
||||
optionsWithoutRelations,
|
||||
shouldCount = false,
|
||||
customJoinBuilders = [],
|
||||
}: {
|
||||
repository: Repository<T>
|
||||
optionsWithoutRelations: Omit<ExtendedFindConfig<T>, "relations">
|
||||
shouldCount: boolean
|
||||
customJoinBuilders: ((
|
||||
qb: SelectQueryBuilder<T>,
|
||||
alias: string
|
||||
) => void)[] = []
|
||||
): Promise<[T[], number]> {
|
||||
) => Promise<{ relation: string; preventOrderJoin: boolean } | void>)[]
|
||||
}): Promise<[T[], number]> {
|
||||
const alias = repository.metadata.name.toLowerCase()
|
||||
|
||||
const qb = repository
|
||||
@@ -115,24 +130,30 @@ export async function queryEntityWithoutRelations<T extends ObjectLiteral>(
|
||||
qb.where(optionsWithoutRelations.where)
|
||||
}
|
||||
|
||||
if (optionsWithoutRelations.order) {
|
||||
const toSelect: string[] = []
|
||||
const parsed = Object.entries(optionsWithoutRelations.order).reduce(
|
||||
(acc, [k, v]) => {
|
||||
const key = `${alias}.${k}`
|
||||
toSelect.push(key)
|
||||
acc[key] = v
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
qb.addSelect(toSelect)
|
||||
qb.orderBy(parsed)
|
||||
const shouldJoins: { relation: string; shouldJoin: boolean }[] = []
|
||||
for (const customJoinBuilder of customJoinBuilders) {
|
||||
const result = await customJoinBuilder(qb, alias)
|
||||
if (result) {
|
||||
shouldJoins.push({
|
||||
relation: result.relation,
|
||||
shouldJoin: !result.preventOrderJoin,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const customJoinBuilder of customJoinBuilders) {
|
||||
customJoinBuilder(qb, alias)
|
||||
}
|
||||
applyOrdering({
|
||||
repository,
|
||||
order: (optionsWithoutRelations.order as any) ?? {},
|
||||
qb,
|
||||
alias,
|
||||
shouldJoin: (relationToJoin) => {
|
||||
return shouldJoins.every(
|
||||
({ relation, shouldJoin }) =>
|
||||
relation !== relationToJoin ||
|
||||
(relation === relationToJoin && shouldJoin)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
if (optionsWithoutRelations.withDeleted) {
|
||||
qb.withDeleted()
|
||||
@@ -252,7 +273,10 @@ export function applyOrdering<T extends ObjectLiteral>({
|
||||
}
|
||||
|
||||
const key = `${alias}.${orderPath}`
|
||||
toSelect.push(key)
|
||||
// Prevent ambiguous column error when top level entity id is ordered
|
||||
if (orderPath !== "id") {
|
||||
toSelect.push(key)
|
||||
}
|
||||
acc[key] = orderDirection
|
||||
return acc
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user