From 6323868f65651f19597e9a02ae83890e2fb378e4 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 24 Feb 2023 09:46:52 +0100 Subject: [PATCH] feat(medusa) allow querying category descendants with a param in list endpoint (#3321) What: Allowing the list endpoint to return a full tree when requested. Why: When scoped with parent_category_id=null and include_descendant_tree=true, the query cost is fairly low. This allows for fast querying and prevent FE from building out the entire tree from a flat list repeatedly. By default, it is set to false, so this should be an intentional change knowing the costs of doing it for the entire result set. How: When include_descendants_tree is included in the request parameter or the service parameter, we do a loop on results of product categories and do a call to fetch the descendants of that product category. RESOLVES CORE-1128 --- .changeset/brown-brooms-cheat.md | 5 ++ .../api/__tests__/admin/product-category.ts | 56 +++++++++++++++++-- .../list-product-categories.ts | 5 ++ .../services/__tests__/product-category.ts | 11 ++++ .../medusa/src/services/product-category.ts | 33 ++++++++--- packages/medusa/src/types/common.ts | 3 + 6 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 .changeset/brown-brooms-cheat.md diff --git a/.changeset/brown-brooms-cheat.md b/.changeset/brown-brooms-cheat.md new file mode 100644 index 0000000000..779d4961b0 --- /dev/null +++ b/.changeset/brown-brooms-cheat.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): allow appending all category descendants with a param in list endpoint diff --git a/integration-tests/api/__tests__/admin/product-category.ts b/integration-tests/api/__tests__/admin/product-category.ts index abc838f762..75cd2180f4 100644 --- a/integration-tests/api/__tests__/admin/product-category.ts +++ b/integration-tests/api/__tests__/admin/product-category.ts @@ -124,21 +124,23 @@ describe("/admin/product-categories", () => { productCategoryParent = await simpleProductCategoryFactory(dbConnection, { name: "Mens", - handle: "mens", }) productCategory = await simpleProductCategoryFactory(dbConnection, { name: "sweater", - handle: "sweater", parent_category: productCategoryParent, is_internal: true, }) productCategoryChild = await simpleProductCategoryFactory(dbConnection, { name: "cashmere", - handle: "cashmere", parent_category: productCategory, }) + + productCategoryChild2 = await simpleProductCategoryFactory(dbConnection, { + name: "specific cashmere", + parent_category: productCategoryChild, + }) }) afterEach(async () => { @@ -155,7 +157,7 @@ describe("/admin/product-categories", () => { ) expect(response.status).toEqual(200) - expect(response.data.count).toEqual(3) + expect(response.data.count).toEqual(4) expect(response.data.offset).toEqual(0) expect(response.data.limit).toEqual(100) expect(response.data.product_categories).toEqual( @@ -185,6 +187,17 @@ describe("/admin/product-categories", () => { parent_category: expect.objectContaining({ id: productCategory.id, }), + category_children: [ + expect.objectContaining({ + id: productCategoryChild2.id, + }) + ], + }), + expect.objectContaining({ + id: productCategoryChild2.id, + parent_category: expect.objectContaining({ + id: productCategoryChild.id, + }), category_children: [], }), ]) @@ -238,6 +251,41 @@ describe("/admin/product-categories", () => { expect(nullCategoryResponse.data.count).toEqual(1) expect(nullCategoryResponse.data.product_categories[0].id).toEqual(productCategoryParent.id) }) + + it("adds all descendants to categories in a nested way", async () => { + const api = useApi() + + const response = await api.get( + `/admin/product-categories?parent_category_id=null&include_descendants_tree=true`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.product_categories).toEqual( + expect.arrayContaining([ + 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", () => { diff --git a/packages/medusa/src/api/routes/admin/product-categories/list-product-categories.ts b/packages/medusa/src/api/routes/admin/product-categories/list-product-categories.ts index 09f04383d5..9e35cbeafe 100644 --- a/packages/medusa/src/api/routes/admin/product-categories/list-product-categories.ts +++ b/packages/medusa/src/api/routes/admin/product-categories/list-product-categories.ts @@ -15,6 +15,7 @@ import { extendedFindParamsMixin } from "../../../../types/common" * - (query) q {string} Query used for searching product category names orhandles. * - (query) is_internal {boolean} Search for only internal categories. * - (query) is_active {boolean} Search for only active categories + * - (query) include_descendants_tree {boolean} Include all nested descendants of category * - (query) parent_category_id {string} Returns categories scoped by parent * - (query) offset=0 {integer} How many product categories to skip in the result. * - (query) limit=100 {integer} Limit the number of product categories returned. @@ -92,6 +93,10 @@ export class AdminGetProductCategoriesParams extends extendedFindParamsMixin({ @IsOptional() q?: string + @IsString() + @IsOptional() + include_descendants_tree?: boolean + @IsString() @IsOptional() is_internal?: boolean diff --git a/packages/medusa/src/services/__tests__/product-category.ts b/packages/medusa/src/services/__tests__/product-category.ts index 8631a4daa0..ba0d61c388 100644 --- a/packages/medusa/src/services/__tests__/product-category.ts +++ b/packages/medusa/src/services/__tests__/product-category.ts @@ -55,6 +55,7 @@ describe("ProductCategoryService", () => { expect(result.length).toEqual(1) expect(result[0].id).toEqual(validID) expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1) + expect(productCategoryRepository.findDescendantsTree).not.toBeCalled() expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledWith( { order: { @@ -77,6 +78,16 @@ describe("ProductCategoryService", () => { expect(result).toEqual([]) expect(count).toEqual(0) }) + + it("successfully calls tree descendants when requested to be included", async () => { + const validID = IdMap.getId(validProdCategoryId) + const [result, count] = await productCategoryService + .listAndCount({ include_descendants_tree: true }) + + expect(result[0].id).toEqual(validID) + expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1) + expect(productCategoryRepository.findDescendantsTree).toHaveBeenCalledTimes(1) + }) }) describe("create", () => { diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index d241393807..284184491c 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -3,7 +3,12 @@ import { EntityManager } from "typeorm" import { TransactionBaseService } from "../interfaces" import { ProductCategory } from "../models" import { ProductCategoryRepository } from "../repositories/product-category" -import { FindConfig, QuerySelector, Selector } from "../types/common" +import { + FindConfig, + QuerySelector, + TreeQuerySelector, + Selector, +} from "../types/common" import { buildQuery } from "../utils" import { EventBusService } from "." import { @@ -49,7 +54,7 @@ class ProductCategoryService extends TransactionBaseService { * as the second element. */ async listAndCount( - selector: QuerySelector, + selector: TreeQuerySelector, config: FindConfig = { skip: 0, take: 100, @@ -57,6 +62,9 @@ class ProductCategoryService extends TransactionBaseService { }, treeSelector: QuerySelector = {} ): Promise<[ProductCategory[], number]> { + const includeDescendantsTree = selector.include_descendants_tree + delete selector.include_descendants_tree + const productCategoryRepo = this.activeManager_.withRepository( this.productCategoryRepo_ ) @@ -71,11 +79,22 @@ class ProductCategoryService extends TransactionBaseService { const query = buildQuery(selector_, config) - return await productCategoryRepo.getFreeTextSearchResultsAndCount( - query, - q, - treeSelector - ) + let [productCategories, count] = + await productCategoryRepo.getFreeTextSearchResultsAndCount( + query, + q, + treeSelector + ) + + if (includeDescendantsTree) { + productCategories = await Promise.all( + productCategories.map(async (productCategory) => + productCategoryRepo.findDescendantsTree(productCategory) + ) + ) + } + + return [productCategories, count] } /** diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 044fd6202a..7a10b4d55d 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -62,6 +62,9 @@ export type ExtendedFindConfig = ( } export type QuerySelector = Selector & { q?: string } +export type TreeQuerySelector = QuerySelector & { + include_descendants_tree?: boolean +} export type Selector = { [key in keyof TEntity]?: