Skip to content

Commit

Permalink
feat(medusa): category list API can return all descendant (medusajs#3392
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
riqwan authored Mar 7, 2023
1 parent cf51ad0 commit 1d09a26
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-frogs-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

feat(medusa): category list API can return all descendant
41 changes: 41 additions & 0 deletions integration-tests/api/__tests__/store/product-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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 "."

/**
Expand All @@ -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:
Expand Down Expand Up @@ -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
}
68 changes: 51 additions & 17 deletions packages/medusa/src/repositories/product-category.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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)
)
},

Expand Down Expand Up @@ -120,7 +118,7 @@ export const ProductCategoryRepository = dataSource
categories.map(async (productCategory) => {
productCategory = await this.findDescendantsTree(productCategory)

return sortChildren(productCategory)
return sortChildren(productCategory, treeScope)
})
)
}
Expand Down Expand Up @@ -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
}
1 change: 0 additions & 1 deletion packages/medusa/src/types/product-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export type UpdateProductCategoryInput = ProductCategoryInput & {
export class AdminProductCategoriesReqBase {
@IsOptional()
@IsString()
@IsNotEmpty()
handle?: string

@IsBoolean()
Expand Down

0 comments on commit 1d09a26

Please sign in to comment.