From 09a22205693da62fbf8fd450535d5024cb9c01d1 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:46:55 +0200 Subject: [PATCH] feat(product): return parents tree (#6944) *What* include option `include_ancestors_tree` to list products including the parent tree. --- .changeset/forty-lamps-admire.md | 6 + .../product-category/data/index.ts | 124 ++++++ .../services/product-category/index.ts | 378 +++++++++++++++++- .../src/repositories/product-category.ts | 157 +++++--- .../product/src/services/product-category.ts | 10 +- .../types/src/product-category/repository.ts | 6 +- packages/types/src/product/common.ts | 4 + 7 files changed, 618 insertions(+), 67 deletions(-) create mode 100644 .changeset/forty-lamps-admire.md diff --git a/.changeset/forty-lamps-admire.md b/.changeset/forty-lamps-admire.md new file mode 100644 index 0000000000..45bfa9a737 --- /dev/null +++ b/.changeset/forty-lamps-admire.md @@ -0,0 +1,6 @@ +--- +"@medusajs/product": patch +"@medusajs/types": patch +--- + +Add parents to product categories diff --git a/packages/product/integration-tests/__fixtures__/product-category/data/index.ts b/packages/product/integration-tests/__fixtures__/product-category/data/index.ts index 65231d6fcb..3c7be6ca27 100644 --- a/packages/product/integration-tests/__fixtures__/product-category/data/index.ts +++ b/packages/product/integration-tests/__fixtures__/product-category/data/index.ts @@ -65,3 +65,127 @@ export const productCategoriesRankData = [ rank: 2, }, ] + +export const eletronicsCategoriesData = eval(`[ + { + id: "electronics", + name: "Electronics", + parent_category_id: null, + }, + { + id: "computers", + name: "Computers & Accessories", + parent_category_id: "electronics", + }, + { + id: "desktops", + name: "Desktops", + parent_category_id: "computers", + }, + { + id: "gaming-desktops", + name: "Gaming Desktops", + parent_category_id: "desktops", + }, + { + id: "office-desktops", + name: "Office Desktops", + parent_category_id: "desktops", + }, + { + id: "laptops", + name: "Laptops", + parent_category_id: "computers", + }, + { + id: "gaming-laptops", + name: "Gaming Laptops", + parent_category_id: "laptops", + }, + { + id: "budget-gaming", + name: "Budget Gaming Laptops", + parent_category_id: "gaming-laptops", + }, + { + id: "high-performance", + name: "High Performance Gaming Laptops", + parent_category_id: "gaming-laptops", + }, + { + id: "vr-ready", + name: "VR-Ready High Performance Gaming Laptops", + parent_category_id: "high-performance", + }, + { + id: "4k-gaming", + name: "4K Gaming Laptops", + parent_category_id: "high-performance", + }, + { + id: "ultrabooks", + name: "Ultrabooks", + parent_category_id: "laptops", + }, + { + id: "thin-light", + name: "Thin & Light Ultrabooks", + parent_category_id: "ultrabooks", + }, + { + id: "convertible-ultrabooks", + name: "Convertible Ultrabooks", + parent_category_id: "ultrabooks", + }, + { + id: "touchscreen-ultrabooks", + name: "Touchscreen Ultrabooks", + parent_category_id: "convertible-ultrabooks", + }, + { + id: "detachable-ultrabooks", + name: "Detachable Ultrabooks", + parent_category_id: "convertible-ultrabooks", + }, + + { + id: "mobile", + name: "Mobile Phones & Accessories", + parent_category_id: "electronics", + }, + { + id: "smartphones", + name: "Smartphones", + parent_category_id: "mobile", + }, + { + id: "android-phones", + name: "Android Phones", + parent_category_id: "smartphones", + }, + { + id: "flagship-phones", + name: "Flagship Smartphones", + parent_category_id: "android-phones", + }, + { + id: "budget-phones", + name: "Budget Smartphones", + parent_category_id: "android-phones", + }, + { + id: "iphones", + name: "iPhones", + parent_category_id: "smartphones", + }, + { + id: "pro-phones", + name: "Pro Models", + parent_category_id: "iphones", + }, + { + id: "mini-phones", + name: "Mini Models", + parent_category_id: "iphones", + }, +]`) diff --git a/packages/product/integration-tests/__tests__/services/product-category/index.ts b/packages/product/integration-tests/__tests__/services/product-category/index.ts index 3822f56ee6..4cb7c75ec5 100644 --- a/packages/product/integration-tests/__tests__/services/product-category/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-category/index.ts @@ -1,13 +1,14 @@ import { ProductCategoryService } from "@services" +import { Modules } from "@medusajs/modules-sdk" +import { IProductModuleService } from "@medusajs/types" +import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" import { createProductCategories } from "../../../__fixtures__/product-category" import { + eletronicsCategoriesData, productCategoriesData, productCategoriesRankData, } from "../../../__fixtures__/product-category/data" -import { Modules } from "@medusajs/modules-sdk" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" -import { IProductModuleService } from "@medusajs/types" jest.setTimeout(30000) @@ -138,14 +139,12 @@ moduleIntegrationTestRunner({ handle: "category-1", mpath: "category-0.category-1.", parent_category_id: "category-0", - parent_category: "category-0", category_children: [ expect.objectContaining({ id: "category-1-a", handle: "category-1-a", mpath: "category-0.category-1.category-1-a.", parent_category_id: "category-1", - parent_category: "category-1", category_children: [], }), expect.objectContaining({ @@ -153,7 +152,6 @@ moduleIntegrationTestRunner({ handle: "category-1-b", mpath: "category-0.category-1.category-1-b.", parent_category_id: "category-1", - parent_category: "category-1", category_children: [ expect.objectContaining({ id: "category-1-b-1", @@ -161,7 +159,6 @@ moduleIntegrationTestRunner({ mpath: "category-0.category-1.category-1-b.category-1-b-1.", parent_category_id: "category-1-b", - parent_category: "category-1-b", category_children: [], }), ], @@ -173,6 +170,365 @@ moduleIntegrationTestRunner({ ]) }) + it("includes the entire list of descendants when include_descendants_tree is true for multiple results", async () => { + const productCategoryResults = await service.list( + { + parent_category_id: "category-1", + include_descendants_tree: true, + }, + { + select: ["id", "handle"], + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + { + id: "category-1-a", + handle: "category-1-a", + mpath: "category-0.category-1.category-1-a.", + parent_category_id: "category-1", + category_children: [], + }, + { + id: "category-1-b", + handle: "category-1-b", + mpath: "category-0.category-1.category-1-b.", + parent_category_id: "category-1", + category_children: [ + { + id: "category-1-b-1", + handle: "category-1-b-1", + mpath: "category-0.category-1.category-1-b.category-1-b-1.", + parent_category_id: "category-1-b", + category_children: [], + }, + ], + }, + ]) + }) + + it("includes the entire list of parents when include_ancestors_tree is true", async () => { + await createProductCategories( + MikroOrmWrapper.forkManager(), + eletronicsCategoriesData + ) + + const productCategoryResults = await service.list( + { + id: "4k-gaming", + include_ancestors_tree: true, + }, + { + select: ["id", "handle"], + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + { + id: "4k-gaming", + handle: "4k-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming.", + parent_category_id: "high-performance", + parent_category: { + id: "high-performance", + parent_category_id: "gaming-laptops", + handle: "high-performance-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.high-performance.", + parent_category: { + id: "gaming-laptops", + handle: "gaming-laptops", + mpath: "electronics.computers.laptops.gaming-laptops.", + parent_category_id: "laptops", + parent_category: { + id: "laptops", + parent_category_id: "computers", + handle: "laptops", + mpath: "electronics.computers.laptops.", + parent_category: { + id: "computers", + handle: "computers-&-accessories", + mpath: "electronics.computers.", + parent_category_id: "electronics", + parent_category: { + id: "electronics", + parent_category_id: null, + handle: "electronics", + mpath: "electronics.", + parent_category: null, + }, + }, + }, + }, + }, + }, + ]) + }) + + it("includes the entire list of descendants when include_descendants_tree is true", async () => { + await createProductCategories( + MikroOrmWrapper.forkManager(), + eletronicsCategoriesData + ) + + const productCategoryResults = await service.list( + { + id: "gaming-laptops", + include_descendants_tree: true, + }, + { + select: ["id", "handle"], + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + { + id: "gaming-laptops", + handle: "gaming-laptops", + mpath: "electronics.computers.laptops.gaming-laptops.", + parent_category_id: "laptops", + category_children: [ + { + id: "budget-gaming", + handle: "budget-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.budget-gaming.", + parent_category_id: "gaming-laptops", + category_children: [], + }, + { + id: "high-performance", + handle: "high-performance-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.high-performance.", + parent_category_id: "gaming-laptops", + category_children: [ + { + id: "4k-gaming", + handle: "4k-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming.", + parent_category_id: "high-performance", + category_children: [], + }, + { + id: "vr-ready", + handle: "vr-ready-high-performance-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.high-performance.vr-ready.", + parent_category_id: "high-performance", + category_children: [], + }, + ], + }, + ], + }, + ]) + }) + + it("includes the entire list of descendants an parents when include_descendants_tree and include_ancestors_tree are true", async () => { + await createProductCategories( + MikroOrmWrapper.forkManager(), + eletronicsCategoriesData + ) + + const productCategoryResults = await service.list( + { + id: "gaming-laptops", + include_descendants_tree: true, + include_ancestors_tree: true, + }, + { + select: ["id", "handle"], + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + { + id: "gaming-laptops", + handle: "gaming-laptops", + mpath: "electronics.computers.laptops.gaming-laptops.", + parent_category_id: "laptops", + parent_category: { + id: "laptops", + handle: "laptops", + mpath: "electronics.computers.laptops.", + parent_category_id: "computers", + parent_category: { + id: "computers", + handle: "computers-&-accessories", + mpath: "electronics.computers.", + parent_category_id: "electronics", + parent_category: { + id: "electronics", + handle: "electronics", + mpath: "electronics.", + parent_category_id: null, + parent_category: null, + }, + }, + }, + category_children: [ + { + id: "budget-gaming", + handle: "budget-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.budget-gaming.", + parent_category_id: "gaming-laptops", + }, + { + id: "high-performance", + handle: "high-performance-gaming-laptops", + mpath: + "electronics.computers.laptops.gaming-laptops.high-performance.", + parent_category_id: "gaming-laptops", + }, + ], + }, + ]) + }) + + it("includes the entire list of parents when include_ancestors_tree is true for multiple results", async () => { + const productCategoryResults = await service.list( + { + parent_category_id: "category-1", + include_ancestors_tree: true, + }, + { + select: ["id", "handle"], + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + { + id: "category-1-a", + handle: "category-1-a", + mpath: "category-0.category-1.category-1-a.", + parent_category_id: "category-1", + parent_category: { + id: "category-1", + handle: "category-1", + mpath: "category-0.category-1.", + parent_category_id: "category-0", + parent_category: { + id: "category-0", + handle: "category-0", + mpath: "category-0.", + parent_category_id: null, + parent_category: null, + }, + }, + }, + { + id: "category-1-b", + handle: "category-1-b", + mpath: "category-0.category-1.category-1-b.", + parent_category_id: "category-1", + parent_category: { + id: "category-1", + handle: "category-1", + mpath: "category-0.category-1.", + parent_category_id: "category-0", + parent_category: { + id: "category-0", + handle: "category-0", + mpath: "category-0.", + parent_category_id: null, + parent_category: null, + }, + }, + }, + ]) + }) + + it("includes the entire list of descendants an parents when include_descendants_tree and include_ancestors_tree are true for multiple results", async () => { + const productCategoryResults = await service.list( + { + parent_category_id: "category-1", + include_descendants_tree: true, + include_ancestors_tree: true, + }, + { + select: ["id", "handle"], + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + { + id: "category-1-a", + handle: "category-1-a", + mpath: "category-0.category-1.category-1-a.", + parent_category_id: "category-1", + parent_category: { + id: "category-1", + handle: "category-1", + mpath: "category-0.category-1.", + parent_category_id: "category-0", + parent_category: { + id: "category-0", + handle: "category-0", + mpath: "category-0.", + parent_category_id: null, + parent_category: null, + }, + }, + category_children: [], + }, + { + id: "category-1-b", + handle: "category-1-b", + mpath: "category-0.category-1.category-1-b.", + parent_category_id: "category-1", + parent_category: { + id: "category-1", + handle: "category-1", + mpath: "category-0.category-1.", + parent_category_id: "category-0", + parent_category: { + id: "category-0", + handle: "category-0", + mpath: "category-0.", + parent_category_id: null, + parent_category: null, + }, + }, + category_children: [ + { + id: "category-1-b-1", + handle: "category-1-b-1", + mpath: "category-0.category-1.category-1-b.category-1-b-1.", + parent_category_id: "category-1-b", + }, + ], + }, + ]) + }) + it("scopes children when include_descendants_tree is true", async () => { const productCategoryResults = await service.list( { @@ -202,14 +558,12 @@ moduleIntegrationTestRunner({ handle: "category-1", mpath: "category-0.category-1.", parent_category_id: "category-0", - parent_category: "category-0", category_children: [ expect.objectContaining({ id: "category-1-a", handle: "category-1-a", mpath: "category-0.category-1.category-1-a.", parent_category_id: "category-1", - parent_category: "category-1", category_children: [], }), ], @@ -456,14 +810,12 @@ moduleIntegrationTestRunner({ handle: "category-1", mpath: "category-0.category-1.", parent_category_id: "category-0", - parent_category: "category-0", category_children: [ expect.objectContaining({ id: "category-1-a", handle: "category-1-a", mpath: "category-0.category-1.category-1-a.", parent_category_id: "category-1", - parent_category: "category-1", category_children: [], }), expect.objectContaining({ @@ -471,7 +823,6 @@ moduleIntegrationTestRunner({ handle: "category-1-b", mpath: "category-0.category-1.category-1-b.", parent_category_id: "category-1", - parent_category: "category-1", category_children: [ expect.objectContaining({ id: "category-1-b-1", @@ -479,7 +830,6 @@ moduleIntegrationTestRunner({ mpath: "category-0.category-1.category-1-b.category-1-b-1.", parent_category_id: "category-1-b", - parent_category: "category-1-b", category_children: [], }), ], @@ -522,14 +872,12 @@ moduleIntegrationTestRunner({ handle: "category-1", mpath: "category-0.category-1.", parent_category_id: "category-0", - parent_category: "category-0", category_children: [ expect.objectContaining({ id: "category-1-a", handle: "category-1-a", mpath: "category-0.category-1.category-1-a.", parent_category_id: "category-1", - parent_category: "category-1", category_children: [], }), ], diff --git a/packages/product/src/repositories/product-category.ts b/packages/product/src/repositories/product-category.ts index 13a8fdee09..6299c5642e 100644 --- a/packages/product/src/repositories/product-category.ts +++ b/packages/product/src/repositories/product-category.ts @@ -1,14 +1,17 @@ import { + Context, + DAL, + ProductCategoryTransformOptions, + ProductTypes, +} from "@medusajs/types" +import { DALUtils, MedusaError, isDefined } from "@medusajs/utils" +import { + LoadStrategy, FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, - LoadStrategy, } from "@mikro-orm/core" -import { ProductCategory } from "@models" -import { Context, DAL, ProductCategoryTransformOptions } from "@medusajs/types" -import groupBy from "lodash/groupBy" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { DALUtils, isDefined, MedusaError } from "@medusajs/utils" -import { ProductTypes } from "@medusajs/types" +import { ProductCategory } from "@models" export type ReorderConditions = { targetCategoryId: string @@ -34,13 +37,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito const manager = super.getActiveManager(context) const findOptions_ = { ...findOptions } - const { includeDescendantsTree } = transformOptions + const { includeDescendantsTree, includeParentsTree } = transformOptions findOptions_.options ??= {} const fields = (findOptions_.options.fields ??= []) // Ref: Building descendants // mpath and parent_category_id needs to be added to the query for the tree building to be done accurately - if (includeDescendantsTree) { + if (includeDescendantsTree || includeParentsTree) { fields.indexOf("mpath") === -1 && fields.push("mpath") fields.indexOf("parent_category_id") === -1 && fields.push("parent_category_id") @@ -56,65 +59,118 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito findOptions_.options as MikroOptions ) - if (!includeDescendantsTree) { + if (!includeDescendantsTree && !includeParentsTree) { return productCategories } - return this.buildProductCategoriesWithDescendants( + return this.buildProductCategoriesWithTree( + { + descendants: includeDescendantsTree, + parents: includeParentsTree, + }, productCategories, findOptions_ ) } - async buildProductCategoriesWithDescendants( + async buildProductCategoriesWithTree( + include: { + descendants?: boolean + parents?: boolean + }, productCategories: ProductCategory[], findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { const manager = super.getActiveManager(context) - for (let productCategory of productCategories) { - const whereOptions = { - ...findOptions.where, - mpath: { - $like: `${productCategory.mpath}%`, - }, + const hasPopulateParentCategory = ( + findOptions.options?.populate ?? ([] as any) + ).find((pop) => pop.field === "parent_category") + + include.parents = include.parents || hasPopulateParentCategory + + const mpaths: any[] = [] + const parentMpaths = new Set() + for (const cat of productCategories) { + if (include.descendants) { + mpaths.push({ mpath: { $like: `${cat.mpath}%` } }) } - if ("parent_category_id" in whereOptions) { - delete whereOptions.parent_category_id - } - - if ("id" in whereOptions) { - delete whereOptions.id - } - - const descendantsForCategory = await manager.find( - ProductCategory, - whereOptions as MikroFilterQuery, - findOptions.options as MikroOptions - ) - - const descendantsByParentId = groupBy( - descendantsForCategory, - (pc) => pc.parent_category_id - ) - - const addChildrenToCategory = (category, children) => { - category.category_children = (children || []).map((categoryChild) => { - const moreChildren = descendantsByParentId[categoryChild.id] || [] - - return addChildrenToCategory(categoryChild, moreChildren) + if (include.parents) { + let parent = "" + cat.mpath?.split(".").forEach((mpath) => { + if (mpath === "") { + return + } + parentMpaths.add(parent + mpath + ".") + parent += mpath + "." }) + } + } + mpaths.push({ mpath: Array.from(parentMpaths) }) + + const whereOptions = { + ...findOptions.where, + $or: mpaths, + } + + if ("parent_category_id" in whereOptions) { + delete whereOptions.parent_category_id + } + + if ("id" in whereOptions) { + delete whereOptions.id + } + + let allCategories = await manager.find( + ProductCategory, + whereOptions as MikroFilterQuery, + findOptions.options as MikroOptions + ) + + allCategories = JSON.parse(JSON.stringify(allCategories)) + + const categoriesById = new Map(allCategories.map((cat) => [cat.id, cat])) + + allCategories.forEach((cat: any) => { + if (cat.parent_category_id) { + cat.parent_category = categoriesById.get(cat.parent_category_id) + } + }) + + const populateChildren = (category, level = 0) => { + const categories = allCategories.filter( + (child) => child.parent_category_id === category.id + ) + + if (include.descendants) { + category.category_children = categories.map((child) => { + return populateChildren(categoriesById.get(child.id), level + 1) + }) + } + + if (level === 0) { return category } - const children = descendantsByParentId[productCategory.id] || [] - productCategory = addChildrenToCategory(productCategory, children) + if (include.parents) { + delete category.category_children + } + if (include.descendants) { + delete category.parent_category + } + + return category } - return productCategories + const populatedProductCategories = productCategories.map((cat) => { + const fullCategory = categoriesById.get(cat.id) + return populateChildren(fullCategory) + }) + + return populatedProductCategories } async findAndCount( @@ -125,7 +181,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito const manager = super.getActiveManager(context) const findOptions_ = { ...findOptions } - const { includeDescendantsTree } = transformOptions + const { includeDescendantsTree, includeParentsTree } = transformOptions findOptions_.options ??= {} const fields = (findOptions_.options.fields ??= []) @@ -146,13 +202,20 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito findOptions_.where as MikroFilterQuery, findOptions_.options as MikroOptions ) - if (!includeDescendantsTree) { return [productCategories, count] } + if (!includeDescendantsTree && !includeParentsTree) { + return [productCategories, count] + } + return [ - await this.buildProductCategoriesWithDescendants( + await this.buildProductCategoriesWithTree( + { + descendants: includeDescendantsTree, + parents: includeParentsTree, + }, productCategories, findOptions_ ), diff --git a/packages/product/src/services/product-category.ts b/packages/product/src/services/product-category.ts index f213b6f912..3dd154dc94 100644 --- a/packages/product/src/services/product-category.ts +++ b/packages/product/src/services/product-category.ts @@ -1,14 +1,14 @@ -import { ProductCategory } from "@models" -import { ProductCategoryRepository } from "@repositories" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, InjectTransactionManager, - isDefined, MedusaContext, MedusaError, ModulesSdkUtils, + isDefined, } from "@medusajs/utils" +import { ProductCategory } from "@models" +import { ProductCategoryRepository } from "@repositories" type InjectedDependencies = { productCategoryRepository: DAL.TreeRepositoryService @@ -71,8 +71,10 @@ export default class ProductCategoryService< ): Promise { const transformOptions = { includeDescendantsTree: filters?.include_descendants_tree || false, + includeParentsTree: filters?.include_ancestors_tree || false, } delete filters.include_descendants_tree + delete filters.include_ancestors_tree const queryOptions = ModulesSdkUtils.buildQuery( filters, @@ -95,8 +97,10 @@ export default class ProductCategoryService< ): Promise<[TEntity[], number]> { const transformOptions = { includeDescendantsTree: filters?.include_descendants_tree || false, + includeParentsTree: filters?.include_ancestors_tree || false, } delete filters.include_descendants_tree + delete filters.include_ancestors_tree const queryOptions = ModulesSdkUtils.buildQuery( filters, diff --git a/packages/types/src/product-category/repository.ts b/packages/types/src/product-category/repository.ts index 4fda420340..c62b0ca833 100644 --- a/packages/types/src/product-category/repository.ts +++ b/packages/types/src/product-category/repository.ts @@ -1,5 +1,7 @@ -import { RepositoryTransformOptions } from '../common' +import { RepositoryTransformOptions } from "../common" -export interface ProductCategoryTransformOptions extends RepositoryTransformOptions { +export interface ProductCategoryTransformOptions + extends RepositoryTransformOptions { includeDescendantsTree?: boolean + includeParentsTree?: boolean } diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index ee73e893b0..2095ecdc1a 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -915,6 +915,10 @@ export interface FilterableProductCategoryProps * Whether to include children of retrieved product categories. */ include_descendants_tree?: boolean + /** + * Whether to include parents of retrieved product categories. + */ + include_ancestors_tree?: boolean } /**