From 85ea143ab5babd486608948415757580c059aed2 Mon Sep 17 00:00:00 2001 From: Tiago Garcia Date: Sat, 17 Jul 2021 16:35:58 +0200 Subject: [PATCH] feat: add depth limiter optional parameter when loading nested trees using TreeRepository's findTrees() and findDescendantsTree() Modified TreeRepository's findTrees() and findDescendantsTree() public methods to now accept an optional object with config options. For now, said object contains a depth parameter that, if set, will limit how far down the tree those methods will crawl and retrieve nested entities. BREAKING CHANGE: TreeRepository's protected method buildChildrenEntityTree() now requires a 4th argument. Anyone affected by this break should also review and update their implementation, otherwise this feature will not work. Closes: #3909 --- docs/tree-entities.md | 5 + src/find-options/FindTreeOptions.ts | 13 +- src/repository/FindTreesOptions.ts | 11 + src/repository/TreeRepository.ts | 15 +- .../closure-table/closure-table.ts | 416 +++++++++++++++++- .../materialized-path/materialized-path.ts | 416 +++++++++++++++++- .../tree-tables/nested-set/nested-set.ts | 350 ++++++++++++++- 7 files changed, 1210 insertions(+), 16 deletions(-) create mode 100644 src/repository/FindTreesOptions.ts diff --git a/docs/tree-entities.md b/docs/tree-entities.md index d12fbbe5af8..3589f9f51bb 100644 --- a/docs/tree-entities.md +++ b/docs/tree-entities.md @@ -206,6 +206,9 @@ const treeCategories = await repository.findTrees(); const treeCategoriesWithRelations = await repository.findTrees({ relations: ["sites"] }); // automatically joins the sites relation + +const treeCategoriesWithLimitedDepth = await repository.findTrees({ depth: 2 }); +// returns root categories with sub categories inside, up to depth 2 ``` * `findRoots` - Roots are entities that have no ancestors. Finds them all. @@ -228,6 +231,8 @@ const children = await repository.findDescendants(parentCategory); ```typescript const childrenTree = await repository.findDescendantsTree(parentCategory); // returns all direct subcategories (with its nested categories) of a parentCategory +const childrenTreeWithLimitedDepth = await repository.findDescendantsTree(parentCategory, { depth: 2 }); +// returns all direct subcategories (with its nested categories) of a parentCategory, up to depth 2 ``` * `createDescendantsQueryBuilder` - Creates a query builder used to get descendants of the entities in a tree. diff --git a/src/find-options/FindTreeOptions.ts b/src/find-options/FindTreeOptions.ts index c8eb2002bcc..8e285386d60 100644 --- a/src/find-options/FindTreeOptions.ts +++ b/src/find-options/FindTreeOptions.ts @@ -3,9 +3,14 @@ */ export interface FindTreeOptions { - /** - * Indicates what relations of entity should be loaded (simplified left join form). - */ - relations?: string[]; + /** + * Indicates what relations of entity should be loaded (simplified left join form). + */ + relations?: string[]; + + /** + * When loading a tree from a TreeRepository, limits the depth of the descendents loaded + */ + depth?: number; } diff --git a/src/repository/FindTreesOptions.ts b/src/repository/FindTreesOptions.ts new file mode 100644 index 00000000000..848f55fce44 --- /dev/null +++ b/src/repository/FindTreesOptions.ts @@ -0,0 +1,11 @@ +/** + * Special options passed to TreeRepository#findTrees + */ +export interface FindTreesOptions { + + /** + * When loading a tree from a TreeRepository, limits the depth of the descendents loaded + */ + depth?: number; + +} diff --git a/src/repository/TreeRepository.ts b/src/repository/TreeRepository.ts index e144e7a1a37..79a3ad21013 100644 --- a/src/repository/TreeRepository.ts +++ b/src/repository/TreeRepository.ts @@ -6,6 +6,7 @@ import { TypeORMError } from "../error/TypeORMError"; import { FindTreeOptions } from "../find-options/FindTreeOptions"; import { FindRelationsNotFoundError } from "../error"; import { FindOptionsUtils } from "../find-options/FindOptionsUtils"; +import { FindTreesOptions } from "./FindTreesOptions"; /** * Repository with additional functions to work with trees. @@ -89,7 +90,11 @@ export class TreeRepository extends Repository { const entities = await qb.getRawAndEntities(); const relationMaps = this.createRelationMaps("treeEntity", entities.raw); - this.buildChildrenEntityTree(entity, entities.entities, relationMaps); + this.buildChildrenEntityTree(entity, entities.entities, relationMaps, { + depth: -1, + ...options + + }); return entity; } @@ -287,14 +292,18 @@ export class TreeRepository extends Repository { }); } - protected buildChildrenEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void { + protected buildChildrenEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[], options: (FindTreesOptions & { depth: number })): void { const childProperty = this.metadata.treeChildrenRelation!.propertyName; + if (options.depth === 0) { + entity[childProperty] = []; + return; + } const parentEntityId = this.metadata.primaryColumns[0].getEntityValue(entity); const childRelationMaps = relationMaps.filter(relationMap => relationMap.parentId === parentEntityId); const childIds = new Set(childRelationMaps.map(relationMap => relationMap.id)); entity[childProperty] = entities.filter(entity => childIds.has(this.metadata.primaryColumns[0].getEntityValue(entity))); entity[childProperty].forEach((childEntity: any) => { - this.buildChildrenEntityTree(childEntity, entities, relationMaps); + this.buildChildrenEntityTree(childEntity, entities, relationMaps, { ...options, depth: options.depth - 1 }); }); } diff --git a/test/functional/tree-tables/closure-table/closure-table.ts b/test/functional/tree-tables/closure-table/closure-table.ts index 19cda481846..e834b4998d6 100644 --- a/test/functional/tree-tables/closure-table/closure-table.ts +++ b/test/functional/tree-tables/closure-table/closure-table.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; -import {Category} from "./entity/Category"; -import {Connection} from "../../../../src/connection/Connection"; -import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; +import { Category } from "./entity/Category"; +import { Connection } from "../../../../src/connection/Connection"; +import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../../utils/test-utils"; describe("tree tables > closure-table", () => { @@ -188,4 +188,414 @@ describe("tree tables > closure-table", () => { // a1ChildrenNames2.should.deep.include("a12"); }))); + describe("findTrees() tests", () => { + it("findTrees should load all category roots and attached children", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + }))); + + it("findTrees should load multiple category roots if they exist", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const b1 = new Category(); + b1.name = "b1"; + + const b11 = new Category(); + b11.name = "b11"; + + const b12 = new Category(); + b12.name = "b12"; + + b1.childCategories = [b11, b12]; + await categoryRepository.save(b1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }, { + id: b1.id, + name: "b1", + childCategories: [ + { + id: b11.id, + name: "b11", + childCategories: [] + }, + { + id: b12.id, + name: "b12", + childCategories: [] + } + ] + } + ]); + }))); + + it("findTrees should filter by depth if optionally provided", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + + const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}); + categoriesTreeWithEmptyOptions.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + + const categoriesTreeWithDepthZero = await categoryRepository.findTrees({ depth: 0 }); + categoriesTreeWithDepthZero.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [] + } + ]); + + const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }); + categoriesTreeWithDepthOne.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + }))); + }); + + describe("findDescendantsTree() tests", () => { + it("findDescendantsTree should load all category descendents and nested children", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findDescendantsTree(a1); + categoriesTree.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + }))); + + it("findDescendantsTree should filter by depth if optionally provided", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findDescendantsTree(a1); + categoriesTree.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + + const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}); + categoriesTreeWithEmptyOptions.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + + const categoriesTreeWithDepthZero = await categoryRepository.findDescendantsTree(a1, { depth: 0 }); + categoriesTreeWithDepthZero.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [] + }); + + const categoriesTreeWithDepthOne = await categoryRepository.findDescendantsTree(a1, { depth: 1 }); + categoriesTreeWithDepthOne.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + }))); + }); }); diff --git a/test/functional/tree-tables/materialized-path/materialized-path.ts b/test/functional/tree-tables/materialized-path/materialized-path.ts index 2986c445a1a..4a365015b9e 100644 --- a/test/functional/tree-tables/materialized-path/materialized-path.ts +++ b/test/functional/tree-tables/materialized-path/materialized-path.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; -import {Category} from "./entity/Category"; -import {Connection} from "../../../../src/connection/Connection"; -import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; +import { Category } from "./entity/Category"; +import { Connection } from "../../../../src/connection/Connection"; +import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../../utils/test-utils"; describe("tree tables > materialized-path", () => { @@ -164,4 +164,414 @@ describe("tree tables > materialized-path", () => { a1ChildrenNames.should.deep.include("a112"); }))); + describe("findTrees() tests", () => { + it("findTrees should load all category roots and attached children", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + }))); + + it("findTrees should load multiple category roots if they exist", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const b1 = new Category(); + b1.name = "b1"; + + const b11 = new Category(); + b11.name = "b11"; + + const b12 = new Category(); + b12.name = "b12"; + + b1.childCategories = [b11, b12]; + await categoryRepository.save(b1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }, { + id: b1.id, + name: "b1", + childCategories: [ + { + id: b11.id, + name: "b11", + childCategories: [] + }, + { + id: b12.id, + name: "b12", + childCategories: [] + } + ] + } + ]); + }))); + + it("findTrees should filter by depth if optionally provided", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + + const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}); + categoriesTreeWithEmptyOptions.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + + const categoriesTreeWithDepthZero = await categoryRepository.findTrees({ depth: 0 }); + categoriesTreeWithDepthZero.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [] + } + ]); + + const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }); + categoriesTreeWithDepthOne.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + }))); + }); + + describe("findDescendantsTree() tests", () => { + it("findDescendantsTree should load all category descendents and nested children", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findDescendantsTree(a1); + categoriesTree.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + }))); + + it("findDescendantsTree should filter by depth if optionally provided", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findDescendantsTree(a1); + categoriesTree.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + + const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}); + categoriesTreeWithEmptyOptions.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + + const categoriesTreeWithDepthZero = await categoryRepository.findDescendantsTree(a1, { depth: 0 }); + categoriesTreeWithDepthZero.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [] + }); + + const categoriesTreeWithDepthOne = await categoryRepository.findDescendantsTree(a1, { depth: 1 }); + categoriesTreeWithDepthOne.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + }))); + }); }); diff --git a/test/functional/tree-tables/nested-set/nested-set.ts b/test/functional/tree-tables/nested-set/nested-set.ts index e4628e8e2e2..1f4cb053eb6 100644 --- a/test/functional/tree-tables/nested-set/nested-set.ts +++ b/test/functional/tree-tables/nested-set/nested-set.ts @@ -1,7 +1,8 @@ import "reflect-metadata"; -import {Category} from "./entity/Category"; -import {Connection} from "../../../../src/connection/Connection"; -import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; +import { Category } from "./entity/Category"; +import { Connection } from "../../../../src/connection/Connection"; +import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../../utils/test-utils"; +import { expect } from "chai"; describe("tree tables > nested-set", () => { @@ -164,4 +165,347 @@ describe("tree tables > nested-set", () => { a1ChildrenNames.should.deep.include("a112"); }))); + describe("findTrees() tests", () => { + it("findTrees should load all category roots and attached children", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + }))); + + it("findTrees should filter by depth if optionally provided", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findTrees(); + categoriesTree.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + + const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}); + categoriesTreeWithEmptyOptions.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + + const categoriesTreeWithDepthZero = await categoryRepository.findTrees({ depth: 0 }); + categoriesTreeWithDepthZero.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [] + } + ]); + + const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }); + categoriesTreeWithDepthOne.should.be.eql([ + { + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + } + ]); + }))); + + it("findTrees should present a meaningful error message when used with multiple roots + nested sets", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + await categoryRepository.save(a1); + + const b1 = new Category(); + b1.name = "b1"; + + expect(categoryRepository.save(b1)).to.eventually.throw("Nested sets do not support multiple root entities."); + }))); + }); + + describe("findDescendantsTree() tests", () => { + it("findDescendantsTree should load all category descendents and nested children", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findDescendantsTree(a1); + categoriesTree.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + }))); + + it("findDescendantsTree should filter by depth if optionally provided", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + + const a11 = new Category(); + a11.name = "a11"; + + const a12 = new Category(); + a12.name = "a12"; + + const a111 = new Category(); + a111.name = "a111"; + + const a112 = new Category(); + a112.name = "a112"; + + a1.childCategories = [a11, a12]; + a11.childCategories = [a111, a112]; + await categoryRepository.save(a1); + + const categoriesTree = await categoryRepository.findDescendantsTree(a1); + categoriesTree.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + + const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}); + categoriesTreeWithEmptyOptions.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [ + { + id: a111.id, + name: "a111", + childCategories: [] + }, + { + id: a112.id, + name: "a112", + childCategories: [] + } + ] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + + const categoriesTreeWithDepthZero = await categoryRepository.findDescendantsTree(a1, { depth: 0 }); + categoriesTreeWithDepthZero.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [] + }); + + const categoriesTreeWithDepthOne = await categoryRepository.findDescendantsTree(a1, { depth: 1 }); + categoriesTreeWithDepthOne.should.be.eql({ + id: a1.id, + name: "a1", + childCategories: [ + { + id: a11.id, + name: "a11", + childCategories: [] + }, + { + id: a12.id, + name: "a12", + childCategories: [] + } + ] + }); + }))); + }); });