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:
1 parent
59bf9afd48
commit
1f44281ed6
3 files changed
+181
-16
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/product": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(product): Update full descendant true when update parent category id
|
||||||
@@ -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", () => {
|
describe("delete", () => {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils"
|
import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
|
EntityDTO,
|
||||||
|
LoadStrategy,
|
||||||
FilterQuery as MikroFilterQuery,
|
FilterQuery as MikroFilterQuery,
|
||||||
FindOptions as MikroOptions,
|
FindOptions as MikroOptions,
|
||||||
LoadStrategy,
|
RequiredEntityData,
|
||||||
} 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"
|
||||||
@@ -121,20 +123,26 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
ancestors?: boolean
|
ancestors?: boolean
|
||||||
},
|
},
|
||||||
productCategories: ProductCategory[],
|
productCategories: ProductCategory[],
|
||||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
findOptions: DAL.FindOptions<ProductCategory> & {
|
||||||
|
serialize?: boolean
|
||||||
|
} = { where: {} },
|
||||||
context: Context = {}
|
context: Context = {}
|
||||||
): Promise<ProductCategory[]> {
|
): Promise<ProductCategory[]> {
|
||||||
|
const { serialize = true } = findOptions
|
||||||
|
delete findOptions.serialize
|
||||||
|
|
||||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
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
|
// We dont want to get the relations as we will fetch all the categories and build the tree manually
|
||||||
let relationIndex =
|
let relationIndex =
|
||||||
findOptions.options?.populate?.indexOf("parent_category")
|
findOptions.options?.populate?.indexOf("parent_category") ?? -1
|
||||||
const shouldPopulateParent = relationIndex !== -1
|
const shouldPopulateParent = relationIndex !== -1
|
||||||
if (shouldPopulateParent && include.ancestors) {
|
if (shouldPopulateParent && include.ancestors) {
|
||||||
findOptions.options!.populate!.splice(relationIndex as number, 1)
|
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
|
const shouldPopulateChildren = relationIndex !== -1
|
||||||
|
|
||||||
if (shouldPopulateChildren && include.descendants) {
|
if (shouldPopulateChildren && include.descendants) {
|
||||||
@@ -171,9 +179,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
delete where.mpath
|
delete where.mpath
|
||||||
delete where.parent_category_id
|
delete where.parent_category_id
|
||||||
|
|
||||||
const categoriesInTree = await this.serialize<ProductCategory[]>(
|
const categoriesInTree = serialize
|
||||||
await manager.find(ProductCategory, where, options)
|
? 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]))
|
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(
|
const categories = await Promise.all(
|
||||||
data.map(async (entry, i) => {
|
data.map(async (entry, i) => {
|
||||||
const categoryData: Partial<ProductCategory> = { ...entry }
|
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
|
||||||
const siblingsCount = await manager.count(ProductCategory, {
|
const siblingsCount = await manager.count(ProductCategory, {
|
||||||
parent_category_id: categoryData?.parent_category_id || null,
|
parent_category_id: categoryData?.parent_category_id || null,
|
||||||
})
|
})
|
||||||
@@ -387,7 +397,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
categoryData.mpath = parentCategory.mpath
|
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 manager = super.getActiveManager<SqlEntityManager>(context)
|
||||||
const categories = await Promise.all(
|
const categories = await Promise.all(
|
||||||
data.map(async (entry, i) => {
|
data.map(async (entry, i) => {
|
||||||
const categoryData: Partial<ProductCategory> = { ...entry }
|
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
|
||||||
const productCategory = await manager.findOne(ProductCategory, {
|
let productCategory = (await manager.findOne(ProductCategory, {
|
||||||
id: categoryData.id,
|
id: categoryData.id,
|
||||||
})
|
})) as ProductCategory
|
||||||
|
|
||||||
if (!productCategory) {
|
if (!productCategory) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
@@ -438,17 +451,59 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
if (categoryData.parent_category_id === null) {
|
if (categoryData.parent_category_id === null) {
|
||||||
categoryData.mpath = ""
|
categoryData.mpath = ""
|
||||||
} else {
|
} else {
|
||||||
|
productCategory = (
|
||||||
|
await this.buildProductCategoriesWithTree(
|
||||||
|
{
|
||||||
|
descendants: true,
|
||||||
|
},
|
||||||
|
[productCategory],
|
||||||
|
{
|
||||||
|
where: { id: productCategory.id },
|
||||||
|
serialize: false,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
|
||||||
const newParentCategory = await manager.findOne(
|
const newParentCategory = await manager.findOne(
|
||||||
ProductCategory,
|
ProductCategory,
|
||||||
categoryData.parent_category_id
|
categoryData.parent_category_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!newParentCategory) {
|
if (!newParentCategory) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
`Parent category with id: '${categoryData.parent_category_id}' does not exist`
|
`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
|
// Rerank the siblings in the new parent
|
||||||
@@ -491,7 +546,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
await this.rerankAllSiblings(
|
await this.rerankAllSiblings(
|
||||||
manager,
|
manager,
|
||||||
productCategory,
|
productCategory,
|
||||||
categoryData as ProductCategory
|
categoryData as Partial<EntityDTO<ProductCategory>> & {
|
||||||
|
rank: number
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +586,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
|
|
||||||
protected async rerankSiblingsAfterCreation(
|
protected async rerankSiblingsAfterCreation(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
addedSibling: Partial<ProductCategory>
|
addedSibling: Partial<EntityDTO<ProductCategory>>
|
||||||
) {
|
) {
|
||||||
const affectedSiblings = await manager.find(ProductCategory, {
|
const affectedSiblings = await manager.find(ProductCategory, {
|
||||||
parent_category_id: addedSibling.parent_category_id,
|
parent_category_id: addedSibling.parent_category_id,
|
||||||
@@ -547,7 +604,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
|
|||||||
protected async rerankAllSiblings(
|
protected async rerankAllSiblings(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
originalSibling: Partial<ProductCategory> & { rank: number },
|
originalSibling: Partial<ProductCategory> & { rank: number },
|
||||||
updatedSibling: Partial<ProductCategory> & { rank: number }
|
updatedSibling: Partial<EntityDTO<ProductCategory>> & { rank: number }
|
||||||
) {
|
) {
|
||||||
if (originalSibling.rank === updatedSibling.rank) {
|
if (originalSibling.rank === updatedSibling.rank) {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in new issue
Block a user