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

typeorm / typeorm / 14796576772

02 May 2025 01:52PM UTC coverage: 45.367% (-30.9%) from 76.309%
14796576772

Pull #11434

github

web-flow
Merge ec4ce2d00 into fadad1a74
Pull Request #11434: feat: release PR releases using pkg.pr.new

5216 of 12761 branches covered (40.87%)

Branch coverage included in aggregate %.

11439 of 23951 relevant lines covered (47.76%)

15712.55 hits per line

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

32.59
/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"
4✔
5
import { DeleteQueryBuilder } from "../../query-builder/DeleteQueryBuilder"
6
import { OrmUtils } from "../../util/OrmUtils"
4✔
7
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
8

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

17
    constructor(protected queryRunner: QueryRunner) {}
488✔
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
28
        const closureJunctionInsertMap: ObjectLiteral = {}
488✔
29
        subject.metadata.closureJunctionTable.ancestorColumns.forEach(
488✔
30
            (column) => {
31
                closureJunctionInsertMap[column.databaseName] =
488✔
32
                    subject.identifier
33
            },
34
        )
35
        subject.metadata.closureJunctionTable.descendantColumns.forEach(
488✔
36
            (column) => {
37
                closureJunctionInsertMap[column.databaseName] =
488✔
38
                    subject.identifier
39
            },
40
        )
41

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

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

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

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

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

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

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

120
            await this.queryRunner.query(
338✔
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> {
139
        let parent = subject.metadata.treeParentRelation!.getEntityValue(
×
140
            subject.entity!,
141
        ) // if entity was attached via parent
142
        if (!parent && subject.parentSubject && subject.parentSubject.entity)
×
143
            // if entity was attached via children
144
            parent = subject.parentSubject.entity
×
145

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

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

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

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

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

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

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

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

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)
199
            for (const column of closureTable.ancestorColumns) {
×
200
                subSelect.andWhere(
×
201
                    `${escape(subAlias)}.${escape(
202
                        column.databaseName,
203
                    )} = :value_${column.referencedColumn!.databaseName}`,
204
                )
205
            }
206

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

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

221
        await this.queryRunner.manager
×
222
            .createQueryBuilder()
223
            .delete()
224
            .from(closureTable.tablePath)
225
            .where(
226
                (qb) =>
227
                    `(${descendantColumnNames.join(", ")}) IN (${createSubQuery(
×
228
                        qb,
229
                        "descendant",
230
                    )})`,
231
            )
232
            .andWhere(
233
                (qb) =>
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
         */
246
        if (parent) {
×
247
            // Insert logic
248
            const queryParams: any[] = []
×
249

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

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

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

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

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

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

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

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.
325
        if (!(this.queryRunner.connection.driver.options.type === "mssql")) {
×
326
            return
×
327
        }
328

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

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

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

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

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 {
367
        return tablePath
338✔
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
371
                return i === ""
338!
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