fix(product): Update full descendant tree mpath when updating parent category id (#10144)

FIXES FRMW-2774

**What**
When updating the parent category id, all descendant mpath should be re computed
This commit is contained in:
Adrien de Peretti
2024-11-19 13:51:34 +01:00
committed by GitHub
parent 59bf9afd48
commit 1f44281ed6
3 changed files with 181 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/product": patch
---
fix(product): Update full descendant true when update parent category id

View File

@@ -1129,6 +1129,109 @@ moduleIntegrationTestRunner<Service>({
])
)
})
it(`should update the mpath of the full descendent tree successfully when moving the grand parent in the hierarchy`, async () => {
for (const entry of eletronicsCategoriesData) {
await service.create([entry])
}
let [productCategory] = await service.list(
{
id: "laptops",
},
{
select: ["id", "handle"],
}
)
await service.update([
{
id: productCategory.id,
parent_category_id: "gaming-desktops",
},
])
;[productCategory] = await service.list({
id: "laptops",
include_descendants_tree: true,
})
expect(productCategory).toEqual(
expect.objectContaining({
id: "laptops",
mpath: "electronics.computers.desktops.gaming-desktops.laptops",
parent_category_id: "gaming-desktops",
category_children: [
expect.objectContaining({
id: "gaming-laptops",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.gaming-laptops",
category_children: [
expect.objectContaining({
id: "budget-gaming",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.gaming-laptops.budget-gaming",
parent_category_id: "gaming-laptops",
}),
expect.objectContaining({
id: "high-performance",
parent_category_id: "gaming-laptops",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.gaming-laptops.high-performance",
category_children: [
expect.objectContaining({
id: "4k-gaming",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
}),
expect.objectContaining({
id: "vr-ready",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.gaming-laptops.high-performance.vr-ready",
parent_category_id: "high-performance",
}),
],
}),
],
}),
expect.objectContaining({
id: "ultrabooks",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.ultrabooks",
parent_category_id: "laptops",
category_children: [
expect.objectContaining({
id: "convertible-ultrabooks",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.ultrabooks.convertible-ultrabooks",
parent_category_id: "ultrabooks",
category_children: [
expect.objectContaining({
id: "detachable-ultrabooks",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.ultrabooks.convertible-ultrabooks.detachable-ultrabooks",
parent_category_id: "convertible-ultrabooks",
}),
expect.objectContaining({
id: "touchscreen-ultrabooks",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.ultrabooks.convertible-ultrabooks.touchscreen-ultrabooks",
parent_category_id: "convertible-ultrabooks",
}),
],
}),
expect.objectContaining({
id: "thin-light",
mpath:
"electronics.computers.desktops.gaming-desktops.laptops.ultrabooks.thin-light",
parent_category_id: "ultrabooks",
}),
],
}),
],
})
)
})
})
describe("delete", () => {

View File

@@ -6,9 +6,11 @@ import {
} from "@medusajs/framework/types"
import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils"
import {
EntityDTO,
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
LoadStrategy,
RequiredEntityData,
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductCategory } from "@models"
@@ -121,20 +123,26 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
ancestors?: boolean
},
productCategories: ProductCategory[],
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<ProductCategory> & {
serialize?: boolean
} = { where: {} },
context: Context = {}
): Promise<ProductCategory[]> {
const { serialize = true } = findOptions
delete findOptions.serialize
const manager = super.getActiveManager<SqlEntityManager>(context)
// We dont want to get the relations as we will fetch all the categories and build the tree manually
let relationIndex =
findOptions.options?.populate?.indexOf("parent_category")
findOptions.options?.populate?.indexOf("parent_category") ?? -1
const shouldPopulateParent = relationIndex !== -1
if (shouldPopulateParent && include.ancestors) {
findOptions.options!.populate!.splice(relationIndex as number, 1)
}
relationIndex = findOptions.options?.populate?.indexOf("category_children")
relationIndex =
findOptions.options?.populate?.indexOf("category_children") ?? -1
const shouldPopulateChildren = relationIndex !== -1
if (shouldPopulateChildren && include.descendants) {
@@ -171,9 +179,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
delete where.mpath
delete where.parent_category_id
const categoriesInTree = await this.serialize<ProductCategory[]>(
await manager.find(ProductCategory, where, options)
)
const categoriesInTree = serialize
? await this.serialize<ProductCategory[]>(
await manager.find(ProductCategory, where, options)
)
: await manager.find(ProductCategory, where, options)
const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat]))
@@ -352,7 +362,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<ProductCategory> = { ...entry }
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
const siblingsCount = await manager.count(ProductCategory, {
parent_category_id: categoryData?.parent_category_id || null,
})
@@ -387,7 +397,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
categoryData.mpath = parentCategory.mpath
}
return manager.create(ProductCategory, categoryData as ProductCategory)
return manager.create(
ProductCategory,
categoryData as RequiredEntityData<ProductCategory>
)
})
)
@@ -402,10 +415,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<ProductCategory> = { ...entry }
const productCategory = await manager.findOne(ProductCategory, {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
let productCategory = (await manager.findOne(ProductCategory, {
id: categoryData.id,
})
})) as ProductCategory
if (!productCategory) {
throw new MedusaError(
@@ -438,17 +451,59 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
if (categoryData.parent_category_id === null) {
categoryData.mpath = ""
} else {
productCategory = (
await this.buildProductCategoriesWithTree(
{
descendants: true,
},
[productCategory],
{
where: { id: productCategory.id },
serialize: false,
},
context
)
)[0]
const newParentCategory = await manager.findOne(
ProductCategory,
categoryData.parent_category_id
)
if (!newParentCategory) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
`Parent category with id: '${categoryData.parent_category_id}' does not exist`
)
}
categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}`
const categoryDataChildren =
categoryData.category_children?.flatMap(
(child) => child.category_children ?? []
)
const categoryDataChildrenMap = new Map(
categoryDataChildren?.map((child) => [child.id, child])
)
function updateMpathRecursively(
category: ProductCategory,
newBaseMpath: string
) {
const newMpath = `${newBaseMpath}.${category.id}`
category.mpath = newMpath
for (let child of category.category_children) {
child = manager.getReference(ProductCategory, child.id)
manager.assign(
child,
categoryDataChildrenMap.get(child.id) ?? {}
)
updateMpathRecursively(child, newMpath)
}
}
updateMpathRecursively(productCategory!, newParentCategory.mpath!)
// categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}`
}
// Rerank the siblings in the new parent
@@ -491,7 +546,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await this.rerankAllSiblings(
manager,
productCategory,
categoryData as ProductCategory
categoryData as Partial<EntityDTO<ProductCategory>> & {
rank: number
}
)
}
@@ -529,7 +586,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterCreation(
manager: SqlEntityManager,
addedSibling: Partial<ProductCategory>
addedSibling: Partial<EntityDTO<ProductCategory>>
) {
const affectedSiblings = await manager.find(ProductCategory, {
parent_category_id: addedSibling.parent_category_id,
@@ -547,7 +604,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankAllSiblings(
manager: SqlEntityManager,
originalSibling: Partial<ProductCategory> & { rank: number },
updatedSibling: Partial<ProductCategory> & { rank: number }
updatedSibling: Partial<EntityDTO<ProductCategory>> & { rank: number }
) {
if (originalSibling.rank === updatedSibling.rank) {
return