From ca26297484542498b8f622f540ca354360d53ed0 Mon Sep 17 00:00:00 2001 From: Lukas Windisch <52009814+TheProgrammer21@users.noreply.github.com> Date: Wed, 4 Aug 2021 18:24:21 +0200 Subject: [PATCH] feat: add relations option to tree queries (#7981) * feat: add relation option to tree queries Add possibility to load relations of tree entities Closes: #7974 #4564 * style: remove unused declaration remove unused declaration to satisfy linting. * Update tree-entities.md docs: rename variable for copy paste * Update FindOptionsUtils.ts style: remove prettified code * style: remove prettified code * style: remove prettified code * test: enable test for all drivers --- docs/tree-entities.md | 3 + src/find-options/FindOptionsUtils.ts | 2 +- src/find-options/FindTreeOptions.ts | 11 ++ src/repository/TreeRepository.ts | 59 +++++++--- test/github-issues/7974/entity/Category.ts | 25 +++++ test/github-issues/7974/entity/Site.ts | 21 ++++ test/github-issues/7974/issue-7974.ts | 125 +++++++++++++++++++++ 7 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 src/find-options/FindTreeOptions.ts create mode 100644 test/github-issues/7974/entity/Category.ts create mode 100644 test/github-issues/7974/entity/Site.ts create mode 100644 test/github-issues/7974/issue-7974.ts diff --git a/docs/tree-entities.md b/docs/tree-entities.md index 0a0b5505e0..5cbafc35af 100644 --- a/docs/tree-entities.md +++ b/docs/tree-entities.md @@ -203,6 +203,9 @@ There are other special methods to work with tree entities through `TreeReposito ```typescript const treeCategories = await repository.findTrees(); // returns root categories with sub categories inside + +const treeCategoriesWithRelations = await repository.findTrees({ relations: ["sites"] }); +// automatically joins the sites relation ``` * `findRoots` - Roots are entities that have no ancestors. Finds them all. diff --git a/src/find-options/FindOptionsUtils.ts b/src/find-options/FindOptionsUtils.ts index 61cd07bebb..7fdd4a0e26 100644 --- a/src/find-options/FindOptionsUtils.ts +++ b/src/find-options/FindOptionsUtils.ts @@ -222,7 +222,7 @@ export class FindOptionsUtils { /** * Adds joins for all relations and sub-relations of the given relations provided in the find options. */ - protected static applyRelationsRecursively(qb: SelectQueryBuilder, allRelations: string[], alias: string, metadata: EntityMetadata, prefix: string): void { + public static applyRelationsRecursively(qb: SelectQueryBuilder, allRelations: string[], alias: string, metadata: EntityMetadata, prefix: string): void { // find all relations that match given prefix let matchedBaseRelations: string[] = []; diff --git a/src/find-options/FindTreeOptions.ts b/src/find-options/FindTreeOptions.ts new file mode 100644 index 0000000000..c8eb2002bc --- /dev/null +++ b/src/find-options/FindTreeOptions.ts @@ -0,0 +1,11 @@ +/** + * Defines a special criteria to find specific entities. + */ +export interface FindTreeOptions { + + /** + * Indicates what relations of entity should be loaded (simplified left join form). + */ + relations?: string[]; + +} diff --git a/src/repository/TreeRepository.ts b/src/repository/TreeRepository.ts index b7957344e7..e144e7a1a3 100644 --- a/src/repository/TreeRepository.ts +++ b/src/repository/TreeRepository.ts @@ -3,6 +3,9 @@ import {SelectQueryBuilder} from "../query-builder/SelectQueryBuilder"; import {ObjectLiteral} from "../common/ObjectLiteral"; import {AbstractSqliteDriver} from "../driver/sqlite-abstract/AbstractSqliteDriver"; import { TypeORMError } from "../error/TypeORMError"; +import { FindTreeOptions } from "../find-options/FindTreeOptions"; +import { FindRelationsNotFoundError } from "../error"; +import { FindOptionsUtils } from "../find-options/FindOptionsUtils"; /** * Repository with additional functions to work with trees. @@ -18,23 +21,38 @@ export class TreeRepository extends Repository { /** * Gets complete trees for all roots in the table. */ - async findTrees(): Promise { - const roots = await this.findRoots(); - await Promise.all(roots.map(root => this.findDescendantsTree(root))); + async findTrees(options?: FindTreeOptions): Promise { + const roots = await this.findRoots(options); + await Promise.all(roots.map(root => this.findDescendantsTree(root, options))); return roots; } /** * Roots are entities that have no ancestors. Finds them all. */ - findRoots(): Promise { + findRoots(options?: FindTreeOptions): Promise { const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias); const escapeColumn = (column: string) => this.manager.connection.driver.escape(column); const parentPropertyName = this.manager.connection.namingStrategy.joinColumnName( this.metadata.treeParentRelation!.propertyName, this.metadata.primaryColumns[0].propertyName ); - return this.createQueryBuilder("treeEntity") + const qb = this.createQueryBuilder("treeEntity"); + + if (options?.relations) { + const allRelations = [...options.relations]; + + FindOptionsUtils.applyRelationsRecursively(qb, allRelations, qb.expressionMap.mainAlias!.name, qb.expressionMap.mainAlias!.metadata, ""); + + // recursive removes found relations from allRelations array + // if there are relations left in this array it means those relations were not found in the entity structure + // so, we give an exception about not found relations + if (allRelations.length > 0) + throw new FindRelationsNotFoundError(allRelations); + } + + + return qb .where(`${escapeAlias("treeEntity")}.${escapeColumn(parentPropertyName)} IS NULL`) .getMany(); } @@ -51,16 +69,29 @@ export class TreeRepository extends Repository { /** * Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other. */ - findDescendantsTree(entity: Entity): Promise { + async findDescendantsTree(entity: Entity, options?: FindTreeOptions): Promise { // todo: throw exception if there is no column of this relation? - return this - .createDescendantsQueryBuilder("treeEntity", "treeClosure", entity) - .getRawAndEntities() - .then(entitiesAndScalars => { - const relationMaps = this.createRelationMaps("treeEntity", entitiesAndScalars.raw); - this.buildChildrenEntityTree(entity, entitiesAndScalars.entities, relationMaps); - return entity; - }); + + const qb: SelectQueryBuilder = this.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity); + + if (options?.relations) { + // Copy because `applyRelationsRecursively` modifies it + const allRelations = [...options.relations]; + + FindOptionsUtils.applyRelationsRecursively(qb, allRelations, qb.expressionMap.mainAlias!.name, qb.expressionMap.mainAlias!.metadata, ""); + + // recursive removes found relations from allRelations array + // if there are relations left in this array it means those relations were not found in the entity structure + // so, we give an exception about not found relations + if (allRelations.length > 0) + throw new FindRelationsNotFoundError(allRelations); + } + + const entities = await qb.getRawAndEntities(); + const relationMaps = this.createRelationMaps("treeEntity", entities.raw); + this.buildChildrenEntityTree(entity, entities.entities, relationMaps); + + return entity; } /** diff --git a/test/github-issues/7974/entity/Category.ts b/test/github-issues/7974/entity/Category.ts new file mode 100644 index 0000000000..805913a53e --- /dev/null +++ b/test/github-issues/7974/entity/Category.ts @@ -0,0 +1,25 @@ +import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn, Tree, TreeChildren, TreeParent } from "../../../../src"; +import { Site } from "./Site"; + +@Entity() +@Tree("materialized-path") +export class Category extends BaseEntity { + + @PrimaryGeneratedColumn() + pk: number; + + @Column({ + length: 250, + nullable: false + }) + title: string; + + @TreeParent() + parentCategory: Category | null; + + @TreeChildren() + childCategories: Category[]; + + @OneToMany(() => Site, site => site.parentCategory) + sites: Site[]; +} \ No newline at end of file diff --git a/test/github-issues/7974/entity/Site.ts b/test/github-issues/7974/entity/Site.ts new file mode 100644 index 0000000000..7ce9b5e5a2 --- /dev/null +++ b/test/github-issues/7974/entity/Site.ts @@ -0,0 +1,21 @@ +import { BaseEntity, Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from "../../../../src"; +import { Category } from "./Category"; + +@Entity() +export class Site extends BaseEntity { + + @PrimaryGeneratedColumn() + pk: number; + + @CreateDateColumn() + createdAt: Date; + + @Column({ + length: 250, + nullable: false + }) + title: string; + + @ManyToOne(() => Category) + parentCategory: Category; +} \ No newline at end of file diff --git a/test/github-issues/7974/issue-7974.ts b/test/github-issues/7974/issue-7974.ts new file mode 100644 index 0000000000..31a7a0a1d0 --- /dev/null +++ b/test/github-issues/7974/issue-7974.ts @@ -0,0 +1,125 @@ +import { expect } from "chai"; +import "reflect-metadata"; +import { Connection } from "../../../src"; +import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../utils/test-utils"; +import { Category } from "./entity/Category"; +import { Site } from "./entity/Site"; + +describe("github issues > #7974 Adding relations option to findTrees()", () => { + + let connections: Connection[]; + + before(async () => connections = await createTestingConnections({ + entities: [Category, Site], + schemaCreate: true, + dropSchema: true + })); + + beforeEach(async () => { + await reloadTestingDatabases(connections); + for (let connection of connections) { + let categoryRepo = connection.getRepository(Category); + let siteRepo = connection.getRepository(Site); + + let c1: Category = new Category(); + c1.title = "Category 1"; + c1.parentCategory = null; + c1.childCategories = []; + c1.sites = []; + + let c2: Category = new Category(); + c2.title = "Category 2"; + c2.parentCategory = null; + c2.childCategories = []; + c2.sites = []; + + let c3: Category = new Category(); + c3.title = "Category 1.1"; + c3.parentCategory = c1; + c3.childCategories = []; + c3.sites = []; + + let c4: Category = new Category(); + c4.title = "Category 1.1.1"; + c4.parentCategory = c3; + c4.childCategories = []; + c4.sites = []; + + c1.childCategories = [c3]; + c3.childCategories = [c4]; + + let s1: Site = new Site(); + s1.title = "Site of Category 1"; + s1.parentCategory = c1; + + let s2: Site = new Site(); + s2.title = "Site of Category 1"; + s2.parentCategory = c1; + + let s3: Site = new Site(); + s3.title = "Site of Category 1.1"; + s3.parentCategory = c3; + + let s4: Site = new Site(); + s4.title = "Site of Category 1.1"; + s4.parentCategory = c3; + + let s5: Site = new Site(); + s5.title = "Site of Category 1.1.1"; + s5.parentCategory = c4; + + // Create the categories + c1 = await categoryRepo.save(c1); + c2 = await categoryRepo.save(c2); + c3 = await categoryRepo.save(c3); + c4 = await categoryRepo.save(c4); + + // Create the sites + [s1, s2, s3, s4, s5] = + await Promise.all([ + siteRepo.save(s1), + siteRepo.save(s2), + siteRepo.save(s3), + siteRepo.save(s4), + siteRepo.save(s5) + ]); + + // Set the just created relations correctly + c1.sites = [s1, s2]; + c2.sites = []; + c3.sites = [s3, s4]; + c4.sites = [s5]; + } + }); + + after(() => closeTestingConnections(connections)); + + it("should return tree without sites relations", async () => await Promise.all(connections.map(async connection => { + + let result = await connection.getTreeRepository(Category).findTrees(); + + // The complete tree should exist but other relations than the parent- / child-relations should not be loaded + expect(result).to.have.lengthOf(2); + expect(result[0].sites).equals(undefined); + expect(result[0].childCategories).to.have.lengthOf(1); + expect(result[0].childCategories[0].childCategories).to.have.lengthOf(1); + expect(result[0].childCategories[0].childCategories[0].sites).equal(undefined); + + }))); + + it("should return tree with sites relations", async () => await Promise.all(connections.map(async connection => { + + let result = await connection.getTreeRepository(Category).findTrees({ relations: ["sites"] }); + + // The complete tree should exist and site relations should not be loaded for every category + expect(result).to.have.lengthOf(2); + expect(result[0].sites).lengthOf(2); + expect(result[1].sites).to.be.an("array"); + expect(result[1].sites).to.have.lengthOf(0); + expect(result[0].childCategories[0].sites).to.have.lengthOf(2); + expect(result[0].childCategories[0].childCategories[0].sites).to.have.lengthOf(1); + expect(result[0].childCategories[0].childCategories[0].sites).to.be.an("array"); + expect(result[0].childCategories[0].childCategories[0].sites[0].title).to.be.equal("Site of Category 1.1.1"); + }))); + +});