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

typeorm / typeorm / 15219332477

23 May 2025 09:13PM UTC coverage: 17.216% (-59.1%) from 76.346%
15219332477

Pull #11332

github

naorpeled
cr comments - move if block
Pull Request #11332: feat: add new undefined and null behavior flags

1603 of 12759 branches covered (12.56%)

Branch coverage included in aggregate %.

0 of 31 new or added lines in 3 files covered. (0.0%)

14132 existing lines in 166 files now uncovered.

4731 of 24033 relevant lines covered (19.69%)

60.22 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

167
        // recompute recover operations
168
        this.recoverSubjects = this.allSubjects.filter(
111✔
169
            (subject) => subject.mustBeRecovered,
271✔
170
        )
171

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

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

381
                    subject.createValueSetAndPopChangeMap()
252✔
382

383
                    bulkInsertSubjects.push(subject)
252✔
384
                    bulkInsertMaps.push(subject.entity!)
252✔
385
                })
UNCOV
386
            } else if (
×
387
                this.queryRunner.connection.driver.options.type === "oracle"
388
            ) {
UNCOV
389
                subjects.forEach((subject) => {
×
UNCOV
390
                    singleInsertSubjects.push(subject)
×
391
                })
392
            } else {
UNCOV
393
                subjects.forEach((subject) => {
×
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
UNCOV
398
                    if (
×
399
                        subject.changeMaps.length === 0 ||
×
400
                        subject.metadata.treeType ||
401
                        this.queryRunner.connection.driver.options.type ===
402
                            "oracle" ||
403
                        this.queryRunner.connection.driver.options.type ===
404
                            "sap"
405
                    ) {
UNCOV
406
                        singleInsertSubjects.push(subject)
×
407
                    } else {
UNCOV
408
                        bulkInsertSubjects.push(subject)
×
UNCOV
409
                        bulkInsertMaps.push(
×
410
                            subject.createValueSetAndPopChangeMap(),
411
                        )
412
                    }
413
                })
414
            }
415

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

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

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

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

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

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

501
            subjects.forEach((subject) => {
96✔
502
                if (subject.generatedMap) {
252✔
503
                    subject.metadata.columns.forEach((column) => {
252✔
504
                        const value = column.getEntityValue(
1,671✔
505
                            subject.generatedMap!,
506
                        )
507
                        if (value !== undefined && value !== null) {
1,671✔
508
                            const preparedValue =
509
                                this.queryRunner.connection.driver.prepareHydratedValue(
252✔
510
                                    value,
511
                                    column,
512
                                )
513
                            column.setEntityValue(
252✔
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) => {
111✔
529
            if (!subject.identifier)
12!
530
                throw new SubjectWithoutIdentifierError(subject)
×
531

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

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

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

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

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

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

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

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

UNCOV
611
                if (subject.entity) {
×
UNCOV
612
                    updateQueryBuilder.whereEntity(subject.identifier)
×
613
                } else {
614
                    // in this case identifier is just conditions object to update by
UNCOV
615
                    updateQueryBuilder.where(subject.identifier)
×
616
                }
617

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

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

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

668
        // Run all remaining subjects in parallel
669
        await Promise.all([
111✔
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")
111✔
684

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

691
                return subject.identifier
4✔
692
            })
693

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

709
                    case "closure-table":
UNCOV
710
                        await new ClosureSubjectExecutor(
×
711
                            this.queryRunner,
712
                        ).remove(subjects)
UNCOV
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
UNCOV
720
                await this.queryRunner.manager
×
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 = {}
15✔
733

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

743
        return target
15✔
744
    }
745

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

755
                let updateResult: UpdateResult
756

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

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

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

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

800
                    const manager = this.queryRunner
3✔
801
                        .manager as MongoEntityManager
802

803
                    updateResult = await manager.update(
3✔
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
UNCOV
813
                    const softDeleteQueryBuilder = this.queryRunner.manager
×
814
                        .createQueryBuilder()
815
                        .softDelete()
816
                        .from(subject.metadata.target)
817
                        .updateEntity(
818
                            this.options && this.options.reload === false
×
819
                                ? false
820
                                : true,
821
                        )
822
                        .callListeners(false)
823

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

UNCOV
831
                    updateResult = await softDeleteQueryBuilder.execute()
×
832
                }
833

834
                subject.generatedMap = updateResult.generatedMaps[0]
3✔
835
                if (subject.generatedMap) {
3!
UNCOV
836
                    subject.metadata.columns.forEach((column) => {
×
UNCOV
837
                        const value = column.getEntityValue(
×
838
                            subject.generatedMap!,
839
                        )
UNCOV
840
                        if (value !== undefined && value !== null) {
×
841
                            const preparedValue =
UNCOV
842
                                this.queryRunner.connection.driver.prepareHydratedValue(
×
843
                                    value,
844
                                    column,
845
                                )
UNCOV
846
                            column.setEntityValue(
×
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(
111✔
874
            this.recoverSubjects.map(async (subject) => {
UNCOV
875
                if (!subject.identifier)
×
876
                    throw new SubjectWithoutIdentifierError(subject)
×
877

878
                let updateResult: UpdateResult
879

880
                // for mongodb we have a bit different updation logic
UNCOV
881
                if (
×
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
UNCOV
936
                    const softDeleteQueryBuilder = this.queryRunner.manager
×
937
                        .createQueryBuilder()
938
                        .restore()
939
                        .from(subject.metadata.target)
940
                        .updateEntity(
941
                            this.options && this.options.reload === false
×
942
                                ? false
943
                                : true,
944
                        )
945
                        .callListeners(false)
946

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

UNCOV
954
                    updateResult = await softDeleteQueryBuilder.execute()
×
955
                }
956

UNCOV
957
                subject.generatedMap = updateResult.generatedMaps[0]
×
UNCOV
958
                if (subject.generatedMap) {
×
UNCOV
959
                    subject.metadata.columns.forEach((column) => {
×
UNCOV
960
                        const value = column.getEntityValue(
×
961
                            subject.generatedMap!,
962
                        )
UNCOV
963
                        if (value !== undefined && value !== null) {
×
964
                            const preparedValue =
UNCOV
965
                                this.queryRunner.connection.driver.prepareHydratedValue(
×
966
                                    value,
967
                                    column,
968
                                )
UNCOV
969
                            column.setEntityValue(
×
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)
111✔
999
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
92✔
1000
                this.insertSubjects,
1001
            )
1002

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

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

1015
        // update recovered entity properties
1016
        if (this.recoverSubjects.length)
111!
UNCOV
1017
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
×
1018
                this.recoverSubjects,
1019
            )
1020

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

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

1032
        // other post-persist updations
1033
        this.allSubjects.forEach((subject) => {
111✔
1034
            if (!subject.entity) return
271!
1035

1036
            subject.metadata.relationIds.forEach((relationId) => {
271✔
UNCOV
1037
                relationId.setValue(subject.entity!)
×
1038
            })
1039

1040
            // mongo _id remove
1041
            if (
271✔
1042
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
1043
            ) {
1044
                if (
271✔
1045
                    subject.metadata.objectIdColumn &&
813✔
1046
                    subject.metadata.objectIdColumn.databaseName &&
1047
                    subject.metadata.objectIdColumn.databaseName !==
1048
                        subject.metadata.objectIdColumn.propertyName
1049
                ) {
1050
                    delete subject.entity[
247✔
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) => {
107✔
1066
            if (!subject.entity) return
267!
1067

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

1079
                // entities does not have virtual columns
1080
                if (column.isVirtual) return
1,743!
1081

1082
                // if column is deletedAt
1083
                if (column.isDeleteDate) return
1,743✔
1084

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

1092
                // update relational columns
1093
                if (subject.updatedRelationMaps.length > 0) {
1,725!
UNCOV
1094
                    subject.updatedRelationMaps.forEach(
×
1095
                        (updatedRelationMap) => {
UNCOV
1096
                            updatedRelationMap.relation.joinColumns.forEach(
×
1097
                                (column) => {
UNCOV
1098
                                    if (column.isVirtual === true) return
×
1099

UNCOV
1100
                                    column.setEntityValue(
×
1101
                                        subject.entity!,
1102
                                        ObjectUtils.isObject(
×
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)
267✔
1119
                this.queryRunner.manager.merge(
252✔
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[] } = {}
222✔
1143
        const keys: string[] = []
222✔
1144
        const hasReturningDependColumns = subjects.some((subject) => {
222✔
1145
            return subject.metadata.getInsertionReturningColumns().length > 0
252✔
1146
        })
1147
        const groupingAllowed =
1148
            type === "delete" ||
222✔
1149
            this.queryRunner.connection.driver.isReturningSqlSupported(
1150
                "insert",
1151
            ) ||
1152
            hasReturningDependColumns === false
1153

1154
        subjects.forEach((subject, index) => {
222✔
1155
            const key =
1156
                groupingAllowed || subject.metadata.isJunction
256✔
1157
                    ? subject.metadata.name
1158
                    : subject.metadata.name + "_" + index
1159
            if (!group[key]) {
256✔
1160
                group[key] = [subject]
100✔
1161
                keys.push(key)
100✔
1162
            } else {
1163
                group[key].push(subject)
156✔
1164
            }
1165
        })
1166

1167
        return [group, keys]
222✔
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