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

typeorm / typeorm / 15805338492

22 Jun 2025 09:51AM UTC coverage: 76.344% (-0.07%) from 76.418%
15805338492

push

github

web-flow
fix: add stricter type-checking and improve event loop handling (#11540)

* refactor: minor type improvements

* chore: add type-checked eslint rules

* fix: enable no-misused-promises

* fix: enable no-floating-promises

* fix: enable await-thenable

* fix: enable require-await

* fix: enable no-misused-new

* refactor: enable no-namespace

* refactor: enable tseslint eslint recommended

* code review

9281 of 12872 branches covered (72.1%)

Branch coverage included in aggregate %.

117 of 210 new or added lines in 18 files covered. (55.71%)

19 existing lines in 9 files now uncovered.

18996 of 24167 relevant lines covered (78.6%)

119161.42 hits per line

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

90.02
/src/persistence/SubjectExecutor.ts
1
import { QueryRunner } from "../query-runner/QueryRunner"
2
import { Subject } from "./Subject"
3
import { SubjectTopologicalSorter } from "./SubjectTopologicalSorter"
24✔
4
import { SubjectChangedColumnsComputer } from "./SubjectChangedColumnsComputer"
24✔
5
import { SubjectWithoutIdentifierError } from "../error/SubjectWithoutIdentifierError"
24✔
6
import { SubjectRemovedAndUpdatedError } from "../error/SubjectRemovedAndUpdatedError"
24✔
7
import { MongoEntityManager } from "../entity-manager/MongoEntityManager"
8
import { ObjectLiteral } from "../common/ObjectLiteral"
9
import { SaveOptions } from "../repository/SaveOptions"
10
import { RemoveOptions } from "../repository/RemoveOptions"
11
import { BroadcasterResult } from "../subscriber/BroadcasterResult"
24✔
12
import { NestedSetSubjectExecutor } from "./tree/NestedSetSubjectExecutor"
24✔
13
import { ClosureSubjectExecutor } from "./tree/ClosureSubjectExecutor"
24✔
14
import { MaterializedPathSubjectExecutor } from "./tree/MaterializedPathSubjectExecutor"
24✔
15
import { OrmUtils } from "../util/OrmUtils"
24✔
16
import { UpdateResult } from "../query-builder/result/UpdateResult"
17
import { ObjectUtils } from "../util/ObjectUtils"
24✔
18
import { InstanceChecker } from "../util/InstanceChecker"
24✔
19

20
/**
21
 * Executes all database operations (inserts, updated, deletes) that must be executed
22
 * with given persistence subjects.
23
 */
24
export class SubjectExecutor {
24✔
25
    // -------------------------------------------------------------------------
26
    // Public Properties
27
    // -------------------------------------------------------------------------
28

29
    /**
30
     * Indicates if executor has any operations to execute (e.g. has insert / update / delete operations to be executed).
31
     */
32
    hasExecutableOperations: boolean = false
153,985✔
33

34
    // -------------------------------------------------------------------------
35
    // Protected Properties
36
    // -------------------------------------------------------------------------
37

38
    /**
39
     * QueryRunner used to execute all queries with a given subjects.
40
     */
41
    protected queryRunner: QueryRunner
42

43
    /**
44
     * Persistence options.
45
     */
46
    protected options?: SaveOptions & RemoveOptions
47

48
    /**
49
     * All subjects that needs to be operated.
50
     */
51
    protected allSubjects: Subject[]
52

53
    /**
54
     * Subjects that must be inserted.
55
     */
56
    protected insertSubjects: Subject[] = []
153,985✔
57

58
    /**
59
     * Subjects that must be updated.
60
     */
61
    protected updateSubjects: Subject[] = []
153,985✔
62

63
    /**
64
     * Subjects that must be removed.
65
     */
66
    protected removeSubjects: Subject[] = []
153,985✔
67

68
    /**
69
     * Subjects that must be soft-removed.
70
     */
71
    protected softRemoveSubjects: Subject[] = []
153,985✔
72

73
    /**
74
     * Subjects that must be recovered.
75
     */
76
    protected recoverSubjects: Subject[] = []
153,985✔
77

78
    // -------------------------------------------------------------------------
79
    // Constructor
80
    // -------------------------------------------------------------------------
81

82
    constructor(
83
        queryRunner: QueryRunner,
84
        subjects: Subject[],
85
        options?: SaveOptions & RemoveOptions,
86
    ) {
87
        this.queryRunner = queryRunner
153,985✔
88
        this.allSubjects = subjects
153,985✔
89
        this.options = options
153,985✔
90
        this.validate()
153,985✔
91
        this.recompute()
153,985✔
92
    }
93

94
    // -------------------------------------------------------------------------
95
    // Public Methods
96
    // -------------------------------------------------------------------------
97

98
    /**
99
     * Executes all operations over given array of subjects.
100
     * Executes queries using given query runner.
101
     */
102
    async execute(): Promise<void> {
103
        // console.time("SubjectExecutor.execute");
104

105
        // broadcast "before" events before we start insert / update / remove operations
106
        let broadcasterResult: BroadcasterResult | undefined = undefined
153,506✔
107
        if (!this.options || this.options.listeners !== false) {
153,506✔
108
            // console.time(".broadcastBeforeEventsForAll");
109
            broadcasterResult = this.broadcastBeforeEventsForAll()
153,434✔
110
            if (broadcasterResult.promises.length > 0)
153,434!
UNCOV
111
                await Promise.all(broadcasterResult.promises)
×
112
            // console.timeEnd(".broadcastBeforeEventsForAll");
113
        }
114

115
        // since event listeners and subscribers can call save methods and/or trigger entity changes we need to recompute operational subjects
116
        // recompute only in the case if any listener or subscriber was really executed
117
        if (broadcasterResult && broadcasterResult.count > 0) {
153,506✔
118
            // console.time(".recompute");
119
            this.insertSubjects.forEach((subject) => subject.recompute())
486✔
120
            this.updateSubjects.forEach((subject) => subject.recompute())
486✔
121
            this.removeSubjects.forEach((subject) => subject.recompute())
486✔
122
            this.softRemoveSubjects.forEach((subject) => subject.recompute())
486✔
123
            this.recoverSubjects.forEach((subject) => subject.recompute())
486✔
124
            this.recompute()
486✔
125
            // console.timeEnd(".recompute");
126
        }
127

128
        // make sure our insert subjects are sorted (using topological sorting) to make cascade inserts work properly
129

130
        // console.timeEnd("prepare");
131

132
        // execute all insert operations
133
        // console.time(".insertion");
134
        this.insertSubjects = new SubjectTopologicalSorter(
153,506✔
135
            this.insertSubjects,
136
        ).sort("insert")
137
        await this.executeInsertOperations()
153,506✔
138
        // console.timeEnd(".insertion");
139

140
        // recompute update operations since insertion can create updation operations for the
141
        // properties it wasn't able to handle on its own (referenced columns)
142
        this.updateSubjects = this.allSubjects.filter(
153,378✔
143
            (subject) => subject.mustBeUpdated,
455,699✔
144
        )
145

146
        // execute update operations
147
        // console.time(".updation");
148
        await this.executeUpdateOperations()
153,378✔
149
        // console.timeEnd(".updation");
150

151
        // make sure our remove subjects are sorted (using topological sorting) when multiple entities are passed for the removal
152
        // console.time(".removal");
153
        this.removeSubjects = new SubjectTopologicalSorter(
153,358✔
154
            this.removeSubjects,
155
        ).sort("delete")
156
        await this.executeRemoveOperations()
153,358✔
157
        // console.timeEnd(".removal");
158

159
        // recompute soft-remove operations
160
        this.softRemoveSubjects = this.allSubjects.filter(
153,358✔
161
            (subject) => subject.mustBeSoftRemoved,
455,663✔
162
        )
163

164
        // execute soft-remove operations
165
        await this.executeSoftRemoveOperations()
153,358✔
166

167
        // recompute recover operations
168
        this.recoverSubjects = this.allSubjects.filter(
153,310✔
169
            (subject) => subject.mustBeRecovered,
455,615✔
170
        )
171

172
        // execute recover operations
173
        await this.executeRecoverOperations()
153,310✔
174

175
        // update all special columns in persisted entities, like inserted id or remove ids from the removed entities
176
        // console.time(".updateSpecialColumnsInPersistedEntities");
177
        this.updateSpecialColumnsInPersistedEntities()
153,262✔
178
        // console.timeEnd(".updateSpecialColumnsInPersistedEntities");
179

180
        // finally broadcast "after" events after we finish insert / update / remove operations
181
        if (!this.options || this.options.listeners !== false) {
153,262✔
182
            // console.time(".broadcastAfterEventsForAll");
183
            broadcasterResult = this.broadcastAfterEventsForAll()
153,190✔
184
            if (broadcasterResult.promises.length > 0)
153,190!
185
                await Promise.all(broadcasterResult.promises)
×
186
            // console.timeEnd(".broadcastAfterEventsForAll");
187
        }
188
        // console.timeEnd("SubjectExecutor.execute");
189
    }
190

191
    // -------------------------------------------------------------------------
192
    // Protected Methods
193
    // -------------------------------------------------------------------------
194

195
    /**
196
     * Validates all given subjects.
197
     */
198
    protected validate() {
199
        this.allSubjects.forEach((subject) => {
153,985✔
200
            if (subject.mustBeUpdated && subject.mustBeRemoved)
456,458!
201
                throw new SubjectRemovedAndUpdatedError(subject)
×
202
        })
203
    }
204

205
    /**
206
     * Performs entity re-computations - finds changed columns, re-builds insert/update/remove subjects.
207
     */
208
    protected recompute(): void {
209
        new SubjectChangedColumnsComputer().compute(this.allSubjects)
154,471✔
210
        this.insertSubjects = this.allSubjects.filter(
154,471✔
211
            (subject) => subject.mustBeInserted,
457,040✔
212
        )
213
        this.updateSubjects = this.allSubjects.filter(
154,471✔
214
            (subject) => subject.mustBeUpdated,
457,040✔
215
        )
216
        this.removeSubjects = this.allSubjects.filter(
154,471✔
217
            (subject) => subject.mustBeRemoved,
457,040✔
218
        )
219
        this.softRemoveSubjects = this.allSubjects.filter(
154,471✔
220
            (subject) => subject.mustBeSoftRemoved,
457,040✔
221
        )
222
        this.recoverSubjects = this.allSubjects.filter(
154,471✔
223
            (subject) => subject.mustBeRecovered,
457,040✔
224
        )
225
        this.hasExecutableOperations =
154,471✔
226
            this.insertSubjects.length > 0 ||
163,581✔
227
            this.updateSubjects.length > 0 ||
228
            this.removeSubjects.length > 0 ||
229
            this.softRemoveSubjects.length > 0 ||
230
            this.recoverSubjects.length > 0
231
    }
232

233
    /**
234
     * Broadcasts "BEFORE_INSERT", "BEFORE_UPDATE", "BEFORE_REMOVE", "BEFORE_SOFT_REMOVE", "BEFORE_RECOVER" events for all given subjects.
235
     */
236
    protected broadcastBeforeEventsForAll(): BroadcasterResult {
237
        const result = new BroadcasterResult()
153,434✔
238
        if (this.insertSubjects.length)
153,434✔
239
            this.insertSubjects.forEach((subject) =>
149,320✔
240
                this.queryRunner.broadcaster.broadcastBeforeInsertEvent(
442,335✔
241
                    result,
242
                    subject.metadata,
243
                    subject.entity!,
244
                ),
245
            )
246
        if (this.updateSubjects.length)
153,434✔
247
            this.updateSubjects.forEach((subject) =>
5,716✔
248
                this.queryRunner.broadcaster.broadcastBeforeUpdateEvent(
8,846✔
249
                    result,
250
                    subject.metadata,
251
                    subject.entity!,
252
                    subject.databaseEntity,
253
                    subject.diffColumns,
254
                    subject.diffRelations,
255
                ),
256
            )
257
        if (this.removeSubjects.length)
153,434✔
258
            this.removeSubjects.forEach((subject) =>
1,472✔
259
                this.queryRunner.broadcaster.broadcastBeforeRemoveEvent(
2,126✔
260
                    result,
261
                    subject.metadata,
262
                    subject.entity!,
263
                    subject.databaseEntity,
264
                    subject.identifier,
265
                ),
266
            )
267
        if (this.softRemoveSubjects.length)
153,434✔
268
            this.softRemoveSubjects.forEach((subject) =>
390✔
269
                this.queryRunner.broadcaster.broadcastBeforeSoftRemoveEvent(
390✔
270
                    result,
271
                    subject.metadata,
272
                    subject.entity!,
273
                    subject.databaseEntity,
274
                    subject.identifier,
275
                ),
276
            )
277
        if (this.recoverSubjects.length)
153,434✔
278
            this.recoverSubjects.forEach((subject) =>
144✔
279
                this.queryRunner.broadcaster.broadcastBeforeRecoverEvent(
144✔
280
                    result,
281
                    subject.metadata,
282
                    subject.entity!,
283
                    subject.databaseEntity,
284
                    subject.identifier,
285
                ),
286
            )
287
        return result
153,434✔
288
    }
289

290
    /**
291
     * Broadcasts "AFTER_INSERT", "AFTER_UPDATE", "AFTER_REMOVE", "AFTER_SOFT_REMOVE", "AFTER_RECOVER" events for all given subjects.
292
     * Returns void if there wasn't any listener or subscriber executed.
293
     * Note: this method has a performance-optimized code organization.
294
     */
295
    protected broadcastAfterEventsForAll(): BroadcasterResult {
296
        const result = new BroadcasterResult()
153,190✔
297
        if (this.insertSubjects.length)
153,190✔
298
            this.insertSubjects.forEach((subject) =>
149,188✔
299
                this.queryRunner.broadcaster.broadcastAfterInsertEvent(
442,199✔
300
                    result,
301
                    subject.metadata,
302
                    subject.entity!,
303
                    subject.identifier,
304
                ),
305
            )
306
        if (this.updateSubjects.length)
153,190✔
307
            this.updateSubjects.forEach((subject) =>
5,772✔
308
                this.queryRunner.broadcaster.broadcastAfterUpdateEvent(
8,898✔
309
                    result,
310
                    subject.metadata,
311
                    subject.entity!,
312
                    subject.databaseEntity,
313
                    subject.diffColumns,
314
                    subject.diffRelations,
315
                ),
316
            )
317
        if (this.removeSubjects.length)
153,190✔
318
            this.removeSubjects.forEach((subject) =>
1,472✔
319
                this.queryRunner.broadcaster.broadcastAfterRemoveEvent(
2,126✔
320
                    result,
321
                    subject.metadata,
322
                    subject.entity!,
323
                    subject.databaseEntity,
324
                    subject.identifier,
325
                ),
326
            )
327
        if (this.softRemoveSubjects.length)
153,190✔
328
            this.softRemoveSubjects.forEach((subject) =>
342✔
329
                this.queryRunner.broadcaster.broadcastAfterSoftRemoveEvent(
342✔
330
                    result,
331
                    subject.metadata,
332
                    subject.entity!,
333
                    subject.databaseEntity,
334
                    subject.identifier,
335
                ),
336
            )
337
        if (this.recoverSubjects.length)
153,190✔
338
            this.recoverSubjects.forEach((subject) =>
96✔
339
                this.queryRunner.broadcaster.broadcastAfterRecoverEvent(
96✔
340
                    result,
341
                    subject.metadata,
342
                    subject.entity!,
343
                    subject.databaseEntity,
344
                    subject.identifier,
345
                ),
346
            )
347
        return result
153,190✔
348
    }
349

350
    /**
351
     * Executes insert operations.
352
     */
353
    protected async executeInsertOperations(): Promise<void> {
354
        // group insertion subjects to make bulk insertions
355
        const [groupedInsertSubjects, groupedInsertSubjectKeys] =
356
            this.groupBulkSubjects(this.insertSubjects, "insert")
153,506✔
357

358
        // then we run insertion in the sequential order which is important since we have an ordered subjects
359
        for (const groupName of groupedInsertSubjectKeys) {
153,506✔
360
            const subjects = groupedInsertSubjects[groupName]
188,120✔
361

362
            // we must separately insert entities which does not have any values to insert
363
            // because its not possible to insert multiple entities with only default values in bulk
364
            const bulkInsertMaps: ObjectLiteral[] = []
188,120✔
365
            const bulkInsertSubjects: Subject[] = []
188,120✔
366
            const singleInsertSubjects: Subject[] = []
188,120✔
367
            if (this.queryRunner.connection.driver.options.type === "mongodb") {
188,120✔
368
                subjects.forEach((subject) => {
192✔
369
                    if (subject.metadata.createDateColumn && subject.entity) {
504✔
370
                        subject.entity[
2✔
371
                            subject.metadata.createDateColumn.databaseName
372
                        ] = new Date()
373
                    }
374

375
                    if (subject.metadata.updateDateColumn && subject.entity) {
504✔
376
                        subject.entity[
10✔
377
                            subject.metadata.updateDateColumn.databaseName
378
                        ] = new Date()
379
                    }
380

381
                    subject.createValueSetAndPopChangeMap()
504✔
382

383
                    bulkInsertSubjects.push(subject)
504✔
384
                    bulkInsertMaps.push(subject.entity!)
504✔
385
                })
386
            } else if (
187,928✔
387
                this.queryRunner.connection.driver.options.type === "oracle"
388
            ) {
389
                subjects.forEach((subject) => {
13,540✔
390
                    singleInsertSubjects.push(subject)
18,058✔
391
                })
392
            } else {
393
                subjects.forEach((subject) => {
174,388✔
394
                    // we do not insert in bulk in following cases:
395
                    // - when there is no values in insert (only defaults are inserted), since we cannot use DEFAULT VALUES expression for multiple inserted rows
396
                    // - when entity is a tree table, since tree tables require extra operation per each inserted row
397
                    // - when oracle is used, since oracle's bulk insertion is very bad
398
                    if (
423,797✔
399
                        subject.changeMaps.length === 0 ||
1,673,491✔
400
                        subject.metadata.treeType ||
401
                        this.queryRunner.connection.driver.options.type ===
402
                            "oracle" ||
403
                        this.queryRunner.connection.driver.options.type ===
404
                            "sap"
405
                    ) {
406
                        singleInsertSubjects.push(subject)
27,635✔
407
                    } else {
408
                        bulkInsertSubjects.push(subject)
396,162✔
409
                        bulkInsertMaps.push(
396,162✔
410
                            subject.createValueSetAndPopChangeMap(),
411
                        )
412
                    }
413
                })
414
            }
415

416
            // for mongodb we have a bit different insertion logic
417
            if (
188,120✔
418
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
419
            ) {
420
                const insertResult = await this.queryRunner.manager.insert(
192✔
421
                    subjects[0].metadata.target,
422
                    bulkInsertMaps,
423
                )
424
                subjects.forEach((subject, index) => {
192✔
425
                    subject.identifier = insertResult.identifiers[index]
504✔
426
                    subject.generatedMap = insertResult.generatedMaps[index]
504✔
427
                    subject.insertedValueSet = bulkInsertMaps[index]
504✔
428
                })
429
            } else {
430
                // here we execute our insertion query
431
                // we need to enable entity updation because we DO need to have updated insertedMap
432
                // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
433
                // also, we disable listeners because we call them on our own in persistence layer
434
                if (bulkInsertMaps.length > 0) {
187,928✔
435
                    const insertResult = await this.queryRunner.manager
150,631✔
436
                        .createQueryBuilder()
437
                        .insert()
438
                        .into(subjects[0].metadata.target)
439
                        .values(bulkInsertMaps)
440
                        .updateEntity(
441
                            this.options && this.options.reload === false
301,377!
442
                                ? false
443
                                : true,
444
                        )
445
                        .callListeners(false)
446
                        .execute()
447

448
                    bulkInsertSubjects.forEach((subject, index) => {
150,591✔
449
                        subject.identifier = insertResult.identifiers[index]
396,122✔
450
                        subject.generatedMap = insertResult.generatedMaps[index]
396,122✔
451
                        subject.insertedValueSet = bulkInsertMaps[index]
396,122✔
452
                    })
453
                }
454

455
                // insert subjects which must be inserted in separate requests (all default values)
456
                if (singleInsertSubjects.length > 0) {
187,888✔
457
                    for (const subject of singleInsertSubjects) {
37,297✔
458
                        subject.insertedValueSet =
45,693✔
459
                            subject.createValueSetAndPopChangeMap() // important to have because query builder sets inserted values into it
460

461
                        // for nested set we execute additional queries
462
                        if (subject.metadata.treeType === "nested-set")
45,693✔
463
                            await new NestedSetSubjectExecutor(
2,356✔
464
                                this.queryRunner,
465
                            ).insert(subject)
466

467
                        await this.queryRunner.manager
45,629✔
468
                            .createQueryBuilder()
469
                            .insert()
470
                            .into(subject.metadata.target)
471
                            .values(subject.insertedValueSet)
472
                            .updateEntity(
473
                                this.options && this.options.reload === false
91,274!
474
                                    ? false
475
                                    : true,
476
                            )
477
                            .callListeners(false)
478
                            .execute()
479
                            .then((insertResult) => {
480
                                subject.identifier = insertResult.identifiers[0]
45,605✔
481
                                subject.generatedMap =
45,605✔
482
                                    insertResult.generatedMaps[0]
483
                            })
484

485
                        // for tree tables we execute additional queries
486
                        if (subject.metadata.treeType === "closure-table") {
45,605✔
487
                            await new ClosureSubjectExecutor(
3,258✔
488
                                this.queryRunner,
489
                            ).insert(subject)
490
                        } else if (
42,347✔
491
                            subject.metadata.treeType === "materialized-path"
492
                        ) {
493
                            await new MaterializedPathSubjectExecutor(
4,504✔
494
                                this.queryRunner,
495
                            ).insert(subject)
496
                        }
497
                    }
498
                }
499
            }
500

501
            subjects.forEach((subject) => {
187,992✔
502
                if (subject.generatedMap) {
442,231✔
503
                    subject.metadata.columns.forEach((column) => {
442,231✔
504
                        const value = column.getEntityValue(
1,560,367✔
505
                            subject.generatedMap!,
506
                        )
507
                        if (value !== undefined && value !== null) {
1,560,367✔
508
                            const preparedValue =
509
                                this.queryRunner.connection.driver.prepareHydratedValue(
243,569✔
510
                                    value,
511
                                    column,
512
                                )
513
                            column.setEntityValue(
243,569✔
514
                                subject.generatedMap!,
515
                                preparedValue,
516
                            )
517
                        }
518
                    })
519
                }
520
            })
521
        }
522
    }
523

524
    /**
525
     * Updates all given subjects in the database.
526
     */
527
    protected async executeUpdateOperations(): Promise<void> {
528
        const updateSubject = async (subject: Subject) => {
153,378✔
529
            if (!subject.identifier)
8,922!
530
                throw new SubjectWithoutIdentifierError(subject)
×
531

532
            // for mongodb we have a bit different updation logic
533
            if (
8,922✔
534
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
535
            ) {
536
                const partialEntity = this.cloneMongoSubjectEntity(subject)
24✔
537
                if (
24✔
538
                    subject.metadata.objectIdColumn &&
48✔
539
                    subject.metadata.objectIdColumn.propertyName
540
                ) {
541
                    delete partialEntity[
24✔
542
                        subject.metadata.objectIdColumn.propertyName
543
                    ]
544
                }
545

546
                if (
24✔
547
                    subject.metadata.createDateColumn &&
26✔
548
                    subject.metadata.createDateColumn.propertyName
549
                ) {
550
                    delete partialEntity[
2✔
551
                        subject.metadata.createDateColumn.propertyName
552
                    ]
553
                }
554

555
                if (
24✔
556
                    subject.metadata.updateDateColumn &&
38✔
557
                    subject.metadata.updateDateColumn.propertyName
558
                ) {
559
                    partialEntity[
14✔
560
                        subject.metadata.updateDateColumn.propertyName
561
                    ] = new Date()
562
                }
563

564
                const manager = this.queryRunner.manager as MongoEntityManager
24✔
565

566
                await manager.update(
24✔
567
                    subject.metadata.target,
568
                    subject.identifier,
569
                    partialEntity,
570
                )
571
            } else {
572
                const updateMap: ObjectLiteral =
573
                    subject.createValueSetAndPopChangeMap()
8,898✔
574

575
                // for tree tables we execute additional queries
576
                switch (subject.metadata.treeType) {
8,898✔
577
                    case "nested-set":
578
                        await new NestedSetSubjectExecutor(
152✔
579
                            this.queryRunner,
580
                        ).update(subject)
581
                        break
136✔
582

583
                    case "closure-table":
584
                        await new ClosureSubjectExecutor(
104✔
585
                            this.queryRunner,
586
                        ).update(subject)
587
                        break
104✔
588

589
                    case "materialized-path":
590
                        await new MaterializedPathSubjectExecutor(
192✔
591
                            this.queryRunner,
592
                        ).update(subject)
593
                        break
192✔
594
                }
595

596
                // here we execute our updation query
597
                // we need to enable entity updation because we update a subject identifier
598
                // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
599
                // also, we disable listeners because we call them on our own in persistence layer
600
                const updateQueryBuilder = this.queryRunner.manager
8,882✔
601
                    .createQueryBuilder()
602
                    .update(subject.metadata.target)
603
                    .set(updateMap)
604
                    .updateEntity(
605
                        this.options && this.options.reload === false
17,808!
606
                            ? false
607
                            : true,
608
                    )
609
                    .callListeners(false)
610

611
                if (subject.entity) {
8,882✔
612
                    updateQueryBuilder.whereEntity(subject.identifier)
2,496✔
613
                } else {
614
                    // in this case identifier is just conditions object to update by
615
                    updateQueryBuilder.where(subject.identifier)
6,386✔
616
                }
617

618
                const updateResult = await updateQueryBuilder.execute()
8,882✔
619
                const updateGeneratedMap = updateResult.generatedMaps[0]
8,874✔
620
                if (updateGeneratedMap) {
8,874✔
621
                    subject.metadata.columns.forEach((column) => {
254✔
622
                        const value = column.getEntityValue(updateGeneratedMap!)
1,370✔
623
                        if (value !== undefined && value !== null) {
1,370✔
624
                            const preparedValue =
625
                                this.queryRunner.connection.driver.prepareHydratedValue(
460✔
626
                                    value,
627
                                    column,
628
                                )
629
                            column.setEntityValue(
460✔
630
                                updateGeneratedMap!,
631
                                preparedValue,
632
                            )
633
                        }
634
                    })
635
                    if (!subject.generatedMap) {
254✔
636
                        subject.generatedMap = {}
226✔
637
                    }
638
                    Object.assign(subject.generatedMap, updateGeneratedMap)
254✔
639
                }
640
            }
641
        }
642

643
        // Nested sets need to be updated one by one
644
        // Split array in two, one with nested set subjects and the other with the remaining subjects
645
        const nestedSetSubjects: Subject[] = []
153,378✔
646
        const remainingSubjects: Subject[] = []
153,378✔
647

648
        for (const subject of this.updateSubjects) {
153,378✔
649
            if (subject.metadata.treeType === "nested-set") {
8,922✔
650
                nestedSetSubjects.push(subject)
152✔
651
            } else {
652
                remainingSubjects.push(subject)
8,770✔
653
            }
654
        }
655

656
        // Run nested set updates one by one
657
        const updateNestSetSubjects = async () => {
153,378✔
658
            for (const subject of nestedSetSubjects) {
153,378✔
659
                await updateSubject(subject)
152✔
660
            }
661
        }
662

663
        // Run all remaining subjects in parallel
664
        await Promise.all([
153,378✔
665
            ...remainingSubjects.map(updateSubject),
666
            updateNestSetSubjects(),
667
        ])
668
    }
669

670
    /**
671
     * Removes all given subjects from the database.
672
     *
673
     * todo: we need to apply topological sort here as well
674
     */
675
    protected async executeRemoveOperations(): Promise<void> {
676
        // group insertion subjects to make bulk insertions
677
        const [groupedRemoveSubjects, groupedRemoveSubjectKeys] =
678
            this.groupBulkSubjects(this.removeSubjects, "delete")
153,358✔
679

680
        for (const groupName of groupedRemoveSubjectKeys) {
153,358✔
681
            const subjects = groupedRemoveSubjects[groupName]
1,904✔
682
            const deleteMaps = subjects.map((subject) => {
1,904✔
683
                if (!subject.identifier)
2,150!
684
                    throw new SubjectWithoutIdentifierError(subject)
×
685

686
                return subject.identifier
2,150✔
687
            })
688

689
            // for mongodb we have a bit different updation logic
690
            if (
1,904✔
691
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
692
            ) {
693
                const manager = this.queryRunner.manager as MongoEntityManager
8✔
694
                await manager.delete(subjects[0].metadata.target, deleteMaps)
8✔
695
            } else {
696
                // for tree tables we execute additional queries
697
                switch (subjects[0].metadata.treeType) {
1,896✔
698
                    case "nested-set":
699
                        await new NestedSetSubjectExecutor(
40✔
700
                            this.queryRunner,
701
                        ).remove(subjects)
702
                        break
40✔
703

704
                    case "closure-table":
705
                        await new ClosureSubjectExecutor(
20✔
706
                            this.queryRunner,
707
                        ).remove(subjects)
708
                        break
20✔
709
                }
710

711
                // here we execute our deletion query
712
                // we don't need to specify entities and set update entity to true since the only thing query builder
713
                // will do for use is a primary keys deletion which is handled by us later once persistence is finished
714
                // also, we disable listeners because we call them on our own in persistence layer
715
                await this.queryRunner.manager
1,896✔
716
                    .createQueryBuilder()
717
                    .delete()
718
                    .from(subjects[0].metadata.target)
719
                    .where(deleteMaps)
720
                    .callListeners(false)
721
                    .execute()
722
            }
723
        }
724
    }
725

726
    private cloneMongoSubjectEntity(subject: Subject): ObjectLiteral {
727
        const target: ObjectLiteral = {}
30✔
728

729
        if (subject.entity) {
30✔
730
            for (const column of subject.metadata.columns) {
30✔
731
                OrmUtils.mergeDeep(
144✔
732
                    target,
733
                    column.getEntityValueMap(subject.entity),
734
                )
735
            }
736
        }
737

738
        return target
30✔
739
    }
740

741
    /**
742
     * Soft-removes all given subjects in the database.
743
     */
744
    protected async executeSoftRemoveOperations(): Promise<void> {
745
        await Promise.all(
153,358✔
746
            this.softRemoveSubjects.map(async (subject) => {
747
                if (!subject.identifier)
414!
748
                    throw new SubjectWithoutIdentifierError(subject)
×
749

750
                let updateResult: UpdateResult
751

752
                // for mongodb we have a bit different updation logic
753
                if (
414✔
754
                    InstanceChecker.isMongoEntityManager(
755
                        this.queryRunner.manager,
756
                    )
757
                ) {
758
                    const partialEntity = this.cloneMongoSubjectEntity(subject)
6✔
759
                    if (
6✔
760
                        subject.metadata.objectIdColumn &&
12✔
761
                        subject.metadata.objectIdColumn.propertyName
762
                    ) {
763
                        delete partialEntity[
6✔
764
                            subject.metadata.objectIdColumn.propertyName
765
                        ]
766
                    }
767

768
                    if (
6!
769
                        subject.metadata.createDateColumn &&
6!
770
                        subject.metadata.createDateColumn.propertyName
771
                    ) {
772
                        delete partialEntity[
×
773
                            subject.metadata.createDateColumn.propertyName
774
                        ]
775
                    }
776

777
                    if (
6!
778
                        subject.metadata.updateDateColumn &&
6!
779
                        subject.metadata.updateDateColumn.propertyName
780
                    ) {
781
                        partialEntity[
×
782
                            subject.metadata.updateDateColumn.propertyName
783
                        ] = new Date()
784
                    }
785

786
                    if (
6✔
787
                        subject.metadata.deleteDateColumn &&
12✔
788
                        subject.metadata.deleteDateColumn.propertyName
789
                    ) {
790
                        partialEntity[
6✔
791
                            subject.metadata.deleteDateColumn.propertyName
792
                        ] = new Date()
793
                    }
794

795
                    const manager = this.queryRunner
6✔
796
                        .manager as MongoEntityManager
797

798
                    updateResult = await manager.update(
6✔
799
                        subject.metadata.target,
800
                        subject.identifier,
801
                        partialEntity,
802
                    )
803
                } else {
804
                    // here we execute our soft-deletion query
805
                    // we need to enable entity soft-deletion because we update a subject identifier
806
                    // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
807
                    // also, we disable listeners because we call them on our own in persistence layer
808
                    const softDeleteQueryBuilder = this.queryRunner.manager
408✔
809
                        .createQueryBuilder()
810
                        .softDelete()
811
                        .from(subject.metadata.target)
812
                        .updateEntity(
813
                            this.options && this.options.reload === false
864!
814
                                ? false
815
                                : true,
816
                        )
817
                        .callListeners(false)
818

819
                    if (subject.entity) {
408✔
820
                        softDeleteQueryBuilder.whereEntity(subject.identifier)
384✔
821
                    } else {
822
                        // in this case identifier is just conditions object to update by
823
                        softDeleteQueryBuilder.where(subject.identifier)
24✔
824
                    }
825

826
                    updateResult = await softDeleteQueryBuilder.execute()
408✔
827
                }
828

829
                subject.generatedMap = updateResult.generatedMaps[0]
366✔
830
                if (subject.generatedMap) {
366✔
831
                    subject.metadata.columns.forEach((column) => {
336✔
832
                        const value = column.getEntityValue(
1,440✔
833
                            subject.generatedMap!,
834
                        )
835
                        if (value !== undefined && value !== null) {
1,440✔
836
                            const preparedValue =
837
                                this.queryRunner.connection.driver.prepareHydratedValue(
570✔
838
                                    value,
839
                                    column,
840
                                )
841
                            column.setEntityValue(
570✔
842
                                subject.generatedMap!,
843
                                preparedValue,
844
                            )
845
                        }
846
                    })
847
                }
848

849
                // experiments, remove probably, need to implement tree tables children removal
850
                // if (subject.updatedRelationMaps.length > 0) {
851
                //     await Promise.all(subject.updatedRelationMaps.map(async updatedRelation => {
852
                //         if (!updatedRelation.relation.isTreeParent) return;
853
                //         if (!updatedRelation.value !== null) return;
854
                //
855
                //         if (subject.metadata.treeType === "closure-table") {
856
                //             await new ClosureSubjectExecutor(this.queryRunner).deleteChildrenOf(subject);
857
                //         }
858
                //     }));
859
                // }
860
            }),
861
        )
862
    }
863

864
    /**
865
     * Recovers all given subjects in the database.
866
     */
867
    protected async executeRecoverOperations(): Promise<void> {
868
        await Promise.all(
153,310✔
869
            this.recoverSubjects.map(async (subject) => {
870
                if (!subject.identifier)
144!
871
                    throw new SubjectWithoutIdentifierError(subject)
×
872

873
                let updateResult: UpdateResult
874

875
                // for mongodb we have a bit different updation logic
876
                if (
144!
877
                    InstanceChecker.isMongoEntityManager(
878
                        this.queryRunner.manager,
879
                    )
880
                ) {
881
                    const partialEntity = this.cloneMongoSubjectEntity(subject)
×
882
                    if (
×
883
                        subject.metadata.objectIdColumn &&
×
884
                        subject.metadata.objectIdColumn.propertyName
885
                    ) {
886
                        delete partialEntity[
×
887
                            subject.metadata.objectIdColumn.propertyName
888
                        ]
889
                    }
890

891
                    if (
×
892
                        subject.metadata.createDateColumn &&
×
893
                        subject.metadata.createDateColumn.propertyName
894
                    ) {
895
                        delete partialEntity[
×
896
                            subject.metadata.createDateColumn.propertyName
897
                        ]
898
                    }
899

900
                    if (
×
901
                        subject.metadata.updateDateColumn &&
×
902
                        subject.metadata.updateDateColumn.propertyName
903
                    ) {
904
                        partialEntity[
×
905
                            subject.metadata.updateDateColumn.propertyName
906
                        ] = new Date()
907
                    }
908

909
                    if (
×
910
                        subject.metadata.deleteDateColumn &&
×
911
                        subject.metadata.deleteDateColumn.propertyName
912
                    ) {
913
                        partialEntity[
×
914
                            subject.metadata.deleteDateColumn.propertyName
915
                        ] = null
916
                    }
917

918
                    const manager = this.queryRunner
×
919
                        .manager as MongoEntityManager
920

921
                    updateResult = await manager.update(
×
922
                        subject.metadata.target,
923
                        subject.identifier,
924
                        partialEntity,
925
                    )
926
                } else {
927
                    // here we execute our restory query
928
                    // we need to enable entity restory because we update a subject identifier
929
                    // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
930
                    // also, we disable listeners because we call them on our own in persistence layer
931
                    const softDeleteQueryBuilder = this.queryRunner.manager
144✔
932
                        .createQueryBuilder()
933
                        .restore()
934
                        .from(subject.metadata.target)
935
                        .updateEntity(
936
                            this.options && this.options.reload === false
312!
937
                                ? false
938
                                : true,
939
                        )
940
                        .callListeners(false)
941

942
                    if (subject.entity) {
144!
943
                        softDeleteQueryBuilder.whereEntity(subject.identifier)
144✔
944
                    } else {
945
                        // in this case identifier is just conditions object to update by
946
                        softDeleteQueryBuilder.where(subject.identifier)
×
947
                    }
948

949
                    updateResult = await softDeleteQueryBuilder.execute()
144✔
950
                }
951

952
                subject.generatedMap = updateResult.generatedMaps[0]
96✔
953
                if (subject.generatedMap) {
96✔
954
                    subject.metadata.columns.forEach((column) => {
96✔
955
                        const value = column.getEntityValue(
576✔
956
                            subject.generatedMap!,
957
                        )
958
                        if (value !== undefined && value !== null) {
576✔
959
                            const preparedValue =
960
                                this.queryRunner.connection.driver.prepareHydratedValue(
84✔
961
                                    value,
962
                                    column,
963
                                )
964
                            column.setEntityValue(
84✔
965
                                subject.generatedMap!,
966
                                preparedValue,
967
                            )
968
                        }
969
                    })
970
                }
971

972
                // experiments, remove probably, need to implement tree tables children removal
973
                // if (subject.updatedRelationMaps.length > 0) {
974
                //     await Promise.all(subject.updatedRelationMaps.map(async updatedRelation => {
975
                //         if (!updatedRelation.relation.isTreeParent) return;
976
                //         if (!updatedRelation.value !== null) return;
977
                //
978
                //         if (subject.metadata.treeType === "closure-table") {
979
                //             await new ClosureSubjectExecutor(this.queryRunner).deleteChildrenOf(subject);
980
                //         }
981
                //     }));
982
                // }
983
            }),
984
        )
985
    }
986

987
    /**
988
     * Updates all special columns of the saving entities (create date, update date, version, etc.).
989
     * Also updates nullable columns and columns with default values.
990
     */
991
    protected updateSpecialColumnsInPersistedEntities(): void {
992
        // update inserted entity properties
993
        if (this.insertSubjects.length)
153,262✔
994
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
149,212✔
995
                this.insertSubjects,
996
            )
997

998
        // update updated entity properties
999
        if (this.updateSubjects.length)
153,262✔
1000
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
5,772✔
1001
                this.updateSubjects,
1002
            )
1003

1004
        // update soft-removed entity properties
1005
        if (this.softRemoveSubjects.length)
153,262✔
1006
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
366✔
1007
                this.softRemoveSubjects,
1008
            )
1009

1010
        // update recovered entity properties
1011
        if (this.recoverSubjects.length)
153,262✔
1012
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
96✔
1013
                this.recoverSubjects,
1014
            )
1015

1016
        // remove ids from the entities that were removed
1017
        if (this.removeSubjects.length) {
153,262✔
1018
            this.removeSubjects.forEach((subject) => {
1,496✔
1019
                if (!subject.entity) return
2,150✔
1020

1021
                subject.metadata.primaryColumns.forEach((primaryColumn) => {
1,268✔
1022
                    primaryColumn.setEntityValue(subject.entity!, undefined)
1,632✔
1023
                })
1024
            })
1025
        }
1026

1027
        // other post-persist updations
1028
        this.allSubjects.forEach((subject) => {
153,262✔
1029
            if (!subject.entity) return
455,567✔
1030

1031
            subject.metadata.relationIds.forEach((relationId) => {
403,379✔
1032
                relationId.setValue(subject.entity!)
7,454✔
1033
            })
1034

1035
            // mongo _id remove
1036
            if (
403,379✔
1037
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
1038
            ) {
1039
                if (
542✔
1040
                    subject.metadata.objectIdColumn &&
1,626✔
1041
                    subject.metadata.objectIdColumn.databaseName &&
1042
                    subject.metadata.objectIdColumn.databaseName !==
1043
                        subject.metadata.objectIdColumn.propertyName
1044
                ) {
1045
                    delete subject.entity[
494✔
1046
                        subject.metadata.objectIdColumn.databaseName
1047
                    ]
1048
                }
1049
            }
1050
        })
1051
    }
1052

1053
    /**
1054
     * Updates all special columns of the saving entities (create date, update date, version, etc.).
1055
     * Also updates nullable columns and columns with default values.
1056
     */
1057
    protected updateSpecialColumnsInInsertedAndUpdatedEntities(
1058
        subjects: Subject[],
1059
    ): void {
1060
        subjects.forEach((subject) => {
155,446✔
1061
            if (!subject.entity) return
451,583✔
1062

1063
            // set values to "null" for nullable columns that did not have values
1064
            subject.metadata.columns.forEach((column) => {
400,277✔
1065
                // if table inheritance is used make sure this column is not child's column
1066
                if (
1,465,767✔
1067
                    subject.metadata.childEntityMetadatas.length > 0 &&
1,466,223✔
1068
                    subject.metadata.childEntityMetadatas
1069
                        .map((metadata) => metadata.target)
888✔
1070
                        .indexOf(column.target) !== -1
1071
                )
1072
                    return
148✔
1073

1074
                // entities does not have virtual columns
1075
                if (column.isVirtual) return
1,465,619✔
1076

1077
                // if column is deletedAt
1078
                if (column.isDeleteDate) return
1,395,089✔
1079

1080
                // update nullable columns
1081
                if (column.isNullable) {
1,392,759✔
1082
                    const columnValue = column.getEntityValue(subject.entity!)
13,071✔
1083
                    if (columnValue === undefined)
13,071✔
1084
                        column.setEntityValue(subject.entity!, null)
6,952✔
1085
                }
1086

1087
                // update relational columns
1088
                if (subject.updatedRelationMaps.length > 0) {
1,392,759✔
1089
                    subject.updatedRelationMaps.forEach(
61,465✔
1090
                        (updatedRelationMap) => {
1091
                            updatedRelationMap.relation.joinColumns.forEach(
68,294✔
1092
                                (column) => {
1093
                                    if (column.isVirtual === true) return
75,081✔
1094

1095
                                    column.setEntityValue(
6,194✔
1096
                                        subject.entity!,
1097
                                        ObjectUtils.isObject(
6,194✔
1098
                                            updatedRelationMap.value,
1099
                                        )
1100
                                            ? column.referencedColumn!.getEntityValue(
1101
                                                  updatedRelationMap.value,
1102
                                              )
1103
                                            : updatedRelationMap.value,
1104
                                    )
1105
                                },
1106
                            )
1107
                        },
1108
                    )
1109
                }
1110
            })
1111

1112
            // merge into entity all generated values returned by a database
1113
            if (subject.generatedMap)
400,277✔
1114
                this.queryRunner.manager.merge(
398,077✔
1115
                    subject.metadata.target,
1116
                    subject.entity,
1117
                    subject.generatedMap,
1118
                )
1119
        })
1120
    }
1121

1122
    /**
1123
     * Groups subjects by metadata names (by tables) to make bulk insertions and deletions possible.
1124
     * However there are some limitations with bulk insertions of data into tables with generated (increment) columns
1125
     * in some drivers. Some drivers like mysql and sqlite does not support returning multiple generated columns
1126
     * after insertion and can only return a single generated column value, that's why its not possible to do bulk insertion,
1127
     * because it breaks insertion result's generatedMap and leads to problems when this subject is used in other subjects saves.
1128
     * That's why we only support bulking in junction tables for those drivers.
1129
     *
1130
     * Other drivers like postgres and sql server support RETURNING / OUTPUT statement which allows to return generated
1131
     * id for each inserted row, that's why bulk insertion is not limited to junction tables in there.
1132
     */
1133
    protected groupBulkSubjects(
1134
        subjects: Subject[],
1135
        type: "insert" | "delete",
1136
    ): [{ [key: string]: Subject[] }, string[]] {
1137
        const group: { [key: string]: Subject[] } = {}
306,864✔
1138
        const keys: string[] = []
306,864✔
1139
        const hasReturningDependColumns = subjects.some((subject) => {
306,864✔
1140
            return subject.metadata.getInsertionReturningColumns().length > 0
238,729✔
1141
        })
1142
        const groupingAllowed =
1143
            type === "delete" ||
306,864✔
1144
            this.queryRunner.connection.driver.isReturningSqlSupported(
1145
                "insert",
1146
            ) ||
1147
            hasReturningDependColumns === false
1148

1149
        subjects.forEach((subject, index) => {
306,864✔
1150
            const key =
1151
                groupingAllowed || subject.metadata.isJunction
444,509✔
1152
                    ? subject.metadata.name
1153
                    : subject.metadata.name + "_" + index
1154
            if (!group[key]) {
444,509✔
1155
                group[key] = [subject]
190,024✔
1156
                keys.push(key)
190,024✔
1157
            } else {
1158
                group[key].push(subject)
254,485✔
1159
            }
1160
        })
1161

1162
        return [group, keys]
306,864✔
1163
    }
1164
}
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