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
This commit is contained in:
Riqwan Thamir
2023-03-07 15:54:49 +01:00
committed by GitHub
parent cf51ad0d46
commit 1d09a266be
5 changed files with 105 additions and 19 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): category list API can return all descendant

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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<ProductCategory>): Promise<ProductCategory | null> {
async findOneWithDescendants(
query: FindOneOptions<ProductCategory>
): Promise<ProductCategory | null> {
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
export default ProductCategoryRepository
const scopeChildren = (
category: ProductCategory,
treeScope: QuerySelector<ProductCategory> = {}
): 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> = {}
): 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
}

View File

@@ -23,7 +23,6 @@ export type UpdateProductCategoryInput = ProductCategoryInput & {
export class AdminProductCategoriesReqBase {
@IsOptional()
@IsString()
@IsNotEmpty()
handle?: string
@IsBoolean()