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

typeorm / typeorm / 23390157208

21 Mar 2026 10:26PM UTC coverage: 56.678% (-16.6%) from 73.277%
23390157208

Pull #12252

github

web-flow
Merge 5b60ba41c into 7038fa166
Pull Request #12252: fix: unskip cascade soft remove test

17767 of 26580 branches covered (66.84%)

Branch coverage included in aggregate %.

64033 of 117744 relevant lines covered (54.38%)

1514.83 hits per line

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

45.74
/src/persistence/tree/NestedSetSubjectExecutor.ts
1
import type { Subject } from "../Subject"
28✔
2
import type { QueryRunner } from "../../query-runner/QueryRunner"
28✔
3
import { OrmUtils } from "../../util/OrmUtils"
28✔
4
import { NestedSetMultipleRootError } from "../../error/NestedSetMultipleRootError"
28✔
5
import type { ObjectLiteral } from "../../common/ObjectLiteral"
28✔
6
import type { EntityMetadata } from "../../metadata/EntityMetadata"
28✔
7

28✔
8
class NestedSetIds {
28✔
9
    left: number
28✔
10
    right: number
28✔
11
}
28✔
12

28✔
13
/**
28✔
14
 * Executes subject operations for nested set tree entities.
28✔
15
 */
28✔
16
export class NestedSetSubjectExecutor {
28✔
17
    // -------------------------------------------------------------------------
28✔
18
    // Constructor
28✔
19
    // -------------------------------------------------------------------------
28✔
20

28✔
21
    constructor(protected queryRunner: QueryRunner) {}
28✔
22

28✔
23
    // -------------------------------------------------------------------------
28✔
24
    // Public Methods
28✔
25
    // -------------------------------------------------------------------------
28✔
26

28✔
27
    /**
28✔
28
     * Executes operations when subject is being inserted.
28✔
29
     * @param subject
28✔
30
     */
28✔
31
    async insert(subject: Subject): Promise<void> {
28✔
32
        const escape = (alias: string) =>
74✔
33
            this.queryRunner.connection.driver.escape(alias)
148✔
34
        const tableName = this.getTableName(subject.metadata.tablePath)
74✔
35
        const leftColumnName = escape(
74✔
36
            subject.metadata.nestedSetLeftColumn!.databaseName,
74✔
37
        )
74✔
38
        const rightColumnName = escape(
74✔
39
            subject.metadata.nestedSetRightColumn!.databaseName,
74✔
40
        )
74✔
41

74✔
42
        let parent = subject.metadata.treeParentRelation!.getEntityValue(
74✔
43
            subject.entity!,
74✔
44
        ) // if entity was attached via parent
74✔
45
        if (!parent && subject.parentSubject && subject.parentSubject.entity)
74✔
46
            // if entity was attached via children
74✔
47
            parent = subject.parentSubject.insertedValueSet
74✔
48
                ? subject.parentSubject.insertedValueSet
48✔
49
                : subject.parentSubject.entity
48✔
50
        const parentId = subject.metadata.getEntityIdMap(parent)
74✔
51

74✔
52
        let parentNsRight: number | undefined = undefined
74✔
53
        if (parentId) {
74✔
54
            parentNsRight = await this.queryRunner.manager
54✔
55
                .createQueryBuilder()
54✔
56
                .select(
54✔
57
                    subject.metadata.targetName +
54✔
58
                        "." +
54✔
59
                        subject.metadata.nestedSetRightColumn!.propertyPath,
54✔
60
                    "right",
54✔
61
                )
54✔
62
                .from(subject.metadata.target, subject.metadata.targetName)
54✔
63
                .whereInIds(parentId)
54✔
64
                .getRawOne()
54✔
65
                .then((result) => {
54✔
66
                    const value: any = result ? result["right"] : undefined
54!
67
                    // CockroachDB returns numeric types as string
54✔
68
                    return typeof value === "string" ? parseInt(value) : value
54!
69
                })
54✔
70
        }
54✔
71

74✔
72
        if (parentNsRight !== undefined) {
74✔
73
            await this.queryRunner.query(
54✔
74
                `UPDATE ${tableName} SET ` +
54✔
75
                    `${leftColumnName} = CASE WHEN ${leftColumnName} > ${parentNsRight} THEN ${leftColumnName} + 2 ELSE ${leftColumnName} END,` +
54✔
76
                    `${rightColumnName} = ${rightColumnName} + 2 ` +
54✔
77
                    `WHERE ${rightColumnName} >= ${parentNsRight}`,
54✔
78
            )
54✔
79

54✔
80
            OrmUtils.mergeDeep(
54✔
81
                subject.insertedValueSet,
54✔
82
                subject.metadata.nestedSetLeftColumn!.createValueMap(
54✔
83
                    parentNsRight,
54✔
84
                ),
54✔
85
                subject.metadata.nestedSetRightColumn!.createValueMap(
54✔
86
                    parentNsRight + 1,
54✔
87
                ),
54✔
88
            )
54✔
89
        } else {
74✔
90
            const isUniqueRoot = await this.isUniqueRootEntity(subject, parent)
20✔
91

20✔
92
            // Validate if a root entity already exits and throw an exception
20✔
93
            if (!isUniqueRoot) throw new NestedSetMultipleRootError()
20✔
94

18✔
95
            OrmUtils.mergeDeep(
18✔
96
                subject.insertedValueSet,
18✔
97
                subject.metadata.nestedSetLeftColumn!.createValueMap(1),
18✔
98
                subject.metadata.nestedSetRightColumn!.createValueMap(2),
18✔
99
            )
18✔
100
        }
18✔
101
    }
74✔
102

28✔
103
    /**
28✔
104
     * Executes operations when subject is being updated.
28✔
105
     * @param subject
28✔
106
     */
28✔
107
    async update(subject: Subject): Promise<void> {
28✔
108
        let parent = subject.metadata.treeParentRelation!.getEntityValue(
×
109
            subject.entity!,
×
110
        ) // if entity was attached via parent
×
111
        if (!parent && subject.parentSubject && subject.parentSubject.entity)
×
112
            // if entity was attached via children
×
113
            parent = subject.parentSubject.entity
×
114

×
115
        let entity = subject.databaseEntity // if entity was attached via parent
×
116
        if (!entity && parent)
×
117
            // if entity was attached via children
×
118
            entity = subject.metadata
×
119
                .treeChildrenRelation!.getEntityValue(parent)
×
120
                .find((child: any) => {
×
121
                    return Object.entries(subject.identifier!).every(
×
122
                        ([key, value]) => child[key] === value,
×
123
                    )
×
124
                })
×
125

×
126
        // Exit if the parent or the entity where never set
×
127
        if (entity === undefined || parent === undefined) {
×
128
            return
×
129
        }
×
130

×
131
        const oldParent = subject.metadata.treeParentRelation!.getEntityValue(
×
132
            entity!,
×
133
        )
×
134
        const oldParentId = subject.metadata.getEntityIdMap(oldParent)
×
135
        const parentId = subject.metadata.getEntityIdMap(parent)
×
136

×
137
        // Exit if the new and old parents are the same
×
138
        if (OrmUtils.compareIds(oldParentId, parentId)) {
×
139
            return
×
140
        }
×
141

×
142
        if (parent) {
×
143
            const escape = (alias: string) =>
×
144
                this.queryRunner.connection.driver.escape(alias)
×
145
            const tableName = this.getTableName(subject.metadata.tablePath)
×
146
            const leftColumnName = escape(
×
147
                subject.metadata.nestedSetLeftColumn!.databaseName,
×
148
            )
×
149
            const rightColumnName = escape(
×
150
                subject.metadata.nestedSetRightColumn!.databaseName,
×
151
            )
×
152

×
153
            const entityId = subject.metadata.getEntityIdMap(entity)
×
154

×
155
            let entityNs: NestedSetIds | undefined = undefined
×
156
            if (entityId) {
×
157
                entityNs = (
×
158
                    await this.getNestedSetIds(subject.metadata, entityId)
×
159
                )[0]
×
160
            }
×
161

×
162
            let parentNs: NestedSetIds | undefined = undefined
×
163
            if (parentId) {
×
164
                parentNs = (
×
165
                    await this.getNestedSetIds(subject.metadata, parentId)
×
166
                )[0]
×
167
            }
×
168

×
169
            if (entityNs !== undefined && parentNs !== undefined) {
×
170
                const isMovingUp = parentNs.left > entityNs.left
×
171
                const treeSize = entityNs.right - entityNs.left + 1
×
172

×
173
                let entitySize: number
×
174
                if (isMovingUp) {
×
175
                    entitySize = parentNs.left - entityNs.right
×
176
                } else {
×
177
                    entitySize = parentNs.right - entityNs.left
×
178
                }
×
179

×
180
                // Moved entity logic
×
181
                const updateLeftSide =
×
182
                    `WHEN ${leftColumnName} >= ${entityNs.left} AND ` +
×
183
                    `${leftColumnName} < ${entityNs.right} ` +
×
184
                    `THEN ${leftColumnName} + ${entitySize} `
×
185

×
186
                const updateRightSide =
×
187
                    `WHEN ${rightColumnName} > ${entityNs.left} AND ` +
×
188
                    `${rightColumnName} <= ${entityNs.right} ` +
×
189
                    `THEN ${rightColumnName} + ${entitySize} `
×
190

×
191
                // Update the surrounding entities
×
192
                if (isMovingUp) {
×
193
                    await this.queryRunner.query(
×
194
                        `UPDATE ${tableName} ` +
×
195
                            `SET ${leftColumnName} = CASE ` +
×
196
                            `WHEN ${leftColumnName} > ${entityNs.right} AND ` +
×
197
                            `${leftColumnName} <= ${parentNs.left} ` +
×
198
                            `THEN ${leftColumnName} - ${treeSize} ` +
×
199
                            updateLeftSide +
×
200
                            `ELSE ${leftColumnName} ` +
×
201
                            `END, ` +
×
202
                            `${rightColumnName} = CASE ` +
×
203
                            `WHEN ${rightColumnName} > ${entityNs.right} AND ` +
×
204
                            `${rightColumnName} < ${parentNs.left} ` +
×
205
                            `THEN ${rightColumnName} - ${treeSize} ` +
×
206
                            updateRightSide +
×
207
                            `ELSE ${rightColumnName} ` +
×
208
                            `END`,
×
209
                    )
×
210
                } else {
×
211
                    await this.queryRunner.query(
×
212
                        `UPDATE ${tableName} ` +
×
213
                            `SET ${leftColumnName} = CASE ` +
×
214
                            `WHEN ${leftColumnName} < ${entityNs.left} AND ` +
×
215
                            `${leftColumnName} > ${parentNs.right} ` +
×
216
                            `THEN ${leftColumnName} + ${treeSize} ` +
×
217
                            updateLeftSide +
×
218
                            `ELSE ${leftColumnName} ` +
×
219
                            `END, ` +
×
220
                            `${rightColumnName} = CASE ` +
×
221
                            `WHEN ${rightColumnName} < ${entityNs.left} AND ` +
×
222
                            `${rightColumnName} >= ${parentNs.right} ` +
×
223
                            `THEN ${rightColumnName} + ${treeSize} ` +
×
224
                            updateRightSide +
×
225
                            `ELSE ${rightColumnName} ` +
×
226
                            `END`,
×
227
                    )
×
228
                }
×
229
            }
×
230
        } else {
×
231
            const isUniqueRoot = await this.isUniqueRootEntity(subject, parent)
×
232

×
233
            // Validate if a root entity already exits and throw an exception
×
234
            if (!isUniqueRoot) throw new NestedSetMultipleRootError()
×
235
        }
×
236
    }
×
237

28✔
238
    /**
28✔
239
     * Executes operations when subject is being removed.
28✔
240
     * @param subjects
28✔
241
     */
28✔
242
    async remove(subjects: Subject | Subject[]): Promise<void> {
28✔
243
        if (!Array.isArray(subjects)) subjects = [subjects]
×
244

×
245
        const metadata = subjects[0].metadata
×
246

×
247
        const escape = (alias: string) =>
×
248
            this.queryRunner.connection.driver.escape(alias)
×
249
        const tableName = this.getTableName(metadata.tablePath)
×
250
        const leftColumnName = escape(
×
251
            metadata.nestedSetLeftColumn!.databaseName,
×
252
        )
×
253
        const rightColumnName = escape(
×
254
            metadata.nestedSetRightColumn!.databaseName,
×
255
        )
×
256

×
257
        const entitiesIds: ObjectLiteral[] = []
×
258
        for (const subject of subjects) {
×
259
            const entityId = metadata.getEntityIdMap(subject.entity)
×
260

×
261
            if (entityId) {
×
262
                entitiesIds.push(entityId)
×
263
            }
×
264
        }
×
265

×
266
        const entitiesNs = await this.getNestedSetIds(metadata, entitiesIds)
×
267

×
268
        for (const entity of entitiesNs) {
×
269
            const treeSize = entity.right - entity.left + 1
×
270

×
271
            await this.queryRunner.query(
×
272
                `UPDATE ${tableName} ` +
×
273
                    `SET ${leftColumnName} = CASE ` +
×
274
                    `WHEN ${leftColumnName} > ${entity.left} THEN ${leftColumnName} - ${treeSize} ` +
×
275
                    `ELSE ${leftColumnName} ` +
×
276
                    `END, ` +
×
277
                    `${rightColumnName} = CASE ` +
×
278
                    `WHEN ${rightColumnName} > ${entity.right} THEN ${rightColumnName} - ${treeSize} ` +
×
279
                    `ELSE ${rightColumnName} ` +
×
280
                    `END`,
×
281
            )
×
282
        }
×
283
    }
×
284

28✔
285
    /**
28✔
286
     * Get the nested set ids for a given entity
28✔
287
     * @param metadata
28✔
288
     * @param ids
28✔
289
     */
28✔
290
    protected getNestedSetIds(
28✔
291
        metadata: EntityMetadata,
×
292
        ids: ObjectLiteral | ObjectLiteral[],
×
293
    ): Promise<NestedSetIds[]> {
×
294
        const select = {
×
295
            left: `${metadata.targetName}.${
×
296
                metadata.nestedSetLeftColumn!.propertyPath
×
297
            }`,
×
298
            right: `${metadata.targetName}.${
×
299
                metadata.nestedSetRightColumn!.propertyPath
×
300
            }`,
×
301
        }
×
302

×
303
        const queryBuilder = this.queryRunner.manager.createQueryBuilder()
×
304

×
305
        Object.entries(select).forEach(([key, value]) => {
×
306
            queryBuilder.addSelect(value, key)
×
307
        })
×
308

×
309
        return queryBuilder
×
310
            .from(metadata.target, metadata.targetName)
×
311
            .whereInIds(ids)
×
312
            .orderBy(select.right, "DESC")
×
313
            .getRawMany()
×
314
            .then((results) => {
×
315
                const data: NestedSetIds[] = []
×
316

×
317
                for (const result of results) {
×
318
                    const entry: any = {}
×
319
                    for (const key of Object.keys(select)) {
×
320
                        const value = result ? result[key] : undefined
×
321

×
322
                        // CockroachDB returns numeric types as string
×
323
                        entry[key] =
×
324
                            typeof value === "string" ? parseInt(value) : value
×
325
                    }
×
326
                    data.push(entry)
×
327
                }
×
328

×
329
                return data
×
330
            })
×
331
    }
×
332

28✔
333
    private async isUniqueRootEntity(
28✔
334
        subject: Subject,
20✔
335
        parent: any,
20✔
336
    ): Promise<boolean> {
20✔
337
        const escape = (alias: string) =>
20✔
338
            this.queryRunner.connection.driver.escape(alias)
40✔
339
        const tableName = this.getTableName(subject.metadata.tablePath)
20✔
340
        const parameters: any[] = []
20✔
341
        const whereCondition = subject.metadata
20✔
342
            .treeParentRelation!.joinColumns.map((column) => {
20✔
343
                const columnName = escape(column.databaseName)
20✔
344
                const parameter = column.getEntityValue(parent)
20✔
345

20✔
346
                if (parameter == null) {
20✔
347
                    return `${columnName} IS NULL`
20✔
348
                }
20✔
349

×
350
                parameters.push(parameter)
×
351
                const parameterName =
×
352
                    this.queryRunner.connection.driver.createParameter(
×
353
                        "entity_" + column.databaseName,
×
354
                        parameters.length - 1,
×
355
                    )
×
356
                return `${columnName} = ${parameterName}`
×
357
            })
20✔
358
            .join(" AND ")
20✔
359

20✔
360
        const countAlias = "count"
20✔
361
        const result = await this.queryRunner.query(
20✔
362
            `SELECT COUNT(1) AS ${escape(
20✔
363
                countAlias,
20✔
364
            )} FROM ${tableName} WHERE ${whereCondition}`,
20✔
365
            parameters,
20✔
366
            true,
20✔
367
        )
20✔
368

20✔
369
        return parseInt(result.records[0][countAlias]) === 0
20✔
370
    }
20✔
371

28✔
372
    /**
28✔
373
     * Gets escaped table name with schema name if SqlServer or Postgres driver used with custom
28✔
374
     * schema name, otherwise returns escaped table name.
28✔
375
     * @param tablePath
28✔
376
     */
28✔
377
    protected getTableName(tablePath: string): string {
28✔
378
        return tablePath
94✔
379
            .split(".")
94✔
380
            .map((i) => {
94✔
381
                // 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
94✔
382
                return i === ""
94✔
383
                    ? i
94!
384
                    : this.queryRunner.connection.driver.escape(i)
94✔
385
            })
94✔
386
            .join(".")
94✔
387
    }
94✔
388
}
28✔
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