• 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

190✔
124
        // Exit if the parent or the entity where never set
190✔
125
        if (entity === undefined || parent === undefined) {
190✔
126
            return
50✔
127
        }
50✔
128

140✔
129
        const oldParent = subject.metadata.treeParentRelation!.getEntityValue(
140✔
130
            entity!,
140✔
131
        )
140✔
132
        const oldParentId = subject.metadata.getEntityIdMap(oldParent)
140✔
133
        const parentId = subject.metadata.getEntityIdMap(parent)
140✔
134

140✔
135
        // Exit if the new and old parents are the same
140✔
136
        if (OrmUtils.compareIds(oldParentId, parentId)) {
190✔
137
            return
20✔
138
        }
20✔
139

120✔
140
        if (parent) {
190✔
141
            const escape = (alias: string) =>
100✔
142
                this.queryRunner.connection.driver.escape(alias)
200✔
143
            const tableName = this.getTableName(subject.metadata.tablePath)
100✔
144
            const leftColumnName = escape(
100✔
145
                subject.metadata.nestedSetLeftColumn!.databaseName,
100✔
146
            )
100✔
147
            const rightColumnName = escape(
100✔
148
                subject.metadata.nestedSetRightColumn!.databaseName,
100✔
149
            )
100✔
150

100✔
151
            const entityId = subject.metadata.getEntityIdMap(entity)
100✔
152

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

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

100✔
167
            if (entityNs !== undefined && parentNs !== undefined) {
100✔
168
                const isMovingUp = parentNs.left > entityNs.left
100✔
169
                const treeSize = entityNs.right - entityNs.left + 1
100✔
170

100✔
171
                let entitySize: number
100✔
172
                if (isMovingUp) {
100✔
173
                    entitySize = parentNs.left - entityNs.right
40✔
174
                } else {
100✔
175
                    entitySize = parentNs.right - entityNs.left
60✔
176
                }
60✔
177

100✔
178
                // Moved entity logic
100✔
179
                const updateLeftSide =
100✔
180
                    `WHEN ${leftColumnName} >= ${entityNs.left} AND ` +
100✔
181
                    `${leftColumnName} < ${entityNs.right} ` +
100✔
182
                    `THEN ${leftColumnName} + ${entitySize} `
100✔
183

100✔
184
                const updateRightSide =
100✔
185
                    `WHEN ${rightColumnName} > ${entityNs.left} AND ` +
100✔
186
                    `${rightColumnName} <= ${entityNs.right} ` +
100✔
187
                    `THEN ${rightColumnName} + ${entitySize} `
100✔
188

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

20✔
231
            // Validate if a root entity already exits and throw an exception
20✔
232
            if (!isUniqueRoot) throw new NestedSetMultipleRootError()
20✔
233
        }
20✔
234
    }
190✔
235

26✔
236
    /**
26✔
237
     * Executes operations when subject is being removed.
26✔
238
     */
26✔
239
    async remove(subjects: Subject | Subject[]): Promise<void> {
26✔
240
        if (!Array.isArray(subjects)) subjects = [subjects]
52!
241

52✔
242
        const metadata = subjects[0].metadata
52✔
243

52✔
244
        const escape = (alias: string) =>
52✔
245
            this.queryRunner.connection.driver.escape(alias)
104✔
246
        const tableName = this.getTableName(metadata.tablePath)
52✔
247
        const leftColumnName = escape(
52✔
248
            metadata.nestedSetLeftColumn!.databaseName,
52✔
249
        )
52✔
250
        const rightColumnName = escape(
52✔
251
            metadata.nestedSetRightColumn!.databaseName,
52✔
252
        )
52✔
253

52✔
254
        const entitiesIds: ObjectLiteral[] = []
52✔
255
        for (const subject of subjects) {
52✔
256
            const entityId = metadata.getEntityIdMap(subject.entity)
68✔
257

68✔
258
            if (entityId) {
68✔
259
                entitiesIds.push(entityId)
68✔
260
            }
68✔
261
        }
68✔
262

52✔
263
        const entitiesNs = await this.getNestedSetIds(metadata, entitiesIds)
52✔
264

52✔
265
        for (const entity of entitiesNs) {
52✔
266
            const treeSize = entity.right - entity.left + 1
68✔
267

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

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

252✔
298
        const queryBuilder = this.queryRunner.manager.createQueryBuilder()
252✔
299

252✔
300
        Object.entries(select).forEach(([key, value]) => {
252✔
301
            queryBuilder.addSelect(value, key)
504✔
302
        })
252✔
303

252✔
304
        return queryBuilder
252✔
305
            .from(metadata.target, metadata.targetName)
252✔
306
            .whereInIds(ids)
252✔
307
            .orderBy(select.right, "DESC")
252✔
308
            .getRawMany()
252✔
309
            .then((results) => {
252✔
310
                const data: NestedSetIds[] = []
252✔
311

252✔
312
                for (const result of results) {
252✔
313
                    const entry: any = {}
268✔
314
                    for (const key of Object.keys(select)) {
268✔
315
                        const value = result ? result[key] : undefined
536!
316

536✔
317
                        // CockroachDB returns numeric types as string
536✔
318
                        entry[key] =
536✔
319
                            typeof value === "string" ? parseInt(value) : value
536!
320
                    }
536✔
321
                    data.push(entry)
268✔
322
                }
268✔
323

252✔
324
                return data
252✔
325
            })
252✔
326
    }
252✔
327

26✔
328
    private async isUniqueRootEntity(
26✔
329
        subject: Subject,
809✔
330
        parent: any,
809✔
331
    ): Promise<boolean> {
809✔
332
        const escape = (alias: string) =>
809✔
333
            this.queryRunner.connection.driver.escape(alias)
1,734✔
334
        const tableName = this.getTableName(subject.metadata.tablePath)
809✔
335
        const parameters: any[] = []
809✔
336
        const whereCondition = subject.metadata
809✔
337
            .treeParentRelation!.joinColumns.map((column) => {
809✔
338
                const columnName = escape(column.databaseName)
925✔
339
                const parameter = column.getEntityValue(parent)
925✔
340

925✔
341
                if (parameter == null) {
925✔
342
                    return `${columnName} IS NULL`
925✔
343
                }
925✔
344

×
345
                parameters.push(parameter)
×
346
                const parameterName =
×
347
                    this.queryRunner.connection.driver.createParameter(
×
348
                        "entity_" + column.databaseName,
×
349
                        parameters.length - 1,
×
350
                    )
×
351
                return `${columnName} = ${parameterName}`
×
352
            })
809✔
353
            .join(" AND ")
809✔
354

809✔
355
        const countAlias = "count"
809✔
356
        const result = await this.queryRunner.query(
809✔
357
            `SELECT COUNT(1) AS ${escape(
809✔
358
                countAlias,
809✔
359
            )} FROM ${tableName} WHERE ${whereCondition}`,
809✔
360
            parameters,
809✔
361
            true,
809✔
362
        )
809✔
363

809✔
364
        return parseInt(result.records[0][countAlias]) === 0
809✔
365
    }
809✔
366

26✔
367
    /**
26✔
368
     * Gets escaped table name with schema name if SqlServer or Postgres driver used with custom
26✔
369
     * schema name, otherwise returns escaped table name.
26✔
370
     */
26✔
371
    protected getTableName(tablePath: string): string {
26✔
372
        return tablePath
3,779✔
373
            .split(".")
3,779✔
374
            .map((i) => {
3,779✔
375
                // 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
3,791✔
376
                return i === ""
3,791✔
377
                    ? i
3,791!
378
                    : this.queryRunner.connection.driver.escape(i)
3,791✔
379
            })
3,779✔
380
            .join(".")
3,779✔
381
    }
3,779✔
382
}
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