Skip to content

Commit

Permalink
fix(product): Update full descendant tree mpath when updating parent …
Browse files Browse the repository at this point in the history
…category id (#10144)

FIXES FRMW-2774

**What**
When updating the parent category id, all descendant mpath should be re computed
  • Loading branch information
adrien2p authored Nov 19, 2024
1 parent 59bf9af commit 1f44281
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-years-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/product": patch
---

fix(product): Update full descendant true when update parent category id
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
89 changes: 73 additions & 16 deletions packages/modules/product/src/repositories/product-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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]))

Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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>
)
})
)

Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
)
}

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down

0 comments on commit 1f44281

Please sign in to comment.