Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add relations option to tree queries #7981

Merged
merged 7 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/tree-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/find-options/FindOptionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>, allRelations: string[], alias: string, metadata: EntityMetadata, prefix: string): void {
public static applyRelationsRecursively(qb: SelectQueryBuilder<any>, allRelations: string[], alias: string, metadata: EntityMetadata, prefix: string): void {
TheProgrammer21 marked this conversation as resolved.
Show resolved Hide resolved

// find all relations that match given prefix
let matchedBaseRelations: string[] = [];
Expand Down
11 changes: 11 additions & 0 deletions src/find-options/FindTreeOptions.ts
Original file line number Diff line number Diff line change
@@ -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[];

}
59 changes: 45 additions & 14 deletions src/repository/TreeRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -18,23 +21,38 @@ export class TreeRepository<Entity> extends Repository<Entity> {
/**
* Gets complete trees for all roots in the table.
*/
async findTrees(): Promise<Entity[]> {
const roots = await this.findRoots();
await Promise.all(roots.map(root => this.findDescendantsTree(root)));
async findTrees(options?: FindTreeOptions): Promise<Entity[]> {
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<Entity[]> {
findRoots(options?: FindTreeOptions): Promise<Entity[]> {
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();
}
Expand All @@ -51,16 +69,29 @@ export class TreeRepository<Entity> extends Repository<Entity> {
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
*/
findDescendantsTree(entity: Entity): Promise<Entity> {
async findDescendantsTree(entity: Entity, options?: FindTreeOptions): Promise<Entity> {
// 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<Entity> = 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;
}

/**
Expand Down
25 changes: 25 additions & 0 deletions test/github-issues/7974/entity/Category.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
21 changes: 21 additions & 0 deletions test/github-issues/7974/entity/Site.ts
Original file line number Diff line number Diff line change
@@ -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;
}
125 changes: 125 additions & 0 deletions test/github-issues/7974/issue-7974.ts
Original file line number Diff line number Diff line change
@@ -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");
})));

});