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:
co-authored by
Riqwan Thamir
parent
26963acc0a
commit
9518efccae
@@ -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 path = require("path")
|
||||||
const { ProductCategory } = require("@medusajs/medusa")
|
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 setupServer = require("../../../../helpers/setup-server")
|
||||||
const { useApi } = require("../../../../helpers/use-api")
|
const { useApi } = require("../../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
const productSeeder = require("../../../helpers/product-seeder")
|
const productSeeder = require("../../../helpers/product-seeder")
|
||||||
const priceListSeeder = require("../../../helpers/price-list-seeder")
|
|
||||||
const {
|
const {
|
||||||
simpleProductFactory,
|
|
||||||
simpleDiscountFactory,
|
|
||||||
simpleProductCategoryFactory,
|
simpleProductCategoryFactory,
|
||||||
simpleSalesChannelFactory,
|
simpleSalesChannelFactory,
|
||||||
simpleRegionFactory,
|
|
||||||
} = require("../../../factories")
|
} = require("../../../factories")
|
||||||
|
|
||||||
const testProductId = "test-product"
|
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 () => {
|
it("returns a list of products in product category without category children", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const params = `category_id[]=${categoryWithProductId}`
|
const params = `category_id[]=${categoryWithProductId}`
|
||||||
const response = await api.get(
|
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||||
`/admin/products?${params}`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(1)
|
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 () => {
|
it("returns a list of products in product category without category children explicitly set to false", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
|
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
|
||||||
const response = await api.get(
|
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||||
`/admin/products?${params}`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(1)
|
expect(response.data.products).toHaveLength(1)
|
||||||
@@ -159,10 +140,7 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const params = `category_id[]=${categoryWithProductId}&include_category_children=true`
|
const params = `category_id[]=${categoryWithProductId}&include_category_children=true`
|
||||||
const response = await api.get(
|
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||||
`/admin/products?${params}`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(3)
|
expect(response.data.products).toHaveLength(3)
|
||||||
@@ -185,10 +163,7 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const params = `category_id[]=${categoryWithoutProductId}&include_category_children=true`
|
const params = `category_id[]=${categoryWithoutProductId}&include_category_children=true`
|
||||||
const response = await api.get(
|
const response = await api.get(`/admin/products?${params}`, adminHeaders)
|
||||||
`/admin/products?${params}`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(0)
|
expect(response.data.products).toHaveLength(0)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ describe("/store/products", () => {
|
|||||||
internalCategoryWithProduct = await simpleProductCategoryFactory(
|
internalCategoryWithProduct = await simpleProductCategoryFactory(
|
||||||
dbConnection,
|
dbConnection,
|
||||||
{
|
{
|
||||||
id: inactiveCategoryWithProductId,
|
id: internalCategoryWithProductId,
|
||||||
name: "inactive category with Product",
|
name: "inactive category with Product",
|
||||||
products: [{ id: testProductFilteringId2 }],
|
products: [{ id: testProductFilteringId2 }],
|
||||||
parent_category: nestedCategoryWithProduct,
|
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 () => {
|
it("does not query products with category that are inactive", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { DeleteResult, FindOperator, FindOptionsRelations, In } from "typeorm"
|
||||||
DeleteResult,
|
|
||||||
FindOperator,
|
|
||||||
FindOptionsRelations,
|
|
||||||
In,
|
|
||||||
SelectQueryBuilder,
|
|
||||||
} from "typeorm"
|
|
||||||
import { CustomerGroup } from "../models"
|
import { CustomerGroup } from "../models"
|
||||||
import { ExtendedFindConfig } from "../types/common"
|
import { ExtendedFindConfig } from "../types/common"
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +9,7 @@ import {
|
|||||||
} from "../utils/repository"
|
} from "../utils/repository"
|
||||||
import { objectToStringPath } from "@medusajs/utils"
|
import { objectToStringPath } from "@medusajs/utils"
|
||||||
import { dataSource } from "../loaders/database"
|
import { dataSource } from "../loaders/database"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
|
||||||
export type DefaultWithoutRelations = Omit<
|
export type DefaultWithoutRelations = Omit<
|
||||||
ExtendedFindConfig<CustomerGroup>,
|
ExtendedFindConfig<CustomerGroup>,
|
||||||
@@ -71,45 +66,66 @@ export const CustomerGroupRepository = dataSource
|
|||||||
|
|
||||||
async findWithRelationsAndCount(
|
async findWithRelationsAndCount(
|
||||||
relations: FindOptionsRelations<CustomerGroup> = {},
|
relations: FindOptionsRelations<CustomerGroup> = {},
|
||||||
idsOrOptionsWithoutRelations: FindWithoutRelationsOptions = { where: {} }
|
idsOrOptionsWithoutRelations: string[] | FindWithoutRelationsOptions = {
|
||||||
|
where: {},
|
||||||
|
}
|
||||||
): Promise<[CustomerGroup[], number]> {
|
): 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 count: number
|
||||||
let entities: CustomerGroup[]
|
let entities: CustomerGroup[]
|
||||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||||
entities = await this.find({
|
entities = await this.find({
|
||||||
where: { id: In(idsOrOptionsWithoutRelations) },
|
where: { id: In(idsOrOptionsWithoutRelations) },
|
||||||
withDeleted: idsOrOptionsWithoutRelations.withDeleted ?? false,
|
withDeleted,
|
||||||
})
|
})
|
||||||
count = entities.length
|
count = entities.length
|
||||||
} else {
|
} else {
|
||||||
const customJoinsBuilders: ((
|
const discountConditionId = (
|
||||||
qb: SelectQueryBuilder<CustomerGroup>,
|
clonedOptions as FindWithoutRelationsOptions
|
||||||
alias: string
|
)?.where?.discount_condition_id
|
||||||
) => void)[] = []
|
delete (clonedOptions as FindWithoutRelationsOptions)?.where
|
||||||
|
?.discount_condition_id
|
||||||
|
|
||||||
if (idsOrOptionsWithoutRelations?.where?.discount_condition_id) {
|
const result = await queryEntityWithoutRelations({
|
||||||
const discountConditionId =
|
repository: this,
|
||||||
idsOrOptionsWithoutRelations?.where?.discount_condition_id
|
optionsWithoutRelations: clonedOptions as FindWithoutRelationsOptions,
|
||||||
delete idsOrOptionsWithoutRelations?.where?.discount_condition_id
|
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(
|
return {
|
||||||
(qb: SelectQueryBuilder<CustomerGroup>, alias: string) => {
|
relation: "discount_condition",
|
||||||
qb.innerJoin(
|
preventOrderJoin: true,
|
||||||
"discount_condition_customer_group",
|
}
|
||||||
"dc_cg",
|
}
|
||||||
`dc_cg.customer_group_id = ${alias}.id AND dc_cg.condition_id = :dcId`,
|
|
||||||
{ dcId: discountConditionId }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await queryEntityWithoutRelations(
|
return
|
||||||
this,
|
},
|
||||||
idsOrOptionsWithoutRelations,
|
],
|
||||||
true,
|
})
|
||||||
customJoinsBuilders
|
|
||||||
)
|
|
||||||
entities = result[0]
|
entities = result[0]
|
||||||
count = result[1]
|
count = result[1]
|
||||||
}
|
}
|
||||||
@@ -121,16 +137,21 @@ export const CustomerGroupRepository = dataSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(relations).length === 0) {
|
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
|
// 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
|
// applying skip/take. Remove those options to avoid getting no results
|
||||||
delete options.skip
|
if (!Array.isArray(clonedOptions)) {
|
||||||
delete options.take
|
delete clonedOptions.skip
|
||||||
|
delete clonedOptions.take
|
||||||
|
}
|
||||||
|
|
||||||
const toReturn = await this.find({
|
const toReturn = await this.find({
|
||||||
...options,
|
...(isOptionsArray
|
||||||
where: { id: In(entitiesIds) },
|
? {}
|
||||||
|
: (clonedOptions as FindWithoutRelationsOptions)),
|
||||||
|
where: {
|
||||||
|
id: In(entitiesIds),
|
||||||
|
...(Array.isArray(clonedOptions) ? {} : clonedOptions.where),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return [toReturn, toReturn.length]
|
return [toReturn, toReturn.length]
|
||||||
}
|
}
|
||||||
@@ -138,16 +159,13 @@ export const CustomerGroupRepository = dataSource
|
|||||||
const legacyRelations = objectToStringPath(relations)
|
const legacyRelations = objectToStringPath(relations)
|
||||||
const groupedRelations = getGroupedRelations(legacyRelations)
|
const groupedRelations = getGroupedRelations(legacyRelations)
|
||||||
|
|
||||||
const legacySelect = objectToStringPath(
|
const entitiesIdsWithRelations = await queryEntityWithIds({
|
||||||
idsOrOptionsWithoutRelations.select
|
repository: this,
|
||||||
)
|
entityIds: entitiesIds,
|
||||||
const entitiesIdsWithRelations = await queryEntityWithIds(
|
|
||||||
this,
|
|
||||||
entitiesIds,
|
|
||||||
groupedRelations,
|
groupedRelations,
|
||||||
idsOrOptionsWithoutRelations.withDeleted,
|
select: originalSelect,
|
||||||
legacySelect
|
withDeleted,
|
||||||
)
|
})
|
||||||
|
|
||||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||||
const entitiesToReturn =
|
const entitiesToReturn =
|
||||||
|
|||||||
@@ -1,21 +1,299 @@
|
|||||||
import {
|
import {
|
||||||
|
Brackets,
|
||||||
FindOperator,
|
FindOperator,
|
||||||
FindOptionsWhere,
|
FindOptionsWhere,
|
||||||
ILike,
|
|
||||||
In,
|
In,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { Product, ProductCategory, ProductVariant } from "../models"
|
|
||||||
import { ExtendedFindConfig } from "../types/common"
|
|
||||||
import { dataSource } from "../loaders/database"
|
|
||||||
import { ProductFilterOptions } from "../types/product"
|
|
||||||
import {
|
import {
|
||||||
isObject,
|
PriceList,
|
||||||
fetchCategoryDescendantsIds,
|
Product,
|
||||||
} from "../utils"
|
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"
|
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({
|
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(
|
async bulkAddToCollection(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
collectionId: string
|
collectionId: string
|
||||||
@@ -42,6 +320,235 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
|
|||||||
return this.findByIds(productIds)
|
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(
|
async isProductInSalesChannels(
|
||||||
id: string,
|
id: string,
|
||||||
salesChannelIds: 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>,
|
options: ExtendedFindConfig<Product & ProductFilterOptions>,
|
||||||
q?: string
|
q?: string
|
||||||
): Promise<[Product[], number]> {
|
): Promise<[Product[], number]> {
|
||||||
@@ -293,7 +819,7 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
|
|||||||
queryBuilder.setFindOptions(options_)
|
queryBuilder.setFindOptions(options_)
|
||||||
|
|
||||||
return queryBuilder
|
return queryBuilder
|
||||||
},
|
},*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upserts shipping profile for products
|
* 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 ProductService from "../product"
|
||||||
import { FlagRouter } from "../../utils/flag-router"
|
import { FlagRouter } from "../../utils/flag-router"
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const mockUpsertType = jest.fn().mockImplementation((value) => {
|
|||||||
describe("ProductService", () => {
|
describe("ProductService", () => {
|
||||||
describe("retrieve", () => {
|
describe("retrieve", () => {
|
||||||
const productRepo = MockRepository({
|
const productRepo = MockRepository({
|
||||||
findOne: (query) => {
|
findOneWithRelations: (rels, query) => {
|
||||||
if (query.where.id === "test id with variants") {
|
if (query.where.id === "test id with variants") {
|
||||||
return {
|
return {
|
||||||
id: "test id with variants",
|
id: "test id with variants",
|
||||||
@@ -62,8 +62,8 @@ describe("ProductService", () => {
|
|||||||
it("successfully retrieves a product", async () => {
|
it("successfully retrieves a product", async () => {
|
||||||
const result = await productService.retrieve(IdMap.getId("ironman"))
|
const result = await productService.retrieve(IdMap.getId("ironman"))
|
||||||
|
|
||||||
expect(productRepo.findOne).toHaveBeenCalledTimes(1)
|
expect(productRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
|
||||||
expect(productRepo.findOne).toHaveBeenCalledWith({
|
expect(productRepo.findOneWithRelations).toHaveBeenCalledWith([], {
|
||||||
where: { id: IdMap.getId("ironman") },
|
where: { id: IdMap.getId("ironman") },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ describe("ProductService", () => {
|
|||||||
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
||||||
variants: product.variants,
|
variants: product.variants,
|
||||||
}),
|
}),
|
||||||
findOne: () => ({
|
findOneWithRelations: () => ({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
title: "Suit",
|
title: "Suit",
|
||||||
options: [],
|
options: [],
|
||||||
@@ -210,7 +210,7 @@ describe("ProductService", () => {
|
|||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const productRepository = MockRepository({
|
const productRepository = MockRepository({
|
||||||
findOne: (query) => {
|
findOneWithRelations: (rels, query) => {
|
||||||
if (query.where.id === IdMap.getId("ironman&co")) {
|
if (query.where.id === IdMap.getId("ironman&co")) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: IdMap.getId("ironman&co"),
|
id: IdMap.getId("ironman&co"),
|
||||||
@@ -414,7 +414,7 @@ describe("ProductService", () => {
|
|||||||
|
|
||||||
describe("addOption", () => {
|
describe("addOption", () => {
|
||||||
const productRepository = MockRepository({
|
const productRepository = MockRepository({
|
||||||
findOne: (query) =>
|
findOneWithRelations: (query) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
options: [{ title: "Color" }],
|
options: [{ title: "Color" }],
|
||||||
@@ -487,7 +487,7 @@ describe("ProductService", () => {
|
|||||||
|
|
||||||
describe("reorderVariants", () => {
|
describe("reorderVariants", () => {
|
||||||
const productRepository = MockRepository({
|
const productRepository = MockRepository({
|
||||||
findOne: (query) =>
|
findOneWithRelations: (query) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }],
|
variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }],
|
||||||
@@ -546,7 +546,7 @@ describe("ProductService", () => {
|
|||||||
|
|
||||||
describe("updateOption", () => {
|
describe("updateOption", () => {
|
||||||
const productRepository = MockRepository({
|
const productRepository = MockRepository({
|
||||||
findOne: (query) =>
|
findOneWithRelations: (query) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
options: [
|
options: [
|
||||||
@@ -622,7 +622,7 @@ describe("ProductService", () => {
|
|||||||
|
|
||||||
describe("deleteOption", () => {
|
describe("deleteOption", () => {
|
||||||
const productRepository = MockRepository({
|
const productRepository = MockRepository({
|
||||||
findOne: (query) =>
|
findOneWithRelations: (query) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
variants: [
|
variants: [
|
||||||
|
|||||||
@@ -13,24 +13,33 @@ import {
|
|||||||
SalesChannel,
|
SalesChannel,
|
||||||
} from "../models"
|
} from "../models"
|
||||||
import { ImageRepository } from "../repositories/image"
|
import { ImageRepository } from "../repositories/image"
|
||||||
import { ProductRepository } from "../repositories/product"
|
import {
|
||||||
|
FindWithoutRelationsOptions,
|
||||||
|
ProductRepository,
|
||||||
|
} from "../repositories/product"
|
||||||
import { ProductCategoryRepository } from "../repositories/product-category"
|
import { ProductCategoryRepository } from "../repositories/product-category"
|
||||||
import { ProductOptionRepository } from "../repositories/product-option"
|
import { ProductOptionRepository } from "../repositories/product-option"
|
||||||
import { ProductTagRepository } from "../repositories/product-tag"
|
import { ProductTagRepository } from "../repositories/product-tag"
|
||||||
import { ProductTypeRepository } from "../repositories/product-type"
|
import { ProductTypeRepository } from "../repositories/product-type"
|
||||||
import { ProductVariantRepository } from "../repositories/product-variant"
|
import { ProductVariantRepository } from "../repositories/product-variant"
|
||||||
import { ExtendedFindConfig, FindConfig, Selector } from "../types/common"
|
import { Selector } from "../types/common"
|
||||||
import {
|
import {
|
||||||
CreateProductInput,
|
CreateProductInput,
|
||||||
|
FilterableProductProps,
|
||||||
FindProductConfig,
|
FindProductConfig,
|
||||||
ProductFilterOptions,
|
|
||||||
ProductOptionInput,
|
ProductOptionInput,
|
||||||
ProductSelector,
|
ProductSelector,
|
||||||
UpdateProductInput,
|
UpdateProductInput,
|
||||||
} from "../types/product"
|
} from "../types/product"
|
||||||
import { buildQuery, isString, setMetadata } from "../utils"
|
import {
|
||||||
|
buildQuery,
|
||||||
|
buildRelationsOrSelect,
|
||||||
|
isString,
|
||||||
|
setMetadata,
|
||||||
|
} from "../utils"
|
||||||
import { FlagRouter } from "../utils/flag-router"
|
import { FlagRouter } from "../utils/flag-router"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
|
import { objectToStringPath } from "@medusajs/utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -138,7 +147,7 @@ class ProductService extends TransactionBaseService {
|
|||||||
include_discount_prices: false,
|
include_discount_prices: false,
|
||||||
}
|
}
|
||||||
): Promise<[Product[], number]> {
|
): Promise<[Product[], number]> {
|
||||||
const productRepo = this.activeManager_.withRepository(
|
/* const productRepo = this.activeManager_.withRepository(
|
||||||
this.productRepository_
|
this.productRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +156,26 @@ class ProductService extends TransactionBaseService {
|
|||||||
Product & ProductFilterOptions
|
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
|
include_discount_prices: false, // TODO: this seams to be unused from the repository
|
||||||
}
|
}
|
||||||
): Promise<Product> {
|
): Promise<Product> {
|
||||||
const productRepo = this.activeManager_.withRepository(
|
/* const productRepo = this.activeManager_.withRepository(
|
||||||
this.productRepository_
|
this.productRepository_
|
||||||
)
|
)
|
||||||
const query = buildQuery(selector, config as FindConfig<Product>)
|
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
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,6 +942,47 @@ class ProductService extends TransactionBaseService {
|
|||||||
return products
|
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
|
export default ProductService
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export function addOrderToSelect<TEntity>(
|
|||||||
* }
|
* }
|
||||||
* @param collection
|
* @param collection
|
||||||
*/
|
*/
|
||||||
function buildRelationsOrSelect<TEntity>(
|
export function buildRelationsOrSelect<TEntity>(
|
||||||
collection: string[]
|
collection: string[]
|
||||||
): FindOptionsRelations<TEntity> | FindOptionsSelect<TEntity> {
|
): FindOptionsRelations<TEntity> | FindOptionsSelect<TEntity> {
|
||||||
const output: 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.
|
* Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs.
|
||||||
* Allow to query the relations for the specified entity ids
|
* Allow to query the relations for the specified entity ids
|
||||||
|
*
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param entityIds
|
* @param entityIds
|
||||||
* @param groupedRelations
|
* @param groupedRelations
|
||||||
@@ -17,18 +18,25 @@ import { ExtendedFindConfig } from "../types/common"
|
|||||||
* @param select
|
* @param select
|
||||||
* @param customJoinBuilders
|
* @param customJoinBuilders
|
||||||
*/
|
*/
|
||||||
export async function queryEntityWithIds<T extends ObjectLiteral>(
|
export async function queryEntityWithIds<T extends ObjectLiteral>({
|
||||||
repository: Repository<T>,
|
repository,
|
||||||
entityIds: string[],
|
entityIds,
|
||||||
groupedRelations: { [toplevel: string]: string[] },
|
groupedRelations,
|
||||||
withDeleted = false,
|
withDeleted = false,
|
||||||
select: (keyof T)[] = [],
|
select = [],
|
||||||
customJoinBuilders: ((
|
customJoinBuilders = [],
|
||||||
|
}: {
|
||||||
|
repository: Repository<T>
|
||||||
|
entityIds: string[]
|
||||||
|
groupedRelations: { [toplevel: string]: string[] }
|
||||||
|
withDeleted?: boolean
|
||||||
|
select?: (keyof T)[]
|
||||||
|
customJoinBuilders?: ((
|
||||||
qb: SelectQueryBuilder<T>,
|
qb: SelectQueryBuilder<T>,
|
||||||
alias: string,
|
alias: string,
|
||||||
toplevel: string
|
toplevel: string
|
||||||
) => boolean)[] = []
|
) => boolean)[]
|
||||||
): Promise<T[]> {
|
}): Promise<T[]> {
|
||||||
const alias = repository.metadata.name.toLowerCase()
|
const alias = repository.metadata.name.toLowerCase()
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
Object.entries(groupedRelations).map(async ([toplevel, rels]) => {
|
Object.entries(groupedRelations).map(async ([toplevel, rels]) => {
|
||||||
@@ -58,10 +66,11 @@ export async function queryEntityWithIds<T extends ObjectLiteral>(
|
|||||||
if (!rest) {
|
if (!rest) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Regex matches all '.' except the rightmost
|
|
||||||
querybuilder = querybuilder.leftJoinAndSelect(
|
querybuilder = querybuilder.leftJoinAndSelect(
|
||||||
|
// Regex matches all '.' except the rightmost
|
||||||
rel.replace(/\.(?=[^.]*\.)/g, "__"),
|
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.
|
* 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
|
* Allow to query the entities without taking into account the relations. The relations will be queried separately
|
||||||
* using the queryEntityWithIds util
|
* using the queryEntityWithIds util
|
||||||
|
*
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param optionsWithoutRelations
|
* @param optionsWithoutRelations
|
||||||
* @param shouldCount
|
* @param shouldCount
|
||||||
* @param customJoinBuilders
|
* @param customJoinBuilders
|
||||||
*/
|
*/
|
||||||
export async function queryEntityWithoutRelations<T extends ObjectLiteral>(
|
export async function queryEntityWithoutRelations<T extends ObjectLiteral>({
|
||||||
repository: Repository<T>,
|
repository,
|
||||||
optionsWithoutRelations: Omit<ExtendedFindConfig<T>, "relations">,
|
optionsWithoutRelations,
|
||||||
shouldCount = false,
|
shouldCount = false,
|
||||||
|
customJoinBuilders = [],
|
||||||
|
}: {
|
||||||
|
repository: Repository<T>
|
||||||
|
optionsWithoutRelations: Omit<ExtendedFindConfig<T>, "relations">
|
||||||
|
shouldCount: boolean
|
||||||
customJoinBuilders: ((
|
customJoinBuilders: ((
|
||||||
qb: SelectQueryBuilder<T>,
|
qb: SelectQueryBuilder<T>,
|
||||||
alias: string
|
alias: string
|
||||||
) => void)[] = []
|
) => Promise<{ relation: string; preventOrderJoin: boolean } | void>)[]
|
||||||
): Promise<[T[], number]> {
|
}): Promise<[T[], number]> {
|
||||||
const alias = repository.metadata.name.toLowerCase()
|
const alias = repository.metadata.name.toLowerCase()
|
||||||
|
|
||||||
const qb = repository
|
const qb = repository
|
||||||
@@ -115,24 +130,30 @@ export async function queryEntityWithoutRelations<T extends ObjectLiteral>(
|
|||||||
qb.where(optionsWithoutRelations.where)
|
qb.where(optionsWithoutRelations.where)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optionsWithoutRelations.order) {
|
const shouldJoins: { relation: string; shouldJoin: boolean }[] = []
|
||||||
const toSelect: string[] = []
|
for (const customJoinBuilder of customJoinBuilders) {
|
||||||
const parsed = Object.entries(optionsWithoutRelations.order).reduce(
|
const result = await customJoinBuilder(qb, alias)
|
||||||
(acc, [k, v]) => {
|
if (result) {
|
||||||
const key = `${alias}.${k}`
|
shouldJoins.push({
|
||||||
toSelect.push(key)
|
relation: result.relation,
|
||||||
acc[key] = v
|
shouldJoin: !result.preventOrderJoin,
|
||||||
return acc
|
})
|
||||||
},
|
}
|
||||||
{}
|
|
||||||
)
|
|
||||||
qb.addSelect(toSelect)
|
|
||||||
qb.orderBy(parsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const customJoinBuilder of customJoinBuilders) {
|
applyOrdering({
|
||||||
customJoinBuilder(qb, alias)
|
repository,
|
||||||
}
|
order: (optionsWithoutRelations.order as any) ?? {},
|
||||||
|
qb,
|
||||||
|
alias,
|
||||||
|
shouldJoin: (relationToJoin) => {
|
||||||
|
return shouldJoins.every(
|
||||||
|
({ relation, shouldJoin }) =>
|
||||||
|
relation !== relationToJoin ||
|
||||||
|
(relation === relationToJoin && shouldJoin)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (optionsWithoutRelations.withDeleted) {
|
if (optionsWithoutRelations.withDeleted) {
|
||||||
qb.withDeleted()
|
qb.withDeleted()
|
||||||
@@ -252,7 +273,10 @@ export function applyOrdering<T extends ObjectLiteral>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = `${alias}.${orderPath}`
|
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
|
acc[key] = orderDirection
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user