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

typeorm / typeorm / 19549987525

20 Nov 2025 08:11PM UTC coverage: 80.769% (+4.3%) from 76.433%
19549987525

push

github

web-flow
ci: run tests on commits to master and next (#11783)

Co-authored-by: Oleg "OSA413" Sokolov <OSA413@users.noreply.github.com>

26500 of 32174 branches covered (82.36%)

Branch coverage included in aggregate %.

91252 of 113615 relevant lines covered (80.32%)

88980.79 hits per line

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

97.03
/src/persistence/tree/ClosureSubjectExecutor.ts
1
import { Subject } from "../Subject"
26✔
2
import { QueryRunner } from "../../query-runner/QueryRunner"
26✔
3
import { ObjectLiteral } from "../../common/ObjectLiteral"
26✔
4
import { CannotAttachTreeChildrenEntityError } from "../../error/CannotAttachTreeChildrenEntityError"
26✔
5
import { DeleteQueryBuilder } from "../../query-builder/DeleteQueryBuilder"
26✔
6
import { OrmUtils } from "../../util/OrmUtils"
26✔
7
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
26✔
8

26✔
9
/**
26✔
10
 * Executes subject operations for closure entities.
26✔
11
 */
26✔
12
export class ClosureSubjectExecutor {
26✔
13
    // -------------------------------------------------------------------------
26✔
14
    // Constructor
26✔
15
    // -------------------------------------------------------------------------
26✔
16

26✔
17
    constructor(protected queryRunner: QueryRunner) {}
26✔
18

26✔
19
    // -------------------------------------------------------------------------
26✔
20
    // Public Methods
26✔
21
    // -------------------------------------------------------------------------
26✔
22

26✔
23
    /**
26✔
24
     * Executes operations when subject is being inserted.
26✔
25
     */
26✔
26
    async insert(subject: Subject): Promise<void> {
26✔
27
        // create values to be inserted into the closure junction
3,836✔
28
        const closureJunctionInsertMap: ObjectLiteral = {}
3,836✔
29
        subject.metadata.closureJunctionTable.ancestorColumns.forEach(
3,836✔
30
            (column) => {
3,836✔
31
                closureJunctionInsertMap[column.databaseName] =
3,836✔
32
                    subject.identifier
3,836✔
33
            },
3,836✔
34
        )
3,836✔
35
        subject.metadata.closureJunctionTable.descendantColumns.forEach(
3,836✔
36
            (column) => {
3,836✔
37
                closureJunctionInsertMap[column.databaseName] =
3,836✔
38
                    subject.identifier
3,836✔
39
            },
3,836✔
40
        )
3,836✔
41

3,836✔
42
        // insert values into the closure junction table
3,836✔
43
        await this.queryRunner.manager
3,836✔
44
            .createQueryBuilder()
3,836✔
45
            .insert()
3,836✔
46
            .into(subject.metadata.closureJunctionTable.tablePath)
3,836✔
47
            .values(closureJunctionInsertMap)
3,836✔
48
            .updateEntity(false)
3,836✔
49
            .callListeners(false)
3,836✔
50
            .execute()
3,836✔
51

3,836✔
52
        let parent = subject.metadata.treeParentRelation!.getEntityValue(
3,836✔
53
            subject.entity!,
3,836✔
54
        ) // if entity was attached via parent
3,836✔
55
        if (!parent && subject.parentSubject && subject.parentSubject.entity)
3,836✔
56
            // if entity was attached via children
3,836✔
57
            parent = subject.parentSubject.insertedValueSet
3,836✔
58
                ? subject.parentSubject.insertedValueSet
1,848✔
59
                : subject.parentSubject.entity
1,848✔
60

3,836✔
61
        if (parent) {
3,836✔
62
            const escape = (alias: string) =>
2,634✔
63
                this.queryRunner.connection.driver.escape(alias)
7,902✔
64
            const tableName = this.getTableName(
2,634✔
65
                subject.metadata.closureJunctionTable.tablePath,
2,634✔
66
            )
2,634✔
67
            const queryParams: any[] = []
2,634✔
68

2,634✔
69
            const ancestorColumnNames =
2,634✔
70
                subject.metadata.closureJunctionTable.ancestorColumns.map(
2,634✔
71
                    (column) => {
2,634✔
72
                        return escape(column.databaseName)
2,634✔
73
                    },
2,634✔
74
                )
2,634✔
75
            const descendantColumnNames =
2,634✔
76
                subject.metadata.closureJunctionTable.descendantColumns.map(
2,634✔
77
                    (column) => {
2,634✔
78
                        return escape(column.databaseName)
2,634✔
79
                    },
2,634✔
80
                )
2,634✔
81
            const childEntityIds1 = subject.metadata.primaryColumns.map(
2,634✔
82
                (column) => {
2,634✔
83
                    queryParams.push(
2,634✔
84
                        column.getEntityValue(
2,634✔
85
                            subject.insertedValueSet
2,634✔
86
                                ? subject.insertedValueSet
2,634✔
87
                                : subject.entity!,
2,634!
88
                        ),
2,634✔
89
                    )
2,634✔
90
                    return this.queryRunner.connection.driver.createParameter(
2,634✔
91
                        "child_entity_" + column.databaseName,
2,634✔
92
                        queryParams.length - 1,
2,634✔
93
                    )
2,634✔
94
                },
2,634✔
95
            )
2,634✔
96

2,634✔
97
            const whereCondition =
2,634✔
98
                subject.metadata.closureJunctionTable.descendantColumns.map(
2,634✔
99
                    (column) => {
2,634✔
100
                        const columnName = escape(column.databaseName)
2,634✔
101
                        const parentId =
2,634✔
102
                            column.referencedColumn!.getEntityValue(parent)
2,634✔
103

2,634✔
104
                        if (!parentId)
2,634✔
105
                            throw new CannotAttachTreeChildrenEntityError(
2,634!
106
                                subject.metadata.name,
×
107
                            )
×
108

2,634✔
109
                        queryParams.push(parentId)
2,634✔
110
                        const parameterName =
2,634✔
111
                            this.queryRunner.connection.driver.createParameter(
2,634✔
112
                                "parent_entity_" +
2,634✔
113
                                    column.referencedColumn!.databaseName,
2,634✔
114
                                queryParams.length - 1,
2,634✔
115
                            )
2,634✔
116
                        return `${columnName} = ${parameterName}`
2,634✔
117
                    },
2,634✔
118
                )
2,634✔
119

2,634✔
120
            await this.queryRunner.query(
2,634✔
121
                `INSERT INTO ${tableName} (${[
2,634✔
122
                    ...ancestorColumnNames,
2,634✔
123
                    ...descendantColumnNames,
2,634✔
124
                ].join(", ")}) ` +
2,634✔
125
                    `SELECT ${ancestorColumnNames.join(
2,634✔
126
                        ", ",
2,634✔
127
                    )}, ${childEntityIds1.join(
2,634✔
128
                        ", ",
2,634✔
129
                    )} FROM ${tableName} WHERE ${whereCondition.join(" AND ")}`,
2,634✔
130
                queryParams,
2,634✔
131
            )
2,634✔
132
        }
2,634✔
133
    }
3,836✔
134

26✔
135
    /**
26✔
136
     * Executes operations when subject is being updated.
26✔
137
     */
26✔
138
    async update(subject: Subject): Promise<void> {
26✔
139
        let parent = subject.metadata.treeParentRelation!.getEntityValue(
130✔
140
            subject.entity!,
130✔
141
        ) // if entity was attached via parent
130✔
142
        if (!parent && subject.parentSubject && subject.parentSubject.entity)
130✔
143
            // if entity was attached via children
130✔
144
            parent = subject.parentSubject.entity
130✔
145

130✔
146
        let entity = subject.databaseEntity // if entity was attached via parent
130✔
147
        if (!entity && parent)
130✔
148
            // if entity was attached via children
130✔
149
            entity = subject.metadata
130✔
150
                .treeChildrenRelation!.getEntityValue(parent)
50✔
151
                .find((child: any) => {
50✔
152
                    return Object.entries(subject.identifier!).every(
50✔
153
                        ([key, value]) => child[key] === value,
50✔
154
                    )
50✔
155
                })
50✔
156

130✔
157
        // Exit if the parent or the entity where never set
130✔
158
        if (entity === undefined || parent === undefined) {
130✔
159
            return
40✔
160
        }
40✔
161

90✔
162
        const oldParent = subject.metadata.treeParentRelation!.getEntityValue(
90✔
163
            entity!,
90✔
164
        )
90✔
165
        const oldParentId = subject.metadata.getEntityIdMap(oldParent)
90✔
166
        const parentId = subject.metadata.getEntityIdMap(parent)
90✔
167

90✔
168
        // Exit if the new and old parents are the same
90✔
169
        if (OrmUtils.compareIds(oldParentId, parentId)) {
130✔
170
            return
10✔
171
        }
10✔
172

80✔
173
        const escape = (alias: string) =>
80✔
174
            this.queryRunner.connection.driver.escape(alias)
720✔
175
        const closureTable = subject.metadata.closureJunctionTable
80✔
176

80✔
177
        const ancestorColumnNames = closureTable.ancestorColumns.map(
80✔
178
            (column) => {
80✔
179
                return escape(column.databaseName)
80✔
180
            },
80✔
181
        )
80✔
182

80✔
183
        const descendantColumnNames = closureTable.descendantColumns.map(
80✔
184
            (column) => {
80✔
185
                return escape(column.databaseName)
80✔
186
            },
80✔
187
        )
80✔
188

80✔
189
        // Delete logic
80✔
190
        const createSubQuery = (qb: DeleteQueryBuilder<any>, alias: string) => {
80✔
191
            const subAlias = `sub${alias}`
160✔
192

160✔
193
            const subSelect = qb
160✔
194
                .createQueryBuilder()
160✔
195
                .select(descendantColumnNames.join(", "))
160✔
196
                .from(closureTable.tablePath, subAlias)
160✔
197

160✔
198
            // Create where conditions e.g. (WHERE "subdescendant"."id_ancestor" = :value_id)
160✔
199
            for (const column of closureTable.ancestorColumns) {
160✔
200
                subSelect.andWhere(
160✔
201
                    `${escape(subAlias)}.${escape(
160✔
202
                        column.databaseName,
160✔
203
                    )} = :value_${column.referencedColumn!.databaseName}`,
160✔
204
                )
160✔
205
            }
160✔
206

160✔
207
            return qb
160✔
208
                .createQueryBuilder()
160✔
209
                .select(descendantColumnNames.join(", "))
160✔
210
                .from(`(${subSelect.getQuery()})`, alias)
160✔
211
                .setParameters(subSelect.getParameters())
160✔
212
                .getQuery()
160✔
213
        }
160✔
214

80✔
215
        const parameters: ObjectLiteral = {}
80✔
216
        for (const column of subject.metadata.primaryColumns) {
80✔
217
            parameters[`value_${column.databaseName}`] =
80✔
218
                entity![column.databaseName]
80✔
219
        }
80✔
220

80✔
221
        await this.queryRunner.manager
80✔
222
            .createQueryBuilder()
80✔
223
            .delete()
80✔
224
            .from(closureTable.tablePath)
80✔
225
            .where(
80✔
226
                (qb) =>
80✔
227
                    `(${descendantColumnNames.join(", ")}) IN (${createSubQuery(
80✔
228
                        qb,
80✔
229
                        "descendant",
80✔
230
                    )})`,
80✔
231
            )
80✔
232
            .andWhere(
80✔
233
                (qb) =>
80✔
234
                    `(${ancestorColumnNames.join(
80✔
235
                        ", ",
80✔
236
                    )}) NOT IN (${createSubQuery(qb, "ancestor")})`,
80✔
237
            )
80✔
238
            .setParameters(parameters)
80✔
239
            .execute()
80✔
240

80✔
241
        /**
80✔
242
         * Only insert new parent if it exits
80✔
243
         *
80✔
244
         * This only happens if the entity doesn't become a root entity
80✔
245
         */
80✔
246
        if (parent) {
130✔
247
            // Insert logic
60✔
248
            const queryParams: any[] = []
60✔
249

60✔
250
            const tableName = this.getTableName(closureTable.tablePath)
60✔
251
            const superAlias = escape("supertree")
60✔
252
            const subAlias = escape("subtree")
60✔
253

60✔
254
            const select = [
60✔
255
                ...ancestorColumnNames.map(
60✔
256
                    (columnName) => `${superAlias}.${columnName}`,
60✔
257
                ),
60✔
258
                ...descendantColumnNames.map(
60✔
259
                    (columnName) => `${subAlias}.${columnName}`,
60✔
260
                ),
60✔
261
            ]
60✔
262

60✔
263
            const entityWhereCondition =
60✔
264
                subject.metadata.closureJunctionTable.ancestorColumns.map(
60✔
265
                    (column) => {
60✔
266
                        const columnName = escape(column.databaseName)
60✔
267
                        const entityId =
60✔
268
                            column.referencedColumn!.getEntityValue(entity!)
60✔
269

60✔
270
                        queryParams.push(entityId)
60✔
271
                        const parameterName =
60✔
272
                            this.queryRunner.connection.driver.createParameter(
60✔
273
                                "entity_" +
60✔
274
                                    column.referencedColumn!.databaseName,
60✔
275
                                queryParams.length - 1,
60✔
276
                            )
60✔
277
                        return `${subAlias}.${columnName} = ${parameterName}`
60✔
278
                    },
60✔
279
                )
60✔
280

60✔
281
            const parentWhereCondition =
60✔
282
                subject.metadata.closureJunctionTable.descendantColumns.map(
60✔
283
                    (column) => {
60✔
284
                        const columnName = escape(column.databaseName)
60✔
285
                        const parentId =
60✔
286
                            column.referencedColumn!.getEntityValue(parent)
60✔
287

60✔
288
                        if (!parentId)
60✔
289
                            throw new CannotAttachTreeChildrenEntityError(
60!
290
                                subject.metadata.name,
×
291
                            )
×
292

60✔
293
                        queryParams.push(parentId)
60✔
294
                        const parameterName =
60✔
295
                            this.queryRunner.connection.driver.createParameter(
60✔
296
                                "parent_entity_" +
60✔
297
                                    column.referencedColumn!.databaseName,
60✔
298
                                queryParams.length - 1,
60✔
299
                            )
60✔
300
                        return `${superAlias}.${columnName} = ${parameterName}`
60✔
301
                    },
60✔
302
                )
60✔
303

60✔
304
            await this.queryRunner.query(
60✔
305
                `INSERT INTO ${tableName} (${[
60✔
306
                    ...ancestorColumnNames,
60✔
307
                    ...descendantColumnNames,
60✔
308
                ].join(", ")}) ` +
60✔
309
                    `SELECT ${select.join(", ")} ` +
60✔
310
                    `FROM ${tableName} AS ${superAlias}, ${tableName} AS ${subAlias} ` +
60✔
311
                    `WHERE ${[
60✔
312
                        ...entityWhereCondition,
60✔
313
                        ...parentWhereCondition,
60✔
314
                    ].join(" AND ")}`,
60✔
315
                queryParams,
60✔
316
            )
60✔
317
        }
60✔
318
    }
130✔
319

26✔
320
    /**
26✔
321
     * Executes operations when subject is being removed.
26✔
322
     */
26✔
323
    async remove(subjects: Subject | Subject[]): Promise<void> {
26✔
324
        // Only mssql need to execute deletes for the juntion table as it doesn't support multi cascade paths.
26✔
325
        if (!(this.queryRunner.connection.driver.options.type === "mssql")) {
26!
326
            return
24✔
327
        }
24✔
328

2!
329
        if (!Array.isArray(subjects)) subjects = [subjects]
2!
330

2✔
331
        const escape = (alias: string) =>
2✔
332
            this.queryRunner.connection.driver.escape(alias)
4✔
333
        const identifiers = subjects.map((subject) => subject.identifier)
2✔
334
        const closureTable = subjects[0].metadata.closureJunctionTable
2✔
335

2✔
336
        const generateWheres = (columns: ColumnMetadata[]) => {
2✔
337
            return columns
4✔
338
                .map((column) => {
4✔
339
                    const data = identifiers.map(
4✔
340
                        (identifier) =>
4✔
341
                            identifier![column.referencedColumn!.databaseName],
4✔
342
                    )
4✔
343
                    return `${escape(column.databaseName)} IN (${data.join(
4✔
344
                        ", ",
4✔
345
                    )})`
4✔
346
                })
4✔
347
                .join(" AND ")
4✔
348
        }
4✔
349

2✔
350
        const ancestorWhere = generateWheres(closureTable.ancestorColumns)
2✔
351
        const descendantWhere = generateWheres(closureTable.descendantColumns)
2✔
352

2✔
353
        await this.queryRunner.manager
2✔
354
            .createQueryBuilder()
2✔
355
            .delete()
2✔
356
            .from(closureTable.tablePath)
2✔
357
            .where(ancestorWhere)
2✔
358
            .orWhere(descendantWhere)
2✔
359
            .execute()
2✔
360
    }
2✔
361

26✔
362
    /**
26✔
363
     * Gets escaped table name with schema name if SqlServer or Postgres driver used with custom
26✔
364
     * schema name, otherwise returns escaped table name.
26✔
365
     */
26✔
366
    protected getTableName(tablePath: string): string {
26✔
367
        return tablePath
2,694✔
368
            .split(".")
2,694✔
369
            .map((i) => {
2,694✔
370
                // this condition need because in SQL Server driver when custom database name was specified and schema name was not, we got `dbName..tableName` string, and doesn't need to escape middle empty string
2,694✔
371
                return i === ""
2,694✔
372
                    ? i
2,694!
373
                    : this.queryRunner.connection.driver.escape(i)
2,694✔
374
            })
2,694✔
375
            .join(".")
2,694✔
376
    }
2,694✔
377
}
26✔
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

© 2026 Coveralls, Inc