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:
1 parent
cf51ad0d46
commit
1d09a266be
5 files changed
+104
-18
No files matched your search
@@ -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 () => {
|
it("throws error when querying not allowed fields", async () => {
|
||||||
const api = useApi()
|
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 { Request, Response } from "express"
|
||||||
import { Transform } from "class-transformer"
|
import { Transform } from "class-transformer"
|
||||||
|
|
||||||
import { ProductCategoryService } from "../../../../services"
|
import { ProductCategoryService } from "../../../../services"
|
||||||
import { extendedFindParamsMixin } from "../../../../types/common"
|
import { extendedFindParamsMixin } from "../../../../types/common"
|
||||||
|
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||||
import { defaultStoreScope } from "."
|
import { defaultStoreScope } from "."
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,6 +16,7 @@ import { defaultStoreScope } from "."
|
|||||||
* parameters:
|
* parameters:
|
||||||
* - (query) q {string} Query used for searching product category names or handles.
|
* - (query) q {string} Query used for searching product category names or handles.
|
||||||
* - (query) parent_category_id {string} Returns categories scoped by parent
|
* - (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) offset=0 {integer} How many product categories to skip in the result.
|
||||||
* - (query) limit=100 {integer} Limit the number of product categories returned.
|
* - (query) limit=100 {integer} Limit the number of product categories returned.
|
||||||
* x-codegen:
|
* x-codegen:
|
||||||
@@ -100,4 +102,9 @@ export class StoreGetProductCategoriesParams extends extendedFindParamsMixin({
|
|||||||
return value === "null" ? null : value
|
return value === "null" ? null : value
|
||||||
})
|
})
|
||||||
parent_category_id?: string | null
|
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 { ProductCategory } from "../models/product-category"
|
||||||
import { ExtendedFindConfig, QuerySelector } from "../types/common"
|
import { ExtendedFindConfig, QuerySelector } from "../types/common"
|
||||||
import { dataSource } from "../loaders/database"
|
import { dataSource } from "../loaders/database"
|
||||||
import { buildLegacyFieldsListFrom } from "../utils"
|
import { buildLegacyFieldsListFrom } from "../utils"
|
||||||
|
import { isEmpty } from "lodash"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProductCategoryRepository = dataSource
|
export const ProductCategoryRepository = dataSource
|
||||||
.getTreeRepository(ProductCategory)
|
.getTreeRepository(ProductCategory)
|
||||||
.extend({
|
.extend({
|
||||||
async findOneWithDescendants(query: FindOneOptions<ProductCategory>): Promise<ProductCategory | null> {
|
async findOneWithDescendants(
|
||||||
|
query: FindOneOptions<ProductCategory>
|
||||||
|
): Promise<ProductCategory | null> {
|
||||||
const productCategory = await this.findOne(query)
|
const productCategory = await this.findOne(query)
|
||||||
|
|
||||||
if (!productCategory) {
|
if (!productCategory) {
|
||||||
@@ -26,9 +26,7 @@ export const ProductCategoryRepository = dataSource
|
|||||||
|
|
||||||
return sortChildren(
|
return sortChildren(
|
||||||
// Returns the productCategory with all of its descendants until the last child node
|
// Returns the productCategory with all of its descendants until the last child node
|
||||||
await this.findDescendantsTree(
|
await this.findDescendantsTree(productCategory)
|
||||||
productCategory
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -120,7 +118,7 @@ export const ProductCategoryRepository = dataSource
|
|||||||
categories.map(async (productCategory) => {
|
categories.map(async (productCategory) => {
|
||||||
productCategory = await this.findDescendantsTree(productCategory)
|
productCategory = await this.findDescendantsTree(productCategory)
|
||||||
|
|
||||||
return sortChildren(productCategory)
|
return sortChildren(productCategory, treeScope)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -161,3 +159,39 @@ 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 {
|
export class AdminProductCategoriesReqBase {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
|
||||||
handle?: string
|
handle?: string
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
Reference in new issue
Block a user