• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

typeorm / typeorm / 15219332477

23 May 2025 09:13PM UTC coverage: 17.216% (-59.1%) from 76.346%
15219332477

Pull #11332

github

naorpeled
cr comments - move if block
Pull Request #11332: feat: add new undefined and null behavior flags

1603 of 12759 branches covered (12.56%)

Branch coverage included in aggregate %.

0 of 31 new or added lines in 3 files covered. (0.0%)

14132 existing lines in 166 files now uncovered.

4731 of 24033 relevant lines covered (19.69%)

60.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

5.83
/src/repository/TreeRepository.ts
1
import { ObjectLiteral } from "../common/ObjectLiteral"
2
import { DriverUtils } from "../driver/DriverUtils"
1✔
3
import { TypeORMError } from "../error/TypeORMError"
1✔
4
import { FindOptionsUtils } from "../find-options/FindOptionsUtils"
1✔
5
import { FindTreeOptions } from "../find-options/FindTreeOptions"
6
import { SelectQueryBuilder } from "../query-builder/SelectQueryBuilder"
7
import { TreeRepositoryUtils } from "../util/TreeRepositoryUtils"
1✔
8
import { Repository } from "./Repository"
1✔
9

10
/**
11
 * Repository with additional functions to work with trees.
12
 *
13
 * @see Repository
14
 */
15
export class TreeRepository<
1✔
16
    Entity extends ObjectLiteral,
17
> extends Repository<Entity> {
18
    // -------------------------------------------------------------------------
19
    // Public Methods
20
    // -------------------------------------------------------------------------
21

22
    /**
23
     * Gets complete trees for all roots in the table.
24
     */
25
    async findTrees(options?: FindTreeOptions): Promise<Entity[]> {
UNCOV
26
        const roots = await this.findRoots(options)
×
UNCOV
27
        await Promise.all(
×
UNCOV
28
            roots.map((root) => this.findDescendantsTree(root, options)),
×
29
        )
UNCOV
30
        return roots
×
31
    }
32

33
    /**
34
     * Roots are entities that have no ancestors. Finds them all.
35
     */
36
    findRoots(options?: FindTreeOptions): Promise<Entity[]> {
UNCOV
37
        const escapeAlias = (alias: string) =>
×
UNCOV
38
            this.manager.connection.driver.escape(alias)
×
UNCOV
39
        const escapeColumn = (column: string) =>
×
UNCOV
40
            this.manager.connection.driver.escape(column)
×
41

UNCOV
42
        const joinColumn = this.metadata.treeParentRelation!.joinColumns[0]
×
43
        const parentPropertyName =
UNCOV
44
            joinColumn.givenDatabaseName || joinColumn.databaseName
×
45

UNCOV
46
        const qb = this.createQueryBuilder("treeEntity")
×
UNCOV
47
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options)
×
48

UNCOV
49
        return qb
×
50
            .where(
51
                `${escapeAlias("treeEntity")}.${escapeColumn(
52
                    parentPropertyName,
53
                )} IS NULL`,
54
            )
55
            .getMany()
56
    }
57

58
    /**
59
     * Gets all children (descendants) of the given entity. Returns them all in a flat array.
60
     */
61
    findDescendants(
62
        entity: Entity,
63
        options?: FindTreeOptions,
64
    ): Promise<Entity[]> {
UNCOV
65
        const qb = this.createDescendantsQueryBuilder(
×
66
            "treeEntity",
67
            "treeClosure",
68
            entity,
69
        )
UNCOV
70
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options)
×
UNCOV
71
        return qb.getMany()
×
72
    }
73

74
    /**
75
     * Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
76
     */
77
    async findDescendantsTree(
78
        entity: Entity,
79
        options?: FindTreeOptions,
80
    ): Promise<Entity> {
81
        // todo: throw exception if there is no column of this relation?
82

83
        const qb: SelectQueryBuilder<Entity> =
UNCOV
84
            this.createDescendantsQueryBuilder(
×
85
                "treeEntity",
86
                "treeClosure",
87
                entity,
88
            )
UNCOV
89
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options)
×
90

UNCOV
91
        const entities = await qb.getRawAndEntities()
×
UNCOV
92
        const relationMaps = TreeRepositoryUtils.createRelationMaps(
×
93
            this.manager,
94
            this.metadata,
95
            "treeEntity",
96
            entities.raw,
97
        )
UNCOV
98
        TreeRepositoryUtils.buildChildrenEntityTree(
×
99
            this.metadata,
100
            entity,
101
            entities.entities,
102
            relationMaps,
103
            {
104
                depth: -1,
105
                ...options,
106
            },
107
        )
108

UNCOV
109
        return entity
×
110
    }
111

112
    /**
113
     * Gets number of descendants of the entity.
114
     */
115
    countDescendants(entity: Entity): Promise<number> {
UNCOV
116
        return this.createDescendantsQueryBuilder(
×
117
            "treeEntity",
118
            "treeClosure",
119
            entity,
120
        ).getCount()
121
    }
122

123
    /**
124
     * Creates a query builder used to get descendants of the entities in a tree.
125
     */
126
    createDescendantsQueryBuilder(
127
        alias: string,
128
        closureTableAlias: string,
129
        entity: Entity,
130
    ): SelectQueryBuilder<Entity> {
131
        // create shortcuts for better readability
UNCOV
132
        const escape = (alias: string) =>
×
UNCOV
133
            this.manager.connection.driver.escape(alias)
×
134

UNCOV
135
        if (this.metadata.treeType === "closure-table") {
×
136
            const joinCondition =
UNCOV
137
                this.metadata.closureJunctionTable.descendantColumns
×
138
                    .map((column) => {
UNCOV
139
                        return (
×
140
                            escape(closureTableAlias) +
141
                            "." +
142
                            escape(column.propertyPath) +
143
                            " = " +
144
                            escape(alias) +
145
                            "." +
146
                            escape(column.referencedColumn!.propertyPath)
147
                        )
148
                    })
149
                    .join(" AND ")
150

UNCOV
151
            const parameters: ObjectLiteral = {}
×
152
            const whereCondition =
UNCOV
153
                this.metadata.closureJunctionTable.ancestorColumns
×
154
                    .map((column) => {
UNCOV
155
                        parameters[column.referencedColumn!.propertyName] =
×
156
                            column.referencedColumn!.getEntityValue(entity)
UNCOV
157
                        return (
×
158
                            escape(closureTableAlias) +
159
                            "." +
160
                            escape(column.propertyPath) +
161
                            " = :" +
162
                            column.referencedColumn!.propertyName
163
                        )
164
                    })
165
                    .join(" AND ")
166

UNCOV
167
            return this.createQueryBuilder(alias)
×
168
                .innerJoin(
169
                    this.metadata.closureJunctionTable.tableName,
170
                    closureTableAlias,
171
                    joinCondition,
172
                )
173
                .where(whereCondition)
174
                .setParameters(parameters)
UNCOV
175
        } else if (this.metadata.treeType === "nested-set") {
×
176
            const whereCondition =
UNCOV
177
                alias +
×
178
                "." +
179
                this.metadata.nestedSetLeftColumn!.propertyPath +
180
                " BETWEEN " +
181
                "joined." +
182
                this.metadata.nestedSetLeftColumn!.propertyPath +
183
                " AND joined." +
184
                this.metadata.nestedSetRightColumn!.propertyPath
UNCOV
185
            const parameters: ObjectLiteral = {}
×
UNCOV
186
            const joinCondition = this.metadata
×
187
                .treeParentRelation!.joinColumns.map((joinColumn) => {
188
                    const parameterName =
UNCOV
189
                        joinColumn.referencedColumn!.propertyPath.replace(
×
190
                            ".",
191
                            "_",
192
                        )
UNCOV
193
                    parameters[parameterName] =
×
194
                        joinColumn.referencedColumn!.getEntityValue(entity)
UNCOV
195
                    return (
×
196
                        "joined." +
197
                        joinColumn.referencedColumn!.propertyPath +
198
                        " = :" +
199
                        parameterName
200
                    )
201
                })
202
                .join(" AND ")
203

UNCOV
204
            return this.createQueryBuilder(alias)
×
205
                .innerJoin(this.metadata.targetName, "joined", whereCondition)
206
                .where(joinCondition, parameters)
UNCOV
207
        } else if (this.metadata.treeType === "materialized-path") {
×
UNCOV
208
            return this.createQueryBuilder(alias).where((qb) => {
×
UNCOV
209
                const subQuery = qb
×
210
                    .subQuery()
211
                    .select(
212
                        `${this.metadata.targetName}.${
213
                            this.metadata.materializedPathColumn!.propertyPath
214
                        }`,
215
                        "path",
216
                    )
217
                    .from(this.metadata.target, this.metadata.targetName)
218
                    .whereInIds(this.metadata.getEntityIdMap(entity))
219

UNCOV
220
                if (
×
221
                    DriverUtils.isSQLiteFamily(this.manager.connection.driver)
222
                ) {
UNCOV
223
                    return `${alias}.${
×
224
                        this.metadata.materializedPathColumn!.propertyPath
225
                    } LIKE ${subQuery.getQuery()} || '%'`
226
                } else {
UNCOV
227
                    return `${alias}.${
×
228
                        this.metadata.materializedPathColumn!.propertyPath
229
                    } LIKE NULLIF(CONCAT(${subQuery.getQuery()}, '%'), '%')`
230
                }
231
            })
232
        }
233

234
        throw new TypeORMError(`Supported only in tree entities`)
×
235
    }
236

237
    /**
238
     * Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
239
     */
240
    findAncestors(
241
        entity: Entity,
242
        options?: FindTreeOptions,
243
    ): Promise<Entity[]> {
UNCOV
244
        const qb = this.createAncestorsQueryBuilder(
×
245
            "treeEntity",
246
            "treeClosure",
247
            entity,
248
        )
UNCOV
249
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options)
×
UNCOV
250
        return qb.getMany()
×
251
    }
252

253
    /**
254
     * Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
255
     */
256
    async findAncestorsTree(
257
        entity: Entity,
258
        options?: FindTreeOptions,
259
    ): Promise<Entity> {
260
        // todo: throw exception if there is no column of this relation?
UNCOV
261
        const qb = this.createAncestorsQueryBuilder(
×
262
            "treeEntity",
263
            "treeClosure",
264
            entity,
265
        )
UNCOV
266
        FindOptionsUtils.applyOptionsToTreeQueryBuilder(qb, options)
×
267

UNCOV
268
        const entities = await qb.getRawAndEntities()
×
UNCOV
269
        const relationMaps = TreeRepositoryUtils.createRelationMaps(
×
270
            this.manager,
271
            this.metadata,
272
            "treeEntity",
273
            entities.raw,
274
        )
UNCOV
275
        TreeRepositoryUtils.buildParentEntityTree(
×
276
            this.metadata,
277
            entity,
278
            entities.entities,
279
            relationMaps,
280
        )
UNCOV
281
        return entity
×
282
    }
283

284
    /**
285
     * Gets number of ancestors of the entity.
286
     */
287
    countAncestors(entity: Entity): Promise<number> {
288
        return this.createAncestorsQueryBuilder(
×
289
            "treeEntity",
290
            "treeClosure",
291
            entity,
292
        ).getCount()
293
    }
294

295
    /**
296
     * Creates a query builder used to get ancestors of the entities in the tree.
297
     */
298
    createAncestorsQueryBuilder(
299
        alias: string,
300
        closureTableAlias: string,
301
        entity: Entity,
302
    ): SelectQueryBuilder<Entity> {
303
        // create shortcuts for better readability
304
        // const escape = (alias: string) => this.manager.connection.driver.escape(alias);
305

UNCOV
306
        if (this.metadata.treeType === "closure-table") {
×
307
            const joinCondition =
UNCOV
308
                this.metadata.closureJunctionTable.ancestorColumns
×
309
                    .map((column) => {
UNCOV
310
                        return (
×
311
                            closureTableAlias +
312
                            "." +
313
                            column.propertyPath +
314
                            " = " +
315
                            alias +
316
                            "." +
317
                            column.referencedColumn!.propertyPath
318
                        )
319
                    })
320
                    .join(" AND ")
321

UNCOV
322
            const parameters: ObjectLiteral = {}
×
323
            const whereCondition =
UNCOV
324
                this.metadata.closureJunctionTable.descendantColumns
×
325
                    .map((column) => {
UNCOV
326
                        parameters[column.referencedColumn!.propertyName] =
×
327
                            column.referencedColumn!.getEntityValue(entity)
UNCOV
328
                        return (
×
329
                            closureTableAlias +
330
                            "." +
331
                            column.propertyPath +
332
                            " = :" +
333
                            column.referencedColumn!.propertyName
334
                        )
335
                    })
336
                    .join(" AND ")
337

UNCOV
338
            return this.createQueryBuilder(alias)
×
339
                .innerJoin(
340
                    this.metadata.closureJunctionTable.tableName,
341
                    closureTableAlias,
342
                    joinCondition,
343
                )
344
                .where(whereCondition)
345
                .setParameters(parameters)
UNCOV
346
        } else if (this.metadata.treeType === "nested-set") {
×
347
            const joinCondition =
UNCOV
348
                "joined." +
×
349
                this.metadata.nestedSetLeftColumn!.propertyPath +
350
                " BETWEEN " +
351
                alias +
352
                "." +
353
                this.metadata.nestedSetLeftColumn!.propertyPath +
354
                " AND " +
355
                alias +
356
                "." +
357
                this.metadata.nestedSetRightColumn!.propertyPath
UNCOV
358
            const parameters: ObjectLiteral = {}
×
UNCOV
359
            const whereCondition = this.metadata
×
360
                .treeParentRelation!.joinColumns.map((joinColumn) => {
361
                    const parameterName =
UNCOV
362
                        joinColumn.referencedColumn!.propertyPath.replace(
×
363
                            ".",
364
                            "_",
365
                        )
UNCOV
366
                    parameters[parameterName] =
×
367
                        joinColumn.referencedColumn!.getEntityValue(entity)
UNCOV
368
                    return (
×
369
                        "joined." +
370
                        joinColumn.referencedColumn!.propertyPath +
371
                        " = :" +
372
                        parameterName
373
                    )
374
                })
375
                .join(" AND ")
376

UNCOV
377
            return this.createQueryBuilder(alias)
×
378
                .innerJoin(this.metadata.targetName, "joined", joinCondition)
379
                .where(whereCondition, parameters)
UNCOV
380
        } else if (this.metadata.treeType === "materialized-path") {
×
381
            // example: SELECT * FROM category category WHERE (SELECT mpath FROM `category` WHERE id = 2) LIKE CONCAT(category.mpath, '%');
UNCOV
382
            return this.createQueryBuilder(alias).where((qb) => {
×
UNCOV
383
                const subQuery = qb
×
384
                    .subQuery()
385
                    .select(
386
                        `${this.metadata.targetName}.${
387
                            this.metadata.materializedPathColumn!.propertyPath
388
                        }`,
389
                        "path",
390
                    )
391
                    .from(this.metadata.target, this.metadata.targetName)
392
                    .whereInIds(this.metadata.getEntityIdMap(entity))
393

UNCOV
394
                if (
×
395
                    DriverUtils.isSQLiteFamily(this.manager.connection.driver)
396
                ) {
UNCOV
397
                    return `${subQuery.getQuery()} LIKE ${alias}.${
×
398
                        this.metadata.materializedPathColumn!.propertyPath
399
                    } || '%'`
400
                } else {
UNCOV
401
                    return `${subQuery.getQuery()} LIKE CONCAT(${alias}.${
×
402
                        this.metadata.materializedPathColumn!.propertyPath
403
                    }, '%')`
404
                }
405
            })
406
        }
407

408
        throw new TypeORMError(`Supported only in tree entities`)
×
409
    }
410

411
    /**
412
     * Moves entity to the children of then given entity.
413
     *
414
    move(entity: Entity, to: Entity): Promise<void> {
415
        return Promise.resolve();
416
    } */
417
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc