From 1d09a266beacefce56b962924d1e3dd1ca4b693f Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 7 Mar 2023 15:54:49 +0100 Subject: [PATCH] feat(medusa): category list API can return all descendant (#3392) * chore: category list API can return all descendant * chore: category handle is no longer required via api * chore: added treescope to sorting * chore: address feedback on PR --- .changeset/sour-frogs-warn.md | 5 ++ .../api/__tests__/store/product-category.ts | 41 +++++++++++ .../list-product-categories.ts | 9 ++- .../src/repositories/product-category.ts | 68 ++++++++++++++----- packages/medusa/src/types/product-category.ts | 1 - 5 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 .changeset/sour-frogs-warn.md diff --git a/.changeset/sour-frogs-warn.md b/.changeset/sour-frogs-warn.md new file mode 100644 index 0000000000..8bee94bb2c --- /dev/null +++ b/.changeset/sour-frogs-warn.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): category list API can return all descendant diff --git a/integration-tests/api/__tests__/store/product-category.ts b/integration-tests/api/__tests__/store/product-category.ts index fc62b2e0ae..15bafc5834 100644 --- a/integration-tests/api/__tests__/store/product-category.ts +++ b/integration-tests/api/__tests__/store/product-category.ts @@ -232,6 +232,47 @@ describe("/store/product-categories", () => { ) }) + it("gets list of product category with all childrens when include_descendants_tree=true", async () => { + const api = useApi() + + const response = await api.get( + `/store/product-categories?parent_category_id=null&include_descendants_tree=true`, + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.product_categories).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: productCategoryParent.id, + parent_category: null, + rank: 0, + category_children: [ + expect.objectContaining({ + id: productCategory.id, + parent_category_id: productCategoryParent.id, + rank: 0, + category_children: [ + expect.objectContaining({ + id: productCategoryChild4.id, + parent_category_id: productCategory.id, + category_children: [], + rank: 2 + }), + expect.objectContaining({ + id: productCategoryChild.id, + parent_category_id: productCategory.id, + category_children: [], + rank: 3, + }), + ], + }), + ], + }), + ]) + ) + }) + it("throws error when querying not allowed fields", async () => { const api = useApi() diff --git a/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts b/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts index 0f434521c3..1dd02e8b9d 100644 --- a/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts +++ b/packages/medusa/src/api/routes/store/product-categories/list-product-categories.ts @@ -1,9 +1,10 @@ -import { IsOptional, IsString } from "class-validator" +import { IsOptional, IsString, IsBoolean } from "class-validator" import { Request, Response } from "express" import { Transform } from "class-transformer" import { ProductCategoryService } from "../../../../services" import { extendedFindParamsMixin } from "../../../../types/common" +import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" import { defaultStoreScope } from "." /** @@ -15,6 +16,7 @@ import { defaultStoreScope } from "." * parameters: * - (query) q {string} Query used for searching product category names or handles. * - (query) parent_category_id {string} Returns categories scoped by parent + * - (query) include_descendants_tree {boolean} Include all nested descendants of category * - (query) offset=0 {integer} How many product categories to skip in the result. * - (query) limit=100 {integer} Limit the number of product categories returned. * x-codegen: @@ -100,4 +102,9 @@ export class StoreGetProductCategoriesParams extends extendedFindParamsMixin({ return value === "null" ? null : value }) parent_category_id?: string | null + + @IsBoolean() + @IsOptional() + @Transform(({ value }) => optionalBooleanMapper.get(value)) + include_descendants_tree?: boolean } diff --git a/packages/medusa/src/repositories/product-category.ts b/packages/medusa/src/repositories/product-category.ts index 2eeca78ab8..8cd988e55f 100644 --- a/packages/medusa/src/repositories/product-category.ts +++ b/packages/medusa/src/repositories/product-category.ts @@ -1,23 +1,23 @@ -import { Brackets, FindOptionsWhere, ILike, DeleteResult, In, FindOneOptions } from "typeorm" +import { + Brackets, + FindOptionsWhere, + ILike, + DeleteResult, + In, + FindOneOptions, +} from "typeorm" import { ProductCategory } from "../models/product-category" import { ExtendedFindConfig, QuerySelector } from "../types/common" import { dataSource } from "../loaders/database" import { buildLegacyFieldsListFrom } from "../utils" - -const sortChildren = (category: ProductCategory): ProductCategory => { - if (category.category_children) { - category.category_children = category?.category_children - .map((child) => sortChildren(child)) - .sort((a, b) => a.rank - b.rank) - } - - return category -} +import { isEmpty } from "lodash" export const ProductCategoryRepository = dataSource .getTreeRepository(ProductCategory) .extend({ - async findOneWithDescendants(query: FindOneOptions): Promise { + async findOneWithDescendants( + query: FindOneOptions + ): Promise { const productCategory = await this.findOne(query) if (!productCategory) { @@ -26,9 +26,7 @@ export const ProductCategoryRepository = dataSource return sortChildren( // Returns the productCategory with all of its descendants until the last child node - await this.findDescendantsTree( - productCategory - ) + await this.findDescendantsTree(productCategory) ) }, @@ -120,7 +118,7 @@ export const ProductCategoryRepository = dataSource categories.map(async (productCategory) => { productCategory = await this.findDescendantsTree(productCategory) - return sortChildren(productCategory) + return sortChildren(productCategory, treeScope) }) ) } @@ -160,4 +158,40 @@ export const ProductCategoryRepository = dataSource }, }) -export default ProductCategoryRepository \ No newline at end of file +export default ProductCategoryRepository + +const scopeChildren = ( + category: ProductCategory, + treeScope: QuerySelector = {} +): ProductCategory => { + if (isEmpty(treeScope)) { + return category + } + + category.category_children = category.category_children.filter( + (categoryChild) => { + return !Object.entries(treeScope).some( + ([attribute, value]) => categoryChild[attribute] !== value + ) + } + ) + + return category +} + +const sortChildren = ( + category: ProductCategory, + treeScope: QuerySelector = {} +): ProductCategory => { + if (category.category_children) { + category.category_children = category?.category_children + .map( + // Before we sort the children, we need scope the children + // to conform to treeScope conditions + (child) => sortChildren(scopeChildren(child, treeScope), treeScope) + ) + .sort((a, b) => a.rank - b.rank) + } + + return category +} diff --git a/packages/medusa/src/types/product-category.ts b/packages/medusa/src/types/product-category.ts index 02d44ae82e..f803de478c 100644 --- a/packages/medusa/src/types/product-category.ts +++ b/packages/medusa/src/types/product-category.ts @@ -23,7 +23,6 @@ export type UpdateProductCategoryInput = ProductCategoryInput & { export class AdminProductCategoriesReqBase { @IsOptional() @IsString() - @IsNotEmpty() handle?: string @IsBoolean()