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:
5
.changeset/sour-frogs-warn.md
Normal file
5
.changeset/sour-frogs-warn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
feat(medusa): category list API can return all descendant
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export type UpdateProductCategoryInput = ProductCategoryInput & {
|
||||
export class AdminProductCategoriesReqBase {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
handle?: string
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
Reference in New Issue
Block a user