/
TreeRepository.ts
317 lines (263 loc) · 15 KB
/
TreeRepository.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import {Repository} from "./Repository";
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.
*
* @see Repository
*/
export class TreeRepository<Entity> extends Repository<Entity> {
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Gets complete trees for all roots in the table.
*/
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(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
);
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();
}
/**
* 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();
}
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
*/
async findDescendantsTree(entity: Entity, options?: FindTreeOptions): Promise<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);
}
const entities = await qb.getRawAndEntities();
const relationMaps = this.createRelationMaps("treeEntity", entities.raw);
this.buildChildrenEntityTree(entity, entities.entities, relationMaps);
return entity;
}
/**
* Gets number of descendants of the entity.
*/
countDescendants(entity: Entity): Promise<number> {
return this
.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
.getCount();
}
/**
* Creates a query builder used to get descendants of the entities in a tree.
*/
createDescendantsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
// create shortcuts for better readability
const escape = (alias: string) => this.manager.connection.driver.escape(alias);
if (this.metadata.treeType === "closure-table") {
const joinCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => {
return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = " + escape(alias) + "." + escape(column.referencedColumn!.propertyPath);
}).join(" AND ");
const parameters: ObjectLiteral = {};
const whereCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => {
parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity);
return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = :" + column.referencedColumn!.propertyName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
.where(whereCondition)
.setParameters(parameters);
} else if (this.metadata.treeType === "nested-set") {
const whereCondition = alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " +
"joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND joined." + this.metadata.nestedSetRightColumn!.propertyPath;
const parameters: ObjectLiteral = {};
const joinCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_");
parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity);
return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.targetName, "joined", whereCondition)
.where(joinCondition, parameters);
} else if (this.metadata.treeType === "materialized-path") {
return this
.createQueryBuilder(alias)
.where(qb => {
const subQuery = qb.subQuery()
.select(`${this.metadata.targetName}.${this.metadata.materializedPathColumn!.propertyPath}`, "path")
.from(this.metadata.target, this.metadata.targetName)
.whereInIds(this.metadata.getEntityIdMap(entity));
if (this.manager.connection.driver instanceof AbstractSqliteDriver) {
return `${alias}.${this.metadata.materializedPathColumn!.propertyPath} LIKE ${subQuery.getQuery()} || '%'`;
} else {
return `${alias}.${this.metadata.materializedPathColumn!.propertyPath} LIKE CONCAT(${subQuery.getQuery()}, '%')`;
}
});
}
throw new TypeORMError(`Supported only in tree entities`);
}
/**
* 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();
}
/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
*/
findAncestorsTree(entity: Entity): 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;
});
}
/**
* Gets number of ancestors of the entity.
*/
countAncestors(entity: Entity): Promise<number> {
return this
.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
.getCount();
}
/**
* Creates a query builder used to get ancestors of the entities in the tree.
*/
createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
// create shortcuts for better readability
// const escape = (alias: string) => this.manager.connection.driver.escape(alias);
if (this.metadata.treeType === "closure-table") {
const joinCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => {
return closureTableAlias + "." + column.propertyPath + " = " + alias + "." + column.referencedColumn!.propertyPath;
}).join(" AND ");
const parameters: ObjectLiteral = {};
const whereCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => {
parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity);
return closureTableAlias + "." + column.propertyPath + " = :" + column.referencedColumn!.propertyName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
.where(whereCondition)
.setParameters(parameters);
} else if (this.metadata.treeType === "nested-set") {
const joinCondition = "joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " +
alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND " + alias + "." + this.metadata.nestedSetRightColumn!.propertyPath;
const parameters: ObjectLiteral = {};
const whereCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_");
parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity);
return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.targetName, "joined", joinCondition)
.where(whereCondition, parameters);
} else if (this.metadata.treeType === "materialized-path") {
// example: SELECT * FROM category category WHERE (SELECT mpath FROM `category` WHERE id = 2) LIKE CONCAT(category.mpath, '%');
return this
.createQueryBuilder(alias)
.where(qb => {
const subQuery = qb.subQuery()
.select(`${this.metadata.targetName}.${this.metadata.materializedPathColumn!.propertyPath}`, "path")
.from(this.metadata.target, this.metadata.targetName)
.whereInIds(this.metadata.getEntityIdMap(entity));
if (this.manager.connection.driver instanceof AbstractSqliteDriver) {
return `${subQuery.getQuery()} LIKE ${alias}.${this.metadata.materializedPathColumn!.propertyPath} || '%'`;
} else {
return `${subQuery.getQuery()} LIKE CONCAT(${alias}.${this.metadata.materializedPathColumn!.propertyPath}, '%')`;
}
});
}
throw new TypeORMError(`Supported only in tree entities`);
}
/**
* Moves entity to the children of then given entity.
*
move(entity: Entity, to: Entity): Promise<void> {
return Promise.resolve();
} */
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
protected createRelationMaps(alias: string, rawResults: any[]): { id: any, parentId: any }[] {
return rawResults.map(rawResult => {
const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
// fixes issue #2518, default to databaseName property when givenDatabaseName is not set
const joinColumnName = joinColumn.givenDatabaseName || joinColumn.databaseName;
const id = rawResult[alias + "_" + this.metadata.primaryColumns[0].databaseName];
const parentId = rawResult[alias + "_" + joinColumnName];
return {
id: this.manager.connection.driver.prepareHydratedValue(id, this.metadata.primaryColumns[0]),
parentId: this.manager.connection.driver.prepareHydratedValue(parentId, joinColumn),
};
});
}
protected buildChildrenEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void {
const childProperty = this.metadata.treeChildrenRelation!.propertyName;
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);
});
}
protected buildParentEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void {
const parentProperty = this.metadata.treeParentRelation!.propertyName;
const entityId = this.metadata.primaryColumns[0].getEntityValue(entity);
const parentRelationMap = relationMaps.find(relationMap => relationMap.id === entityId);
const parentEntity = entities.find(entity => {
if (!parentRelationMap)
return false;
return this.metadata.primaryColumns[0].getEntityValue(entity) === parentRelationMap.parentId;
});
if (parentEntity) {
entity[parentProperty] = parentEntity;
this.buildParentEntityTree(entity[parentProperty], entities, relationMaps);
}
}
}