diff --git a/.changeset/warm-cobras-serve.md b/.changeset/warm-cobras-serve.md new file mode 100644 index 0000000000000..db9249c9d3c9c --- /dev/null +++ b/.changeset/warm-cobras-serve.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): fixes bug for mpath incorrectly updated for nested categories diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index 0d9ceb3315691..abc838f76243c 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -243,6 +243,17 @@ describe("/admin/product-categories", () => { describe("POST /admin/product-categories", () => { beforeEach(async () => { await adminSeeder(dbConnection) + + productCategoryParent = await simpleProductCategoryFactory(dbConnection, { + name: "category parent", + handle: "category-parent", + }) + + productCategory = await simpleProductCategoryFactory(dbConnection, { + name: "category", + handle: "category", + parent_category: productCategoryParent, + }) }) afterEach(async () => { @@ -267,11 +278,6 @@ describe("/admin/product-categories", () => { }) it("successfully creates a product category", async () => { - productCategoryParent = await simpleProductCategoryFactory(dbConnection, { - name: "category parent", - handle: "category-parent", - }) - const api = useApi() const response = await api.post( @@ -280,7 +286,7 @@ describe("/admin/product-categories", () => { name: "test", handle: "test", is_internal: true, - parent_category_id: productCategoryParent.id, + parent_category_id: productCategory.id, }, adminHeaders ) @@ -296,13 +302,49 @@ describe("/admin/product-categories", () => { created_at: expect.any(String), updated_at: expect.any(String), parent_category: expect.objectContaining({ - id: productCategoryParent.id + id: productCategory.id }), category_children: [] }), }) ) }) + + it("root parent returns children correctly on creating new category", async () => { + const api = useApi() + + const response = await api.post( + `/admin/product-categories`, + { + name: "last descendant", + parent_category_id: productCategory.id, + }, + adminHeaders + ) + const lastDescendant = response.data.product_category + + const parentResponse = await api.get( + `/admin/product-categories/${productCategoryParent.id}`, + adminHeaders + ) + + expect(parentResponse.data.product_category).toEqual( + expect.objectContaining({ + id: productCategoryParent.id, + category_children: [ + expect.objectContaining({ + id: productCategory.id, + category_children: [ + expect.objectContaining({ + id: lastDescendant.id, + category_children: [] + }) + ] + }) + ] + }) + ) + }) }) describe("DELETE /admin/product-categories/:id", () => { @@ -381,14 +423,27 @@ describe("/admin/product-categories", () => { beforeEach(async () => { await adminSeeder(dbConnection) + productCategoryParent = await simpleProductCategoryFactory(dbConnection, { + name: "category parent", + handle: "category-parent", + }) + productCategory = await simpleProductCategoryFactory(dbConnection, { - name: "skinny jeans", - handle: "skinny-jeans", + name: "category", + handle: "category", + parent_category: productCategoryParent, }) - productCategory2 = await simpleProductCategoryFactory(dbConnection, { - name: "sweater", - handle: "sweater", + productCategoryChild = await simpleProductCategoryFactory(dbConnection, { + name: "category child", + handle: "category-child", + parent_category: productCategory, + }) + + productCategoryChild2 = await simpleProductCategoryFactory(dbConnection, { + name: "category child 2", + handle: "category-child-2", + parent_category: productCategoryChild, }) }) @@ -437,13 +492,13 @@ describe("/admin/product-categories", () => { const api = useApi() const response = await api.post( - `/admin/product-categories/${productCategory.id}`, + `/admin/product-categories/${productCategoryChild2.id}`, { name: "test", handle: "test", is_internal: true, is_active: true, - parent_category_id: productCategory2.id, + parent_category_id: productCategory.id, }, adminHeaders ) @@ -459,13 +514,52 @@ describe("/admin/product-categories", () => { created_at: expect.any(String), updated_at: expect.any(String), parent_category: expect.objectContaining({ - id: productCategory2.id, + id: productCategory.id, }), category_children: [] }), }) ) }) + + it("root parent returns children correctly on updating new category", async () => { + const api = useApi() + + const response = await api.post( + `/admin/product-categories/${productCategoryChild2.id}`, + { + parent_category_id: productCategory.id, + }, + adminHeaders + ) + const lastDescendant = response.data.product_category + + const parentResponse = await api.get( + `/admin/product-categories/${productCategoryParent.id}`, + adminHeaders + ) + + expect(parentResponse.data.product_category).toEqual( + expect.objectContaining({ + id: productCategoryParent.id, + category_children: [ + expect.objectContaining({ + id: productCategory.id, + category_children: [ + expect.objectContaining({ + id: productCategoryChild.id, + category_children: [] + }), + expect.objectContaining({ + id: productCategoryChild2.id, + category_children: [] + }) + ] + }) + ] + }) + ) + }) }) describe("POST /admin/product-categories/:id/products/batch", () => { diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index c8f891857133a..d241393807be7 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -121,7 +121,7 @@ class ProductCategoryService extends TransactionBaseService { /** * Creates a product category - * @param productCategory - params used to create + * @param productCategoryInput - parameters to create a product category * @return created product category */ async create( @@ -129,6 +129,9 @@ class ProductCategoryService extends TransactionBaseService { ): Promise { return await this.atomicPhase_(async (manager) => { const pcRepo = manager.withRepository(this.productCategoryRepo_) + + await this.transformParentIdToEntity(productCategoryInput) + let productCategory = pcRepo.create(productCategoryInput) productCategory = await pcRepo.save(productCategory) @@ -157,6 +160,8 @@ class ProductCategoryService extends TransactionBaseService { this.productCategoryRepo_ ) + await this.transformParentIdToEntity(productCategoryInput) + let productCategory = await this.retrieve(productCategoryId) for (const key in productCategoryInput) { @@ -253,6 +258,34 @@ class ProductCategoryService extends TransactionBaseService { ) }) } + + /** + * Accepts an input object and transforms product_category_id + * into product_category entity. + * @param productCategoryInput - params used to create/update + * @return transformed productCategoryInput + */ + protected async transformParentIdToEntity( + productCategoryInput: + | CreateProductCategoryInput + | UpdateProductCategoryInput + ): Promise { + // Typeorm only updates mpath when the category entity of the parent + // is passed into create/save. For this reason, everytime we create a + // category, we must fetch the entity and push to create + const parentCategoryId = productCategoryInput.parent_category_id + + if (!parentCategoryId) { + return productCategoryInput + } + + const parentCategory = await this.retrieve(parentCategoryId) + + productCategoryInput.parent_category = parentCategory + delete productCategoryInput.parent_category_id + + return productCategoryInput + } } export default ProductCategoryService diff --git a/packages/medusa/src/types/product-category.ts b/packages/medusa/src/types/product-category.ts index 85e96468d9b9e..897afc4ad12b2 100644 --- a/packages/medusa/src/types/product-category.ts +++ b/packages/medusa/src/types/product-category.ts @@ -1,5 +1,6 @@ import { Transform } from "class-transformer" import { IsNotEmpty, IsOptional, IsString, IsBoolean } from "class-validator" +import { ProductCategory } from "../models" export type CreateProductCategoryInput = { name: string @@ -7,6 +8,7 @@ export type CreateProductCategoryInput = { is_internal?: boolean is_active?: boolean parent_category_id?: string | null + parent_category?: ProductCategory | null } export type UpdateProductCategoryInput = { @@ -15,6 +17,7 @@ export type UpdateProductCategoryInput = { is_internal?: boolean is_active?: boolean parent_category_id?: string | null + parent_category?: ProductCategory | null } export class AdminProductCategoriesReqBase {