diff --git a/.changeset/mighty-years-crash.md b/.changeset/mighty-years-crash.md new file mode 100644 index 0000000000..0e0d252ac6 --- /dev/null +++ b/.changeset/mighty-years-crash.md @@ -0,0 +1,5 @@ +--- +"@medusajs/product": patch +--- + +fix(product): Update full descendant true when update parent category id diff --git a/packages/modules/product/integration-tests/__tests__/product-category.ts b/packages/modules/product/integration-tests/__tests__/product-category.ts index 5c66a95972..0adaabf2f7 100644 --- a/packages/modules/product/integration-tests/__tests__/product-category.ts +++ b/packages/modules/product/integration-tests/__tests__/product-category.ts @@ -1129,6 +1129,109 @@ moduleIntegrationTestRunner({ ]) ) }) + + 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", () => { diff --git a/packages/modules/product/src/repositories/product-category.ts b/packages/modules/product/src/repositories/product-category.ts index b3374d1aeb..98e67bc2ca 100644 --- a/packages/modules/product/src/repositories/product-category.ts +++ b/packages/modules/product/src/repositories/product-category.ts @@ -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 = { where: {} }, + findOptions: DAL.FindOptions & { + serialize?: boolean + } = { where: {} }, context: Context = {} ): Promise { + const { serialize = true } = findOptions + delete findOptions.serialize + const manager = super.getActiveManager(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( - await manager.find(ProductCategory, where, options) - ) + const categoriesInTree = serialize + ? await this.serialize( + 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 = { ...entry } + const categoryData: Partial> = { ...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 + ) }) ) @@ -402,10 +415,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito const manager = super.getActiveManager(context) const categories = await Promise.all( data.map(async (entry, i) => { - const categoryData: Partial = { ...entry } - const productCategory = await manager.findOne(ProductCategory, { + const categoryData: Partial> = { ...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> & { + rank: number + } ) } @@ -529,7 +586,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito protected async rerankSiblingsAfterCreation( manager: SqlEntityManager, - addedSibling: Partial + addedSibling: Partial> ) { 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 & { rank: number }, - updatedSibling: Partial & { rank: number } + updatedSibling: Partial> & { rank: number } ) { if (originalSibling.rank === updatedSibling.rank) { return