1 5
import {Repository} from "./Repository";
2
import {SelectQueryBuilder} from "../query-builder/SelectQueryBuilder";
3
import {ObjectLiteral} from "../common/ObjectLiteral";
4 5
import {AbstractSqliteDriver} from "../driver/sqlite-abstract/AbstractSqliteDriver";
5

6
/**
7
 * Repository with additional functions to work with trees.
8
 *
9
 * @see Repository
10
 */
11 5
export class TreeRepository<Entity> extends Repository<Entity> {
12

13
    // todo: implement moving
14
    // todo: implement removing
15

16
    // -------------------------------------------------------------------------
17
    // Public Methods
18
    // -------------------------------------------------------------------------
19

20
    /**
21
     * Gets complete trees for all roots in the table.
22
     */
23 5
    async findTrees(): Promise<Entity[]> {
24 5
        const roots = await this.findRoots();
25 5
        await Promise.all(roots.map(root => this.findDescendantsTree(root)));
26 5
        return roots;
27
    }
28

29
    /**
30
     * Roots are entities that have no ancestors. Finds them all.
31
     */
32 5
    findRoots(): Promise<Entity[]> {
33 5
        const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
34 5
        const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
35 5
        const parentPropertyName = this.manager.connection.namingStrategy.joinColumnName(
36
          this.metadata.treeParentRelation!.propertyName, this.metadata.primaryColumns[0].propertyName
37
        );
38

39 5
        return this.createQueryBuilder("treeEntity")
40
            .where(`${escapeAlias("treeEntity")}.${escapeColumn(parentPropertyName)} IS NULL`)
41
            .getMany();
42
    }
43

44
    /**
45
     * Gets all children (descendants) of the given entity. Returns them all in a flat array.
46
     */
47 5
    findDescendants(entity: Entity): Promise<Entity[]> {
48 5
        return this
49
            .createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
50
            .getMany();
51
    }
52

53
    /**
54
     * Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
55
     */
56 5
    findDescendantsTree(entity: Entity): Promise<Entity> {
57
        // todo: throw exception if there is no column of this relation?
58 5
        return this
59
            .createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
60
            .getRawAndEntities()
61 5
            .then(entitiesAndScalars => {
62 5
                const relationMaps = this.createRelationMaps("treeEntity", entitiesAndScalars.raw);
63 5
                this.buildChildrenEntityTree(entity, entitiesAndScalars.entities, relationMaps);
64 5
                return entity;
65
            });
66
    }
67

68
    /**
69
     * Gets number of descendants of the entity.
70
     */
71 5
    countDescendants(entity: Entity): Promise<number> {
72 0
        return this
73
            .createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
74
            .getCount();
75
    }
76

77
    /**
78
     * Creates a query builder used to get descendants of the entities in a tree.
79
     */
80 5
    createDescendantsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
81

82
        // create shortcuts for better readability
83 5
        const escape = (alias: string) => this.manager.connection.driver.escape(alias);
84

85 5
        if (this.metadata.treeType === "closure-table") {
86

87 5
            const joinCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => {
88 5
                return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = " + escape(alias) + "." + escape(column.referencedColumn!.propertyPath);
89
            }).join(" AND ");
90

91 5
            const parameters: ObjectLiteral = {};
92 5
            const whereCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => {
93 5
                parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity);
94 5
                return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = :" + column.referencedColumn!.propertyName;
95
            }).join(" AND ");
96

97 5
            return this
98
                .createQueryBuilder(alias)
99
                .innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
100
                .where(whereCondition)
101
                .setParameters(parameters);
102

103 5
        } else if (this.metadata.treeType === "nested-set") {
104

105 5
            const whereCondition = alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " +
106
                "joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND joined." + this.metadata.nestedSetRightColumn!.propertyPath;
107 5
            const parameters: ObjectLiteral = {};
108 5
            const joinCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
109 5
                const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_");
110 5
                parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity);
111 5
                return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName;
112
            }).join(" AND ");
113

114 5
            return this
115
                .createQueryBuilder(alias)
116
                .innerJoin(this.metadata.targetName, "joined", whereCondition)
117
                .where(joinCondition, parameters);
118

119 5
        } else if (this.metadata.treeType === "materialized-path") {
120 5
            return this
121
                .createQueryBuilder(alias)
122 5
                .where(qb => {
123 5
                    const subQuery = qb.subQuery()
124
                        .select(`${this.metadata.targetName}.${this.metadata.materializedPathColumn!.propertyPath}`, "path")
125
                        .from(this.metadata.target, this.metadata.targetName)
126
                        .whereInIds(this.metadata.getEntityIdMap(entity));
127

128 5
                    qb.setNativeParameters(subQuery.expressionMap.nativeParameters);
129 5
                    if (this.manager.connection.driver instanceof AbstractSqliteDriver) {
130 3
                        return `${alias}.${this.metadata.materializedPathColumn!.propertyPath} LIKE ${subQuery.getQuery()} || '%'`;
131
                    } else {
132 5
                        return `${alias}.${this.metadata.materializedPathColumn!.propertyPath} LIKE CONCAT(${subQuery.getQuery()}, '%')`;
133
                    }
134
                });
135
        }
136

137 0
        throw new Error(`Supported only in tree entities`);
138
    }
139

140
    /**
141
     * Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
142
     */
143 5
    findAncestors(entity: Entity): Promise<Entity[]> {
144 5
        return this
145
            .createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
146
            .getMany();
147
    }
148

149
    /**
150
     * Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
151
     */
152 5
    findAncestorsTree(entity: Entity): Promise<Entity> {
153
        // todo: throw exception if there is no column of this relation?
154 0
        return this
155
            .createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
156
            .getRawAndEntities()
157 0
            .then(entitiesAndScalars => {
158 0
                const relationMaps = this.createRelationMaps("treeEntity", entitiesAndScalars.raw);
159 0
                this.buildParentEntityTree(entity, entitiesAndScalars.entities, relationMaps);
160 0
                return entity;
161
            });
162
    }
163

164
    /**
165
     * Gets number of ancestors of the entity.
166
     */
167 5
    countAncestors(entity: Entity): Promise<number> {
168 0
        return this
169
            .createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
170
            .getCount();
171
    }
172

173
    /**
174
     * Creates a query builder used to get ancestors of the entities in the tree.
175
     */
176 5
    createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
177

178
        // create shortcuts for better readability
179
        // const escape = (alias: string) => this.manager.connection.driver.escape(alias);
180

181 5
        if (this.metadata.treeType === "closure-table") {
182 5
            const joinCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => {
183 5
                return closureTableAlias + "." + column.propertyPath + " = " + alias + "." + column.referencedColumn!.propertyPath;
184
            }).join(" AND ");
185

186 5
            const parameters: ObjectLiteral = {};
187 5
            const whereCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => {
188 5
                parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity);
189 5
                return closureTableAlias + "." + column.propertyPath + " = :" + column.referencedColumn!.propertyName;
190
            }).join(" AND ");
191

192 5
            return this
193
                .createQueryBuilder(alias)
194
                .innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
195
                .where(whereCondition)
196
                .setParameters(parameters);
197

198 5
        } else if (this.metadata.treeType === "nested-set") {
199

200 5
            const joinCondition = "joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " +
201
                alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND " + alias + "." + this.metadata.nestedSetRightColumn!.propertyPath;
202 5
            const parameters: ObjectLiteral = {};
203 5
            const whereCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
204 5
                const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_");
205 5
                parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity);
206 5
                return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName;
207
            }).join(" AND ");
208

209 5
            return this
210
                .createQueryBuilder(alias)
211
                .innerJoin(this.metadata.targetName, "joined", joinCondition)
212
                .where(whereCondition, parameters);
213

214

215 5
        } else if (this.metadata.treeType === "materialized-path") {
216
            // example: SELECT * FROM category category WHERE (SELECT mpath FROM `category` WHERE id = 2) LIKE CONCAT(category.mpath, '%');
217 5
            return this
218
                .createQueryBuilder(alias)
219 5
                .where(qb => {
220 5
                    const subQuery = qb.subQuery()
221
                        .select(`${this.metadata.targetName}.${this.metadata.materializedPathColumn!.propertyPath}`, "path")
222
                        .from(this.metadata.target, this.metadata.targetName)
223
                        .whereInIds(this.metadata.getEntityIdMap(entity));
224

225 5
                    qb.setNativeParameters(subQuery.expressionMap.nativeParameters);
226 5
                    if (this.manager.connection.driver instanceof AbstractSqliteDriver) {
227 3
                        return `${subQuery.getQuery()} LIKE ${alias}.${this.metadata.materializedPathColumn!.propertyPath} || '%'`;
228

229
                    } else {
230 5
                        return `${subQuery.getQuery()} LIKE CONCAT(${alias}.${this.metadata.materializedPathColumn!.propertyPath}, '%')`;
231
                    }
232
                });
233
        }
234

235 0
        throw new Error(`Supported only in tree entities`);
236
    }
237

238
    /**
239
     * Moves entity to the children of then given entity.
240
     *
241
    move(entity: Entity, to: Entity): Promise<void> {
242
        return Promise.resolve();
243
    } */
244

245
    // -------------------------------------------------------------------------
246
    // Protected Methods
247
    // -------------------------------------------------------------------------
248

249 5
    protected createRelationMaps(alias: string, rawResults: any[]): { id: any, parentId: any }[] {
250 5
        return rawResults.map(rawResult => {
251 5
            const joinColumn = this.metadata.treeParentRelation!.joinColumns[0];
252
            // fixes issue #2518, default to databaseName property when givenDatabaseName is not set
253 5
            const joinColumnName = joinColumn.givenDatabaseName || joinColumn.databaseName;
254 5
            const id = rawResult[alias + "_" + this.metadata.primaryColumns[0].databaseName];
255 5
            const parentId = rawResult[alias + "_" + joinColumnName];
256 5
            return {
257
                id: this.manager.connection.driver.prepareHydratedValue(id, this.metadata.primaryColumns[0]),
258
                parentId: this.manager.connection.driver.prepareHydratedValue(parentId, joinColumn),
259
            };
260
        });
261
    }
262

263 5
    protected buildChildrenEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void {
264 5
        const childProperty = this.metadata.treeChildrenRelation!.propertyName;
265 5
        const parentEntityId = this.metadata.primaryColumns[0].getEntityValue(entity);
266 5
        const childRelationMaps = relationMaps.filter(relationMap => relationMap.parentId === parentEntityId);
267 5
        const childIds = new Set(childRelationMaps.map(relationMap => relationMap.id));
268 5
        entity[childProperty] = entities.filter(entity => childIds.has(entity[this.metadata.primaryColumns[0].propertyName]));
269 5
        entity[childProperty].forEach((childEntity: any) => {
270 5
            this.buildChildrenEntityTree(childEntity, entities, relationMaps);
271
        });
272
    }
273

274 5
    protected buildParentEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void {
275 0
        const parentProperty = this.metadata.treeParentRelation!.propertyName;
276 0
        const entityId = this.metadata.primaryColumns[0].getEntityValue(entity);
277 0
        const parentRelationMap = relationMaps.find(relationMap => relationMap.id === entityId);
278 0
        const parentEntity = entities.find(entity => {
279 0
            if (!parentRelationMap)
280 0
                return false;
281

282 0
            return entity[this.metadata.primaryColumns[0].propertyName] === parentRelationMap.parentId;
283
        });
284 0
        if (parentEntity) {
285 0
            entity[parentProperty] = parentEntity;
286 0
            this.buildParentEntityTree(entity[parentProperty], entities, relationMaps);
287
        }
288
    }
289

290 5
}

Read our documentation on viewing source code .

Loading