• 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

72.76
/src/persistence/SubjectExecutor.ts
1
import { QueryRunner } from "../query-runner/QueryRunner"
2
import { Subject } from "./Subject"
3
import { SubjectTopologicalSorter } from "./SubjectTopologicalSorter"
4✔
4
import { SubjectChangedColumnsComputer } from "./SubjectChangedColumnsComputer"
4✔
5
import { SubjectWithoutIdentifierError } from "../error/SubjectWithoutIdentifierError"
4✔
6
import { SubjectRemovedAndUpdatedError } from "../error/SubjectRemovedAndUpdatedError"
4✔
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"
4✔
12
import { NestedSetSubjectExecutor } from "./tree/NestedSetSubjectExecutor"
4✔
13
import { ClosureSubjectExecutor } from "./tree/ClosureSubjectExecutor"
4✔
14
import { MaterializedPathSubjectExecutor } from "./tree/MaterializedPathSubjectExecutor"
4✔
15
import { OrmUtils } from "../util/OrmUtils"
4✔
16
import { UpdateResult } from "../query-builder/result/UpdateResult"
17
import { ObjectUtils } from "../util/ObjectUtils"
4✔
18
import { InstanceChecker } from "../util/InstanceChecker"
4✔
19

20
/**
21
 * Executes all database operations (inserts, updated, deletes) that must be executed
22
 * with given persistence subjects.
23
 */
24
export class SubjectExecutor {
4✔
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
25,715✔
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[] = []
25,715✔
57

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

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

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

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

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

82
    constructor(
83
        queryRunner: QueryRunner,
84
        subjects: Subject[],
85
        options?: SaveOptions & RemoveOptions,
86
    ) {
87
        this.queryRunner = queryRunner
25,715✔
88
        this.allSubjects = subjects
25,715✔
89
        this.options = options
25,715✔
90
        this.validate()
25,715✔
91
        this.recompute()
25,715✔
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
25,640✔
107
        if (!this.options || this.options.listeners !== false) {
25,640✔
108
            // console.time(".broadcastBeforeEventsForAll");
109
            broadcasterResult = this.broadcastBeforeEventsForAll()
25,628✔
110
            if (broadcasterResult.promises.length > 0)
25,628✔
111
                await Promise.all(broadcasterResult.promises)
12✔
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) {
25,640✔
118
            // console.time(".recompute");
119
            this.insertSubjects.forEach((subject) => subject.recompute())
76✔
120
            this.updateSubjects.forEach((subject) => subject.recompute())
76✔
121
            this.removeSubjects.forEach((subject) => subject.recompute())
76✔
122
            this.softRemoveSubjects.forEach((subject) => subject.recompute())
76✔
123
            this.recoverSubjects.forEach((subject) => subject.recompute())
76✔
124
            this.recompute()
76✔
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(
25,640✔
135
            this.insertSubjects,
136
        ).sort("insert")
137
        await this.executeInsertOperations()
25,640✔
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(
25,622✔
143
            (subject) => subject.mustBeUpdated,
50,181✔
144
        )
145

146
        // execute update operations
147
        // console.time(".updation");
148
        await this.executeUpdateOperations()
25,622✔
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(
25,622✔
154
            this.removeSubjects,
155
        ).sort("delete")
156
        await this.executeRemoveOperations()
25,622✔
157
        // console.timeEnd(".removal");
158

159
        // recompute soft-remove operations
160
        this.softRemoveSubjects = this.allSubjects.filter(
25,622✔
161
            (subject) => subject.mustBeSoftRemoved,
50,181✔
162
        )
163

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

167
        // recompute recover operations
168
        this.recoverSubjects = this.allSubjects.filter(
25,614✔
169
            (subject) => subject.mustBeRecovered,
50,173✔
170
        )
171

172
        // execute recover operations
173
        await this.executeRecoverOperations()
25,614✔
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()
25,606✔
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) {
25,606✔
182
            // console.time(".broadcastAfterEventsForAll");
183
            broadcasterResult = this.broadcastAfterEventsForAll()
25,594✔
184
            if (broadcasterResult.promises.length > 0)
25,594!
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) => {
25,715✔
200
            if (subject.mustBeUpdated && subject.mustBeRemoved)
50,300!
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)
25,791✔
210
        this.insertSubjects = this.allSubjects.filter(
25,791✔
211
            (subject) => subject.mustBeInserted,
50,392✔
212
        )
213
        this.updateSubjects = this.allSubjects.filter(
25,791✔
214
            (subject) => subject.mustBeUpdated,
50,392✔
215
        )
216
        this.removeSubjects = this.allSubjects.filter(
25,791✔
217
            (subject) => subject.mustBeRemoved,
50,392✔
218
        )
219
        this.softRemoveSubjects = this.allSubjects.filter(
25,791✔
220
            (subject) => subject.mustBeSoftRemoved,
50,392✔
221
        )
222
        this.recoverSubjects = this.allSubjects.filter(
25,791✔
223
            (subject) => subject.mustBeRecovered,
50,392✔
224
        )
225
        this.hasExecutableOperations =
25,791✔
226
            this.insertSubjects.length > 0 ||
27,208✔
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()
25,628✔
238
        if (this.insertSubjects.length)
25,628✔
239
            this.insertSubjects.forEach((subject) =>
25,017✔
240
                this.queryRunner.broadcaster.broadcastBeforeInsertEvent(
48,054✔
241
                    result,
242
                    subject.metadata,
243
                    subject.entity!,
244
                ),
245
            )
246
        if (this.updateSubjects.length)
25,628✔
247
            this.updateSubjects.forEach((subject) =>
885✔
248
                this.queryRunner.broadcaster.broadcastBeforeUpdateEvent(
1,381✔
249
                    result,
250
                    subject.metadata,
251
                    subject.entity!,
252
                    subject.databaseEntity,
253
                    subject.diffColumns,
254
                    subject.diffRelations,
255
                ),
256
            )
257
        if (this.removeSubjects.length)
25,628✔
258
            this.removeSubjects.forEach((subject) =>
226✔
259
                this.queryRunner.broadcaster.broadcastBeforeRemoveEvent(
330✔
260
                    result,
261
                    subject.metadata,
262
                    subject.entity!,
263
                    subject.databaseEntity,
264
                    subject.identifier,
265
                ),
266
            )
267
        if (this.softRemoveSubjects.length)
25,628✔
268
            this.softRemoveSubjects.forEach((subject) =>
72✔
269
                this.queryRunner.broadcaster.broadcastBeforeSoftRemoveEvent(
72✔
270
                    result,
271
                    subject.metadata,
272
                    subject.entity!,
273
                    subject.databaseEntity,
274
                    subject.identifier,
275
                ),
276
            )
277
        if (this.recoverSubjects.length)
25,628✔
278
            this.recoverSubjects.forEach((subject) =>
24✔
279
                this.queryRunner.broadcaster.broadcastBeforeRecoverEvent(
24✔
280
                    result,
281
                    subject.metadata,
282
                    subject.entity!,
283
                    subject.databaseEntity,
284
                    subject.identifier,
285
                ),
286
            )
287
        return result
25,628✔
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()
25,594✔
297
        if (this.insertSubjects.length)
25,594✔
298
            this.insertSubjects.forEach((subject) =>
24,999✔
299
                this.queryRunner.broadcaster.broadcastAfterInsertEvent(
48,036✔
300
                    result,
301
                    subject.metadata,
302
                    subject.entity!,
303
                    subject.identifier,
304
                ),
305
            )
306
        if (this.updateSubjects.length)
25,594✔
307
            this.updateSubjects.forEach((subject) =>
893✔
308
                this.queryRunner.broadcaster.broadcastAfterUpdateEvent(
1,389✔
309
                    result,
310
                    subject.metadata,
311
                    subject.entity!,
312
                    subject.databaseEntity,
313
                    subject.diffColumns,
314
                    subject.diffRelations,
315
                ),
316
            )
317
        if (this.removeSubjects.length)
25,594✔
318
            this.removeSubjects.forEach((subject) =>
226✔
319
                this.queryRunner.broadcaster.broadcastAfterRemoveEvent(
330✔
320
                    result,
321
                    subject.metadata,
322
                    subject.entity!,
323
                    subject.databaseEntity,
324
                    subject.identifier,
325
                ),
326
            )
327
        if (this.softRemoveSubjects.length)
25,594✔
328
            this.softRemoveSubjects.forEach((subject) =>
64✔
329
                this.queryRunner.broadcaster.broadcastAfterSoftRemoveEvent(
64✔
330
                    result,
331
                    subject.metadata,
332
                    subject.entity!,
333
                    subject.databaseEntity,
334
                    subject.identifier,
335
                ),
336
            )
337
        if (this.recoverSubjects.length)
25,594✔
338
            this.recoverSubjects.forEach((subject) =>
16✔
339
                this.queryRunner.broadcaster.broadcastAfterRecoverEvent(
16✔
340
                    result,
341
                    subject.metadata,
342
                    subject.entity!,
343
                    subject.databaseEntity,
344
                    subject.identifier,
345
                ),
346
            )
347
        return result
25,594✔
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")
25,640✔
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) {
25,640✔
360
            const subjects = groupedInsertSubjects[groupName]
32,275✔
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[] = []
32,275✔
365
            const bulkInsertSubjects: Subject[] = []
32,275✔
366
            const singleInsertSubjects: Subject[] = []
32,275✔
367
            if (this.queryRunner.connection.driver.options.type === "mongodb") {
32,275!
368
                subjects.forEach((subject) => {
×
369
                    if (subject.metadata.createDateColumn && subject.entity) {
×
370
                        subject.entity[
×
371
                            subject.metadata.createDateColumn.databaseName
372
                        ] = new Date()
373
                    }
374

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

381
                    subject.createValueSetAndPopChangeMap()
×
382

383
                    bulkInsertSubjects.push(subject)
×
384
                    bulkInsertMaps.push(subject.entity!)
×
385
                })
386
            } else if (
32,275✔
387
                this.queryRunner.connection.driver.options.type === "oracle"
388
            ) {
389
                subjects.forEach((subject) => {
6,712✔
390
                    singleInsertSubjects.push(subject)
8,962✔
391
                })
392
            } else {
393
                subjects.forEach((subject) => {
25,563✔
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 (
39,096✔
399
                        subject.changeMaps.length === 0 ||
153,957✔
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)
1,169✔
407
                    } else {
408
                        bulkInsertSubjects.push(subject)
37,927✔
409
                        bulkInsertMaps.push(
37,927✔
410
                            subject.createValueSetAndPopChangeMap(),
411
                        )
412
                    }
413
                })
414
            }
415

416
            // for mongodb we have a bit different insertion logic
417
            if (
32,275!
418
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
419
            ) {
420
                const insertResult = await this.queryRunner.manager.insert(
×
421
                    subjects[0].metadata.target,
422
                    bulkInsertMaps,
423
                )
424
                subjects.forEach((subject, index) => {
×
425
                    subject.identifier = insertResult.identifiers[index]
×
426
                    subject.generatedMap = insertResult.generatedMaps[index]
×
427
                    subject.insertedValueSet = bulkInsertMaps[index]
×
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) {
32,275✔
435
                    const insertResult = await this.queryRunner.manager
24,394✔
436
                        .createQueryBuilder()
437
                        .insert()
438
                        .into(subjects[0].metadata.target)
439
                        .values(bulkInsertMaps)
440
                        .updateEntity(
441
                            this.options && this.options.reload === false
48,805!
442
                                ? false
443
                                : true,
444
                        )
445
                        .callListeners(false)
446
                        .execute()
447

448
                    bulkInsertSubjects.forEach((subject, index) => {
24,388✔
449
                        subject.identifier = insertResult.identifiers[index]
37,921✔
450
                        subject.generatedMap = insertResult.generatedMaps[index]
37,921✔
451
                        subject.insertedValueSet = bulkInsertMaps[index]
37,921✔
452
                    })
453
                }
454

455
                // insert subjects which must be inserted in separate requests (all default values)
456
                if (singleInsertSubjects.length > 0) {
32,269✔
457
                    for (const subject of singleInsertSubjects) {
7,881✔
458
                        subject.insertedValueSet =
10,131✔
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")
10,131✔
463
                            await new NestedSetSubjectExecutor(
296✔
464
                                this.queryRunner,
465
                            ).insert(subject)
466

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

485
                        // for tree tables we execute additional queries
486
                        if (subject.metadata.treeType === "closure-table") {
10,119✔
487
                            await new ClosureSubjectExecutor(
488✔
488
                                this.queryRunner,
489
                            ).insert(subject)
490
                        } else if (
9,631✔
491
                            subject.metadata.treeType === "materialized-path"
492
                        ) {
493
                            await new MaterializedPathSubjectExecutor(
652✔
494
                                this.queryRunner,
495
                            ).insert(subject)
496
                        }
497
                    }
498
                }
499
            }
500

501
            subjects.forEach((subject) => {
32,257✔
502
                if (subject.generatedMap) {
48,040✔
503
                    subject.metadata.columns.forEach((column) => {
48,040✔
504
                        const value = column.getEntityValue(
135,656✔
505
                            subject.generatedMap!,
506
                        )
507
                        if (value !== undefined && value !== null) {
135,656✔
508
                            const preparedValue =
509
                                this.queryRunner.connection.driver.prepareHydratedValue(
16,176✔
510
                                    value,
511
                                    column,
512
                                )
513
                            column.setEntityValue(
16,176✔
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) => {
25,622✔
529
            if (!subject.identifier)
1,389!
530
                throw new SubjectWithoutIdentifierError(subject)
×
531

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

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

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

564
                const manager = this.queryRunner.manager as MongoEntityManager
×
565

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

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

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

589
                    case "materialized-path":
590
                        await new MaterializedPathSubjectExecutor(
4✔
591
                            this.queryRunner,
592
                        ).update(subject)
593
                        break
4✔
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
1,389✔
601
                    .createQueryBuilder()
602
                    .update(subject.metadata.target)
603
                    .set(updateMap)
604
                    .updateEntity(
605
                        this.options && this.options.reload === false
2,786!
606
                            ? false
607
                            : true,
608
                    )
609
                    .callListeners(false)
610

611
                if (subject.entity) {
1,389✔
612
                    updateQueryBuilder.whereEntity(subject.identifier)
369✔
613
                } else {
614
                    // in this case identifier is just conditions object to update by
615
                    updateQueryBuilder.where(subject.identifier)
1,020✔
616
                }
617

618
                const updateResult = await updateQueryBuilder.execute()
1,389✔
619
                const updateGeneratedMap = updateResult.generatedMaps[0]
1,389✔
620
                if (updateGeneratedMap) {
1,389✔
621
                    subject.metadata.columns.forEach((column) => {
42✔
622
                        const value = column.getEntityValue(updateGeneratedMap!)
228✔
623
                        if (value !== undefined && value !== null) {
228✔
624
                            const preparedValue =
625
                                this.queryRunner.connection.driver.prepareHydratedValue(
82✔
626
                                    value,
627
                                    column,
628
                                )
629
                            column.setEntityValue(
82✔
630
                                updateGeneratedMap!,
631
                                preparedValue,
632
                            )
633
                        }
634
                    })
635
                    if (!subject.generatedMap) {
42✔
636
                        subject.generatedMap = {}
38✔
637
                    }
638
                    Object.assign(subject.generatedMap, updateGeneratedMap)
42✔
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[] = []
25,622✔
646
        const remainingSubjects: Subject[] = []
25,622✔
647

648
        for (const subject of this.updateSubjects) {
25,622✔
649
            if (subject.metadata.treeType === "nested-set") {
1,389!
650
                nestedSetSubjects.push(subject)
×
651
            } else {
652
                remainingSubjects.push(subject)
1,389✔
653
            }
654
        }
655

656
        // Run nested set updates one by one
657
        const nestedSetPromise = new Promise<void>(async (ok, fail) => {
25,622✔
658
            for (const subject of nestedSetSubjects) {
25,622✔
659
                try {
×
660
                    await updateSubject(subject)
×
661
                } catch (error) {
662
                    fail(error)
×
663
                }
664
            }
665
            ok()
25,622✔
666
        })
667

668
        // Run all remaining subjects in parallel
669
        await Promise.all([
25,622✔
670
            ...remainingSubjects.map(updateSubject),
671
            nestedSetPromise,
672
        ])
673
    }
674

675
    /**
676
     * Removes all given subjects from the database.
677
     *
678
     * todo: we need to apply topological sort here as well
679
     */
680
    protected async executeRemoveOperations(): Promise<void> {
681
        // group insertion subjects to make bulk insertions
682
        const [groupedRemoveSubjects, groupedRemoveSubjectKeys] =
683
            this.groupBulkSubjects(this.removeSubjects, "delete")
25,622✔
684

685
        for (const groupName of groupedRemoveSubjectKeys) {
25,622✔
686
            const subjects = groupedRemoveSubjects[groupName]
298✔
687
            const deleteMaps = subjects.map((subject) => {
298✔
688
                if (!subject.identifier)
334!
689
                    throw new SubjectWithoutIdentifierError(subject)
×
690

691
                return subject.identifier
334✔
692
            })
693

694
            // for mongodb we have a bit different updation logic
695
            if (
298!
696
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
697
            ) {
698
                const manager = this.queryRunner.manager as MongoEntityManager
×
699
                await manager.delete(subjects[0].metadata.target, deleteMaps)
×
700
            } else {
701
                // for tree tables we execute additional queries
702
                switch (subjects[0].metadata.treeType) {
298!
703
                    case "nested-set":
704
                        await new NestedSetSubjectExecutor(
×
705
                            this.queryRunner,
706
                        ).remove(subjects)
707
                        break
×
708

709
                    case "closure-table":
710
                        await new ClosureSubjectExecutor(
×
711
                            this.queryRunner,
712
                        ).remove(subjects)
713
                        break
×
714
                }
715

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

731
    private cloneMongoSubjectEntity(subject: Subject): ObjectLiteral {
732
        const target: ObjectLiteral = {}
×
733

734
        if (subject.entity) {
×
735
            for (const column of subject.metadata.columns) {
×
736
                OrmUtils.mergeDeep(
×
737
                    target,
738
                    column.getEntityValueMap(subject.entity),
739
                )
740
            }
741
        }
742

743
        return target
×
744
    }
745

746
    /**
747
     * Soft-removes all given subjects in the database.
748
     */
749
    protected async executeSoftRemoveOperations(): Promise<void> {
750
        await Promise.all(
25,622✔
751
            this.softRemoveSubjects.map(async (subject) => {
752
                if (!subject.identifier)
76!
753
                    throw new SubjectWithoutIdentifierError(subject)
×
754

755
                let updateResult: UpdateResult
756

757
                // for mongodb we have a bit different updation logic
758
                if (
76!
759
                    InstanceChecker.isMongoEntityManager(
760
                        this.queryRunner.manager,
761
                    )
762
                ) {
763
                    const partialEntity = this.cloneMongoSubjectEntity(subject)
×
764
                    if (
×
765
                        subject.metadata.objectIdColumn &&
×
766
                        subject.metadata.objectIdColumn.propertyName
767
                    ) {
768
                        delete partialEntity[
×
769
                            subject.metadata.objectIdColumn.propertyName
770
                        ]
771
                    }
772

773
                    if (
×
774
                        subject.metadata.createDateColumn &&
×
775
                        subject.metadata.createDateColumn.propertyName
776
                    ) {
777
                        delete partialEntity[
×
778
                            subject.metadata.createDateColumn.propertyName
779
                        ]
780
                    }
781

782
                    if (
×
783
                        subject.metadata.updateDateColumn &&
×
784
                        subject.metadata.updateDateColumn.propertyName
785
                    ) {
786
                        partialEntity[
×
787
                            subject.metadata.updateDateColumn.propertyName
788
                        ] = new Date()
789
                    }
790

791
                    if (
×
792
                        subject.metadata.deleteDateColumn &&
×
793
                        subject.metadata.deleteDateColumn.propertyName
794
                    ) {
795
                        partialEntity[
×
796
                            subject.metadata.deleteDateColumn.propertyName
797
                        ] = new Date()
798
                    }
799

800
                    const manager = this.queryRunner
×
801
                        .manager as MongoEntityManager
802

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

824
                    if (subject.entity) {
76✔
825
                        softDeleteQueryBuilder.whereEntity(subject.identifier)
64✔
826
                    } else {
827
                        // in this case identifier is just conditions object to update by
828
                        softDeleteQueryBuilder.where(subject.identifier)
12✔
829
                    }
830

831
                    updateResult = await softDeleteQueryBuilder.execute()
76✔
832
                }
833

834
                subject.generatedMap = updateResult.generatedMaps[0]
68✔
835
                if (subject.generatedMap) {
68✔
836
                    subject.metadata.columns.forEach((column) => {
56✔
837
                        const value = column.getEntityValue(
240✔
838
                            subject.generatedMap!,
839
                        )
840
                        if (value !== undefined && value !== null) {
240✔
841
                            const preparedValue =
842
                                this.queryRunner.connection.driver.prepareHydratedValue(
102✔
843
                                    value,
844
                                    column,
845
                                )
846
                            column.setEntityValue(
102✔
847
                                subject.generatedMap!,
848
                                preparedValue,
849
                            )
850
                        }
851
                    })
852
                }
853

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

869
    /**
870
     * Recovers all given subjects in the database.
871
     */
872
    protected async executeRecoverOperations(): Promise<void> {
873
        await Promise.all(
25,614✔
874
            this.recoverSubjects.map(async (subject) => {
875
                if (!subject.identifier)
24!
876
                    throw new SubjectWithoutIdentifierError(subject)
×
877

878
                let updateResult: UpdateResult
879

880
                // for mongodb we have a bit different updation logic
881
                if (
24!
882
                    InstanceChecker.isMongoEntityManager(
883
                        this.queryRunner.manager,
884
                    )
885
                ) {
886
                    const partialEntity = this.cloneMongoSubjectEntity(subject)
×
887
                    if (
×
888
                        subject.metadata.objectIdColumn &&
×
889
                        subject.metadata.objectIdColumn.propertyName
890
                    ) {
891
                        delete partialEntity[
×
892
                            subject.metadata.objectIdColumn.propertyName
893
                        ]
894
                    }
895

896
                    if (
×
897
                        subject.metadata.createDateColumn &&
×
898
                        subject.metadata.createDateColumn.propertyName
899
                    ) {
900
                        delete partialEntity[
×
901
                            subject.metadata.createDateColumn.propertyName
902
                        ]
903
                    }
904

905
                    if (
×
906
                        subject.metadata.updateDateColumn &&
×
907
                        subject.metadata.updateDateColumn.propertyName
908
                    ) {
909
                        partialEntity[
×
910
                            subject.metadata.updateDateColumn.propertyName
911
                        ] = new Date()
912
                    }
913

914
                    if (
×
915
                        subject.metadata.deleteDateColumn &&
×
916
                        subject.metadata.deleteDateColumn.propertyName
917
                    ) {
918
                        partialEntity[
×
919
                            subject.metadata.deleteDateColumn.propertyName
920
                        ] = null
921
                    }
922

923
                    const manager = this.queryRunner
×
924
                        .manager as MongoEntityManager
925

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

947
                    if (subject.entity) {
24!
948
                        softDeleteQueryBuilder.whereEntity(subject.identifier)
24✔
949
                    } else {
950
                        // in this case identifier is just conditions object to update by
951
                        softDeleteQueryBuilder.where(subject.identifier)
×
952
                    }
953

954
                    updateResult = await softDeleteQueryBuilder.execute()
24✔
955
                }
956

957
                subject.generatedMap = updateResult.generatedMaps[0]
16✔
958
                if (subject.generatedMap) {
16✔
959
                    subject.metadata.columns.forEach((column) => {
16✔
960
                        const value = column.getEntityValue(
96✔
961
                            subject.generatedMap!,
962
                        )
963
                        if (value !== undefined && value !== null) {
96✔
964
                            const preparedValue =
965
                                this.queryRunner.connection.driver.prepareHydratedValue(
16✔
966
                                    value,
967
                                    column,
968
                                )
969
                            column.setEntityValue(
16✔
970
                                subject.generatedMap!,
971
                                preparedValue,
972
                            )
973
                        }
974
                    })
975
                }
976

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

992
    /**
993
     * Updates all special columns of the saving entities (create date, update date, version, etc.).
994
     * Also updates nullable columns and columns with default values.
995
     */
996
    protected updateSpecialColumnsInPersistedEntities(): void {
997
        // update inserted entity properties
998
        if (this.insertSubjects.length)
25,606✔
999
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
25,003✔
1000
                this.insertSubjects,
1001
            )
1002

1003
        // update updated entity properties
1004
        if (this.updateSubjects.length)
25,606✔
1005
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
893✔
1006
                this.updateSubjects,
1007
            )
1008

1009
        // update soft-removed entity properties
1010
        if (this.softRemoveSubjects.length)
25,606✔
1011
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
68✔
1012
                this.softRemoveSubjects,
1013
            )
1014

1015
        // update recovered entity properties
1016
        if (this.recoverSubjects.length)
25,606✔
1017
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
16✔
1018
                this.recoverSubjects,
1019
            )
1020

1021
        // remove ids from the entities that were removed
1022
        if (this.removeSubjects.length) {
25,606✔
1023
            this.removeSubjects.forEach((subject) => {
230✔
1024
                if (!subject.entity) return
334✔
1025

1026
                subject.metadata.primaryColumns.forEach((primaryColumn) => {
186✔
1027
                    primaryColumn.setEntityValue(subject.entity!, undefined)
238✔
1028
                })
1029
            })
1030
        }
1031

1032
        // other post-persist updations
1033
        this.allSubjects.forEach((subject) => {
25,606✔
1034
            if (!subject.entity) return
50,165✔
1035

1036
            subject.metadata.relationIds.forEach((relationId) => {
41,628✔
1037
                relationId.setValue(subject.entity!)
1,156✔
1038
            })
1039

1040
            // mongo _id remove
1041
            if (
41,628!
1042
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
1043
            ) {
1044
                if (
×
1045
                    subject.metadata.objectIdColumn &&
×
1046
                    subject.metadata.objectIdColumn.databaseName &&
1047
                    subject.metadata.objectIdColumn.databaseName !==
1048
                        subject.metadata.objectIdColumn.propertyName
1049
                ) {
1050
                    delete subject.entity[
×
1051
                        subject.metadata.objectIdColumn.databaseName
1052
                    ]
1053
                }
1054
            }
1055
        })
1056
    }
1057

1058
    /**
1059
     * Updates all special columns of the saving entities (create date, update date, version, etc.).
1060
     * Also updates nullable columns and columns with default values.
1061
     */
1062
    protected updateSpecialColumnsInInsertedAndUpdatedEntities(
1063
        subjects: Subject[],
1064
    ): void {
1065
        subjects.forEach((subject) => {
25,980✔
1066
            if (!subject.entity) return
49,513✔
1067

1068
            // set values to "null" for nullable columns that did not have values
1069
            subject.metadata.columns.forEach((column) => {
41,124✔
1070
                // if table inheritance is used make sure this column is not child's column
1071
                if (
119,920✔
1072
                    subject.metadata.childEntityMetadatas.length > 0 &&
119,992✔
1073
                    subject.metadata.childEntityMetadatas
1074
                        .map((metadata) => metadata.target)
144✔
1075
                        .indexOf(column.target) !== -1
1076
                )
1077
                    return
24✔
1078

1079
                // entities does not have virtual columns
1080
                if (column.isVirtual) return
119,896✔
1081

1082
                // if column is deletedAt
1083
                if (column.isDeleteDate) return
109,873✔
1084

1085
                // update nullable columns
1086
                if (column.isNullable) {
109,533✔
1087
                    const columnValue = column.getEntityValue(subject.entity!)
1,627✔
1088
                    if (columnValue === undefined)
1,627✔
1089
                        column.setEntityValue(subject.entity!, null)
744✔
1090
                }
1091

1092
                // update relational columns
1093
                if (subject.updatedRelationMaps.length > 0) {
109,533✔
1094
                    subject.updatedRelationMaps.forEach(
9,584✔
1095
                        (updatedRelationMap) => {
1096
                            updatedRelationMap.relation.joinColumns.forEach(
10,681✔
1097
                                (column) => {
1098
                                    if (column.isVirtual === true) return
11,602✔
1099

1100
                                    column.setEntityValue(
1,068✔
1101
                                        subject.entity!,
1102
                                        ObjectUtils.isObject(
1,068✔
1103
                                            updatedRelationMap.value,
1104
                                        )
1105
                                            ? column.referencedColumn!.getEntityValue(
1106
                                                  updatedRelationMap.value,
1107
                                              )
1108
                                            : updatedRelationMap.value,
1109
                                    )
1110
                                },
1111
                            )
1112
                        },
1113
                    )
1114
                }
1115
            })
1116

1117
            // merge into entity all generated values returned by a database
1118
            if (subject.generatedMap)
41,124✔
1119
                this.queryRunner.manager.merge(
40,805✔
1120
                    subject.metadata.target as any,
1121
                    subject.entity,
1122
                    subject.generatedMap,
1123
                )
1124
        })
1125
    }
1126

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

1154
        subjects.forEach((subject, index) => {
51,262✔
1155
            const key =
1156
                groupingAllowed || subject.metadata.isJunction
48,392✔
1157
                    ? subject.metadata.name
1158
                    : subject.metadata.name + "_" + index
1159
            if (!group[key]) {
48,392✔
1160
                group[key] = [subject]
32,573✔
1161
                keys.push(key)
32,573✔
1162
            } else {
1163
                group[key].push(subject)
15,819✔
1164
            }
1165
        })
1166

1167
        return [group, keys]
51,262✔
1168
    }
1169
}
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