• 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

242
        const metadata = subjects[0].metadata
×
243

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

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

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

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

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

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

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

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

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

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

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

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

324
                return data
×
325
            })
326
    }
327

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

341
                if (parameter == null) {
80✔
342
                    return `${columnName} IS NULL`
80✔
343
                }
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
            })
353
            .join(" AND ")
354

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

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

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