Skip to content

Commit

Permalink
feat: add relation options to all tree queries (#8080)
Browse files Browse the repository at this point in the history
* feat: add relation options to all tree queries

Closes: #8076

* fix: corrected array index in test case

* try to fix failing test

* fix: sort the array in order to be able to apply tests
  • Loading branch information
TheProgrammer21 committed Nov 8, 2021
1 parent 19d4a91 commit e4d4636
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 64 deletions.
23 changes: 20 additions & 3 deletions docs/tree-entities.md
Expand Up @@ -203,9 +203,6 @@ 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 Expand Up @@ -273,3 +270,23 @@ const parents = await repository
```typescript
const parentsCount = await repository.countAncestors(childCategory);
```

For the following methods, options can be passed:
* findTrees
* findRoots
* findDescendants
* findDescendantsTree
* findAncestors
* findAncestorsTree

The following options are available:
* `relations` - Indicates what relations of entity should be loaded (simplified left join form).

Examples:
```typescript
const treeCategoriesWithRelations = await repository.findTrees({ relations: ["sites"] });
// automatically joins the sites relation

const parentsWithRelations = await repository.findAncestors(childCategory, { relations: ["members"] });
// returns all direct childCategory's parent categories (without "parent of parents") and joins the 'members' relation
```
52 changes: 35 additions & 17 deletions src/find-options/FindOptionsUtils.ts
Expand Up @@ -5,6 +5,7 @@ import {FindRelationsNotFoundError} from "../error/FindRelationsNotFoundError";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {DriverUtils} from "../driver/DriverUtils";
import { TypeORMError } from "../error";
import { FindTreeOptions } from "./FindTreeOptions";

/**
* Utilities to work with FindOptions.
Expand All @@ -21,23 +22,23 @@ export class FindOptionsUtils {
static isFindOneOptions<Entity = any>(obj: any): obj is FindOneOptions<Entity> {
const possibleOptions: FindOneOptions<Entity> = obj;
return possibleOptions &&
(
Array.isArray(possibleOptions.select) ||
possibleOptions.where instanceof Object ||
typeof possibleOptions.where === "string" ||
Array.isArray(possibleOptions.relations) ||
possibleOptions.join instanceof Object ||
possibleOptions.order instanceof Object ||
possibleOptions.cache instanceof Object ||
typeof possibleOptions.cache === "boolean" ||
typeof possibleOptions.cache === "number" ||
possibleOptions.lock instanceof Object ||
possibleOptions.loadRelationIds instanceof Object ||
typeof possibleOptions.loadRelationIds === "boolean" ||
typeof possibleOptions.loadEagerRelations === "boolean" ||
typeof possibleOptions.withDeleted === "boolean" ||
typeof possibleOptions.transaction === "boolean"
);
(
Array.isArray(possibleOptions.select) ||
possibleOptions.where instanceof Object ||
typeof possibleOptions.where === "string" ||
Array.isArray(possibleOptions.relations) ||
possibleOptions.join instanceof Object ||
possibleOptions.order instanceof Object ||
possibleOptions.cache instanceof Object ||
typeof possibleOptions.cache === "boolean" ||
typeof possibleOptions.cache === "number" ||
possibleOptions.lock instanceof Object ||
possibleOptions.loadRelationIds instanceof Object ||
typeof possibleOptions.loadRelationIds === "boolean" ||
typeof possibleOptions.loadEagerRelations === "boolean" ||
typeof possibleOptions.withDeleted === "boolean" ||
typeof possibleOptions.transaction === "boolean"
);
}

/**
Expand Down Expand Up @@ -215,6 +216,23 @@ export class FindOptionsUtils {
return qb;
}

static applyOptionsToTreeQueryBuilder<T>(qb: SelectQueryBuilder<T>, options?: FindTreeOptions): SelectQueryBuilder<T> {
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);
}

return qb;
}

// -------------------------------------------------------------------------
// Protected Static Methods
// -------------------------------------------------------------------------
Expand Down
62 changes: 18 additions & 44 deletions src/repository/TreeRepository.ts
Expand Up @@ -4,7 +4,6 @@ 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";

/**
Expand Down Expand Up @@ -38,19 +37,7 @@ export class TreeRepository<Entity> extends Repository<Entity> {
);

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);
}

FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);

return qb
.where(`${escapeAlias("treeEntity")}.${escapeColumn(parentPropertyName)} IS NULL`)
Expand All @@ -60,10 +47,10 @@ export class TreeRepository<Entity> extends Repository<Entity> {
/**
* Gets all children (descendants) of the given entity. Returns them all in a flat array.
*/
findDescendants(entity: Entity): Promise<Entity[]> {
return this
.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
.getMany();
findDescendants(entity: Entity, options?: FindTreeOptions): Promise<Entity[]> {
const qb = this.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity);
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
return qb.getMany();
}

/**
Expand All @@ -73,19 +60,7 @@ export class TreeRepository<Entity> extends Repository<Entity> {
// todo: throw exception if there is no column of this relation?

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);
}
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);

const entities = await qb.getRawAndEntities();
const relationMaps = this.createRelationMaps("treeEntity", entities.raw);
Expand Down Expand Up @@ -168,25 +143,24 @@ export class TreeRepository<Entity> extends Repository<Entity> {
/**
* Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
*/
findAncestors(entity: Entity): Promise<Entity[]> {
return this
.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
.getMany();
findAncestors(entity: Entity, options?: FindTreeOptions): Promise<Entity[]> {
const qb = this.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity);
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);
return qb.getMany();
}

/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
*/
findAncestorsTree(entity: Entity): Promise<Entity> {
async findAncestorsTree(entity: Entity, options?: FindTreeOptions): Promise<Entity> {
// todo: throw exception if there is no column of this relation?
return this
.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
.getRawAndEntities()
.then(entitiesAndScalars => {
const relationMaps = this.createRelationMaps("treeEntity", entitiesAndScalars.raw);
this.buildParentEntityTree(entity, entitiesAndScalars.entities, relationMaps);
return entity;
});
const qb = this.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity);
FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options);

const entities = await qb.getRawAndEntities();
const relationMaps = this.createRelationMaps("treeEntity", entities.raw);
this.buildParentEntityTree(entity, entities.entities, relationMaps);
return entity;
}

/**
Expand Down
29 changes: 29 additions & 0 deletions test/github-issues/8076/entity/Category.ts
@@ -0,0 +1,29 @@
import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn, Tree, TreeChildren, TreeParent } from "../../../../src";
import { Site } from "./Site";
import { Member } from "./Member";

@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[];

@OneToMany(() => Member, m => m.category)
members: Member[];
}
18 changes: 18 additions & 0 deletions test/github-issues/8076/entity/Member.ts
@@ -0,0 +1,18 @@
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "../../../../src";
import { Category } from "./Category";

@Entity()
export class Member extends BaseEntity {

@PrimaryGeneratedColumn()
pk: number;

@Column({
length: 250,
nullable: false
})
title: string;

@ManyToOne(() => Category, c => c.members)
category: Category;
}
21 changes: 21 additions & 0 deletions test/github-issues/8076/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;
}

0 comments on commit e4d4636

Please sign in to comment.