• 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

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

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

UNCOV
17
    constructor(protected queryRunner: QueryRunner) {}
×
18

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
350
        const ancestorWhere = generateWheres(closureTable.ancestorColumns)
×
UNCOV
351
        const descendantWhere = generateWheres(closureTable.descendantColumns)
×
352

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

362
    /**
363
     * Gets escaped table name with schema name if SqlServer or Postgres driver used with custom
364
     * schema name, otherwise returns escaped table name.
365
     */
366
    protected getTableName(tablePath: string): string {
UNCOV
367
        return tablePath
×
368
            .split(".")
369
            .map((i) => {
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
UNCOV
371
                return i === ""
×
372
                    ? i
373
                    : this.queryRunner.connection.driver.escape(i)
374
            })
375
            .join(".")
376
    }
377
}
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