• 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

1.91
/src/persistence/tree/NestedSetSubjectExecutor.ts
1
import { Subject } from "../Subject"
2
import { QueryRunner } from "../../query-runner/QueryRunner"
3
import { OrmUtils } from "../../util/OrmUtils"
1✔
4
import { NestedSetMultipleRootError } from "../../error/NestedSetMultipleRootError"
1✔
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 {
1✔
17
    // -------------------------------------------------------------------------
18
    // Constructor
19
    // -------------------------------------------------------------------------
20

UNCOV
21
    constructor(protected queryRunner: QueryRunner) {}
×
22

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

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

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

UNCOV
51
        let parentNsRight: number | undefined = undefined
×
UNCOV
52
        if (parentId) {
×
UNCOV
53
            parentNsRight = await this.queryRunner.manager
×
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) => {
UNCOV
65
                    const value: any = result ? result["right"] : undefined
×
66
                    // CockroachDB returns numeric types as string
UNCOV
67
                    return typeof value === "string" ? parseInt(value) : value
×
68
                })
69
        }
70

UNCOV
71
        if (parentNsRight !== undefined) {
×
UNCOV
72
            await this.queryRunner.query(
×
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

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

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

UNCOV
94
            OrmUtils.mergeDeep(
×
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> {
UNCOV
106
        let parent = subject.metadata.treeParentRelation!.getEntityValue(
×
107
            subject.entity!,
108
        ) // if entity was attached via parent
UNCOV
109
        if (!parent && subject.parentSubject && subject.parentSubject.entity)
×
110
            // if entity was attached via children
UNCOV
111
            parent = subject.parentSubject.entity
×
112

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

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

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

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

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

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

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

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

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

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

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

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

189
                // Update the surrounding entities
UNCOV
190
                if (isMovingUp) {
×
UNCOV
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 {
UNCOV
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 {
UNCOV
229
            const isUniqueRoot = await this.isUniqueRootEntity(subject, parent)
×
230

231
            // Validate if a root entity already exits and throw an exception
UNCOV
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> {
UNCOV
240
        if (!Array.isArray(subjects)) subjects = [subjects]
×
241

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

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

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

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

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

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

UNCOV
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[]> {
UNCOV
289
        const select = {
×
290
            left: `${metadata.targetName}.${
291
                metadata.nestedSetLeftColumn!.propertyPath
292
            }`,
293
            right: `${metadata.targetName}.${
294
                metadata.nestedSetRightColumn!.propertyPath
295
            }`,
296
        }
297

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

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

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

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

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

UNCOV
324
                return data
×
325
            })
326
    }
327

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

UNCOV
341
                if (parameter == null) {
×
UNCOV
342
                    return `${columnName} IS NULL`
×
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

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

UNCOV
364
        return parseInt(result.records[0][countAlias]) === 0
×
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 {
UNCOV
372
        return tablePath
×
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
UNCOV
376
                return i === ""
×
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