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
This commit is contained in:
1 parent
ad7f56506f
commit
6323868f65
6 files changed
+98
-7
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa): allow appending all category descendants with a param in list endpoint
|
||||||
@@ -124,21 +124,23 @@ describe("/admin/product-categories", () => {
|
|||||||
|
|
||||||
productCategoryParent = await simpleProductCategoryFactory(dbConnection, {
|
productCategoryParent = await simpleProductCategoryFactory(dbConnection, {
|
||||||
name: "Mens",
|
name: "Mens",
|
||||||
handle: "mens",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
productCategory = await simpleProductCategoryFactory(dbConnection, {
|
productCategory = await simpleProductCategoryFactory(dbConnection, {
|
||||||
name: "sweater",
|
name: "sweater",
|
||||||
handle: "sweater",
|
|
||||||
parent_category: productCategoryParent,
|
parent_category: productCategoryParent,
|
||||||
is_internal: true,
|
is_internal: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
productCategoryChild = await simpleProductCategoryFactory(dbConnection, {
|
productCategoryChild = await simpleProductCategoryFactory(dbConnection, {
|
||||||
name: "cashmere",
|
name: "cashmere",
|
||||||
handle: "cashmere",
|
|
||||||
parent_category: productCategory,
|
parent_category: productCategory,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
productCategoryChild2 = await simpleProductCategoryFactory(dbConnection, {
|
||||||
|
name: "specific cashmere",
|
||||||
|
parent_category: productCategoryChild,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -155,7 +157,7 @@ describe("/admin/product-categories", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
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.offset).toEqual(0)
|
||||||
expect(response.data.limit).toEqual(100)
|
expect(response.data.limit).toEqual(100)
|
||||||
expect(response.data.product_categories).toEqual(
|
expect(response.data.product_categories).toEqual(
|
||||||
@@ -185,6 +187,17 @@ describe("/admin/product-categories", () => {
|
|||||||
parent_category: expect.objectContaining({
|
parent_category: expect.objectContaining({
|
||||||
id: productCategory.id,
|
id: productCategory.id,
|
||||||
}),
|
}),
|
||||||
|
category_children: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: productCategoryChild2.id,
|
||||||
|
})
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: productCategoryChild2.id,
|
||||||
|
parent_category: expect.objectContaining({
|
||||||
|
id: productCategoryChild.id,
|
||||||
|
}),
|
||||||
category_children: [],
|
category_children: [],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -238,6 +251,41 @@ describe("/admin/product-categories", () => {
|
|||||||
expect(nullCategoryResponse.data.count).toEqual(1)
|
expect(nullCategoryResponse.data.count).toEqual(1)
|
||||||
expect(nullCategoryResponse.data.product_categories[0].id).toEqual(productCategoryParent.id)
|
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", () => {
|
describe("POST /admin/product-categories", () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { extendedFindParamsMixin } from "../../../../types/common"
|
|||||||
* - (query) q {string} Query used for searching product category names orhandles.
|
* - (query) q {string} Query used for searching product category names orhandles.
|
||||||
* - (query) is_internal {boolean} Search for only internal categories.
|
* - (query) is_internal {boolean} Search for only internal categories.
|
||||||
* - (query) is_active {boolean} Search for only active 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) parent_category_id {string} Returns categories scoped by parent
|
||||||
* - (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.
|
||||||
@@ -92,6 +93,10 @@ export class AdminGetProductCategoriesParams extends extendedFindParamsMixin({
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
q?: string
|
q?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
include_descendants_tree?: boolean
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
is_internal?: boolean
|
is_internal?: boolean
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe("ProductCategoryService", () => {
|
|||||||
expect(result.length).toEqual(1)
|
expect(result.length).toEqual(1)
|
||||||
expect(result[0].id).toEqual(validID)
|
expect(result[0].id).toEqual(validID)
|
||||||
expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1)
|
expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productCategoryRepository.findDescendantsTree).not.toBeCalled()
|
||||||
expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledWith(
|
expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
order: {
|
order: {
|
||||||
@@ -77,6 +78,16 @@ describe("ProductCategoryService", () => {
|
|||||||
expect(result).toEqual([])
|
expect(result).toEqual([])
|
||||||
expect(count).toEqual(0)
|
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", () => {
|
describe("create", () => {
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { EntityManager } from "typeorm"
|
|||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { ProductCategory } from "../models"
|
import { ProductCategory } from "../models"
|
||||||
import { ProductCategoryRepository } from "../repositories/product-category"
|
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 { buildQuery } from "../utils"
|
||||||
import { EventBusService } from "."
|
import { EventBusService } from "."
|
||||||
import {
|
import {
|
||||||
@@ -49,7 +54,7 @@ class ProductCategoryService extends TransactionBaseService {
|
|||||||
* as the second element.
|
* as the second element.
|
||||||
*/
|
*/
|
||||||
async listAndCount(
|
async listAndCount(
|
||||||
selector: QuerySelector<ProductCategory>,
|
selector: TreeQuerySelector<ProductCategory>,
|
||||||
config: FindConfig<ProductCategory> = {
|
config: FindConfig<ProductCategory> = {
|
||||||
skip: 0,
|
skip: 0,
|
||||||
take: 100,
|
take: 100,
|
||||||
@@ -57,6 +62,9 @@ class ProductCategoryService extends TransactionBaseService {
|
|||||||
},
|
},
|
||||||
treeSelector: QuerySelector<ProductCategory> = {}
|
treeSelector: QuerySelector<ProductCategory> = {}
|
||||||
): Promise<[ProductCategory[], number]> {
|
): Promise<[ProductCategory[], number]> {
|
||||||
|
const includeDescendantsTree = selector.include_descendants_tree
|
||||||
|
delete selector.include_descendants_tree
|
||||||
|
|
||||||
const productCategoryRepo = this.activeManager_.withRepository(
|
const productCategoryRepo = this.activeManager_.withRepository(
|
||||||
this.productCategoryRepo_
|
this.productCategoryRepo_
|
||||||
)
|
)
|
||||||
@@ -71,11 +79,22 @@ class ProductCategoryService extends TransactionBaseService {
|
|||||||
|
|
||||||
const query = buildQuery(selector_, config)
|
const query = buildQuery(selector_, config)
|
||||||
|
|
||||||
return await productCategoryRepo.getFreeTextSearchResultsAndCount(
|
let [productCategories, count] =
|
||||||
|
await productCategoryRepo.getFreeTextSearchResultsAndCount(
|
||||||
query,
|
query,
|
||||||
q,
|
q,
|
||||||
treeSelector
|
treeSelector
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (includeDescendantsTree) {
|
||||||
|
productCategories = await Promise.all(
|
||||||
|
productCategories.map(async (productCategory) =>
|
||||||
|
productCategoryRepo.findDescendantsTree(productCategory)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [productCategories, count]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export type ExtendedFindConfig<TEntity> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type QuerySelector<TEntity> = Selector<TEntity> & { q?: string }
|
export type QuerySelector<TEntity> = Selector<TEntity> & { q?: string }
|
||||||
|
export type TreeQuerySelector<TEntity> = QuerySelector<TEntity> & {
|
||||||
|
include_descendants_tree?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type Selector<TEntity> = {
|
export type Selector<TEntity> = {
|
||||||
[key in keyof TEntity]?:
|
[key in keyof TEntity]?:
|
||||||
|
|||||||
Reference in new issue
Block a user