fix(product): category tree breaks due to pagination limit (#7947)
what: - when children scope beyond the pagination limit, we fail to load all descendants and ancestors - we fix this by fetching all categories in the tree scoped by the where clause
This commit is contained in:
+112
@@ -133,6 +133,118 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("with tree inclusion", () => {
|
||||||
|
let root, child1, child2, child1a, child2a, child2a1
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
root = await service.createProductCategories({
|
||||||
|
name: "Root",
|
||||||
|
})
|
||||||
|
|
||||||
|
child1 = await service.createProductCategories({
|
||||||
|
name: "Child 1",
|
||||||
|
parent_category_id: root.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
child1a = await service.createProductCategories({
|
||||||
|
name: "Child 1 a",
|
||||||
|
parent_category_id: child1.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
child2 = await service.createProductCategories({
|
||||||
|
name: "Child 2",
|
||||||
|
parent_category_id: root.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
child2a = await service.createProductCategories({
|
||||||
|
name: "Child 2 a",
|
||||||
|
parent_category_id: child2.id,
|
||||||
|
is_internal: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
child2a1 = await service.createProductCategories({
|
||||||
|
name: "Child 2 a 1",
|
||||||
|
parent_category_id: child2a.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return all descendants of a category", async () => {
|
||||||
|
const results = await service.listProductCategories(
|
||||||
|
{
|
||||||
|
id: root.id,
|
||||||
|
include_descendants_tree: true,
|
||||||
|
is_internal: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: ["id"],
|
||||||
|
take: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: root.id,
|
||||||
|
category_children: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: child1.id,
|
||||||
|
category_children: [
|
||||||
|
expect.objectContaining({ id: child1a.id }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: child2.id,
|
||||||
|
// child2a & child2a1 should not show up as we're scoping by internal
|
||||||
|
category_children: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return all ancestors of a category", async () => {
|
||||||
|
const results = await service.listProductCategories(
|
||||||
|
{
|
||||||
|
id: child1a.id,
|
||||||
|
include_ancestors_tree: true,
|
||||||
|
is_internal: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: ["id"],
|
||||||
|
take: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: child1a.id,
|
||||||
|
parent_category: expect.objectContaining({
|
||||||
|
id: child1.id,
|
||||||
|
parent_category: expect.objectContaining({ id: root.id }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const results2 = await service.listProductCategories(
|
||||||
|
{
|
||||||
|
id: child2a1.id,
|
||||||
|
include_ancestors_tree: true,
|
||||||
|
is_internal: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: ["id"],
|
||||||
|
take: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// If the where query includes scoped categories, we hide from the tree
|
||||||
|
expect(results2).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: child2a1.id,
|
||||||
|
parent_category: undefined,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("listAndCountCategories", () => {
|
describe("listAndCountCategories", () => {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { DALUtils, isDefined, MedusaError } from "@medusajs/utils"
|
import { DALUtils, isDefined, MedusaError } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
|
LoadStrategy,
|
||||||
FilterQuery as MikroFilterQuery,
|
FilterQuery as MikroFilterQuery,
|
||||||
FindOptions as MikroOptions,
|
FindOptions as MikroOptions,
|
||||||
LoadStrategy,
|
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { ProductCategory } from "@models"
|
import { ProductCategory } from "@models"
|
||||||
@@ -72,7 +72,6 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
context: Context = {}
|
context: Context = {}
|
||||||
): Promise<ProductCategory[]> {
|
): Promise<ProductCategory[]> {
|
||||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||||
|
|
||||||
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
|
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
|
||||||
|
|
||||||
const productCategories = await manager.find(
|
const productCategories = await manager.find(
|
||||||
@@ -136,12 +135,14 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
|
|
||||||
relationIndex = findOptions.options?.populate?.indexOf("category_children")
|
relationIndex = findOptions.options?.populate?.indexOf("category_children")
|
||||||
const shouldPopulateChildren = relationIndex !== -1
|
const shouldPopulateChildren = relationIndex !== -1
|
||||||
|
|
||||||
if (shouldPopulateChildren && include.descendants) {
|
if (shouldPopulateChildren && include.descendants) {
|
||||||
findOptions.options!.populate!.splice(relationIndex as number, 1)
|
findOptions.options!.populate!.splice(relationIndex as number, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mpaths: any[] = []
|
const mpaths: any[] = []
|
||||||
const parentMpaths = new Set()
|
const parentMpaths = new Set()
|
||||||
|
|
||||||
for (const cat of productCategories) {
|
for (const cat of productCategories) {
|
||||||
if (include.descendants) {
|
if (include.descendants) {
|
||||||
mpaths.push({ mpath: { $like: `${cat.mpath}%` } })
|
mpaths.push({ mpath: { $like: `${cat.mpath}%` } })
|
||||||
@@ -158,37 +159,34 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
|
|
||||||
mpaths.push({ mpath: Array.from(parentMpaths) })
|
mpaths.push({ mpath: Array.from(parentMpaths) })
|
||||||
|
|
||||||
const whereOptions = {
|
const where = { ...findOptions.where, $or: mpaths }
|
||||||
...findOptions.where,
|
const options = {
|
||||||
$or: mpaths,
|
...findOptions.options,
|
||||||
}
|
limit: undefined,
|
||||||
|
offset: 0,
|
||||||
|
} as MikroOptions<ProductCategory>
|
||||||
|
|
||||||
if ("parent_category_id" in whereOptions) {
|
delete where.id
|
||||||
delete whereOptions.parent_category_id
|
delete where.mpath
|
||||||
}
|
delete where.parent_category_id
|
||||||
|
|
||||||
if ("id" in whereOptions) {
|
const categoriesInTree = await this.serialize<ProductCategory[]>(
|
||||||
delete whereOptions.id
|
await manager.find(ProductCategory, where, options)
|
||||||
}
|
|
||||||
|
|
||||||
let allCategories = await manager.find(
|
|
||||||
ProductCategory,
|
|
||||||
whereOptions as MikroFilterQuery<ProductCategory>,
|
|
||||||
findOptions.options as MikroOptions<ProductCategory>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
allCategories = JSON.parse(JSON.stringify(allCategories))
|
const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat]))
|
||||||
|
|
||||||
const categoriesById = new Map(allCategories.map((cat) => [cat.id, cat]))
|
categoriesInTree.forEach((cat: any) => {
|
||||||
|
|
||||||
allCategories.forEach((cat: any) => {
|
|
||||||
if (cat.parent_category_id && include.ancestors) {
|
if (cat.parent_category_id && include.ancestors) {
|
||||||
cat.parent_category = categoriesById.get(cat.parent_category_id)
|
cat.parent_category = categoriesById.get(cat.parent_category_id)
|
||||||
|
cat.parent_category_id = categoriesById.get(cat.parent_category_id)?.[
|
||||||
|
"id"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const populateChildren = (category, level = 0) => {
|
const populateChildren = (category, level = 0) => {
|
||||||
const categories = allCategories.filter(
|
const categories = categoriesInTree.filter(
|
||||||
(child) => child.parent_category_id === category.id
|
(child) => child.parent_category_id === category.id
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +229,6 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
context: Context = {}
|
context: Context = {}
|
||||||
): Promise<[ProductCategory[], number]> {
|
): Promise<[ProductCategory[], number]> {
|
||||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||||
|
|
||||||
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
|
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
|
||||||
|
|
||||||
const [productCategories, count] = await manager.findAndCount(
|
const [productCategories, count] = await manager.findAndCount(
|
||||||
|
|||||||
Reference in New Issue
Block a user