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

typeorm / typeorm / 23390157208

21 Mar 2026 10:26PM UTC coverage: 56.678% (-16.6%) from 73.277%
23390157208

Pull #12252

github

web-flow
Merge 5b60ba41c into 7038fa166
Pull Request #12252: fix: unskip cascade soft remove test

17767 of 26580 branches covered (66.84%)

Branch coverage included in aggregate %.

64033 of 117744 relevant lines covered (54.38%)

1514.83 hits per line

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

97.41
/src/persistence/SubjectDatabaseEntityLoader.ts
1
import type { Subject } from "./Subject"
28✔
2
import type { ObjectLiteral } from "../common/ObjectLiteral"
28✔
3
import type { QueryRunner } from "../query-runner/QueryRunner"
28✔
4
import type { FindManyOptions } from "../find-options/FindManyOptions"
28✔
5
import type { MongoRepository } from "../repository/MongoRepository"
28✔
6
import { OrmUtils } from "../util/OrmUtils"
28✔
7

28✔
8
/**
28✔
9
 * Loads database entities for all operate subjects which do not have database entity set.
28✔
10
 * All entities that we load database entities for are marked as updated or inserted.
28✔
11
 * To understand which of them really needs to be inserted or updated we need to load
28✔
12
 * their original representations from the database.
28✔
13
 */
28✔
14
export class SubjectDatabaseEntityLoader {
28✔
15
    // ---------------------------------------------------------------------
28✔
16
    // Constructor
28✔
17
    // ---------------------------------------------------------------------
28✔
18

28✔
19
    constructor(
28✔
20
        protected queryRunner: QueryRunner,
6,109✔
21
        protected subjects: Subject[],
6,109✔
22
    ) {}
6,109✔
23

28✔
24
    // ---------------------------------------------------------------------
28✔
25
    // Public Methods
28✔
26
    // ---------------------------------------------------------------------
28✔
27

28✔
28
    /**
28✔
29
     * Loads database entities for all subjects.
28✔
30
     *
28✔
31
     * loadAllRelations flag is used to load all relation ids of the object, no matter if they present in subject entity or not.
28✔
32
     * This option is used for deletion.
28✔
33
     * @param operationType
28✔
34
     */
28✔
35
    async load(
28✔
36
        operationType: "save" | "remove" | "soft-remove" | "recover",
6,109✔
37
    ): Promise<void> {
6,109✔
38
        // we are grouping subjects by target to perform more optimized queries using WHERE IN operator
6,109✔
39
        // go through the groups and perform loading of database entities of each subject in the group
6,109✔
40
        const promises = this.groupByEntityTargets().map(
6,109✔
41
            async (subjectGroup) => {
6,109✔
42
                // prepare entity ids of the subjects we need to load
6,597✔
43
                const allIds: ObjectLiteral[] = []
6,597✔
44
                const allSubjects: Subject[] = []
6,597✔
45
                subjectGroup.subjects.forEach((subject) => {
6,597✔
46
                    // we don't load if subject already has a database entity loaded
8,113✔
47
                    if (subject.databaseEntity || !subject.identifier) return
8,113✔
48

4,246✔
49
                    allIds.push(subject.identifier)
4,246✔
50
                    allSubjects.push(subject)
4,246✔
51
                })
6,597✔
52

6,597✔
53
                // if there no ids found (means all entities are new and have generated ids) - then nothing to load there
6,597✔
54
                if (!allIds.length) return
6,597✔
55

3,970✔
56
                const loadRelationPropertyPaths: string[] = []
3,970✔
57

3,970✔
58
                // for the save, soft-remove and recover operation
3,970✔
59
                // extract all property paths of the relations we need to load relation ids for
3,970✔
60
                // this is for optimization purpose - this way we don't load relation ids for entities
3,970✔
61
                // whose relations are undefined, and since they are undefined its really pointless to
3,970✔
62
                // load something for them, since undefined properties are skipped by the orm
3,970✔
63
                if (
3,970✔
64
                    operationType === "save" ||
3,970✔
65
                    operationType === "soft-remove" ||
6,597✔
66
                    operationType === "recover"
100✔
67
                ) {
6,597✔
68
                    subjectGroup.subjects.forEach((subject) => {
3,926✔
69
                        // gets all relation property paths that exist in the persisted entity.
4,222✔
70
                        subject.metadata.relations.forEach((relation) => {
4,222✔
71
                            const value = relation.getEntityValue(
2,265✔
72
                                subject.entityWithFulfilledIds!,
2,265✔
73
                            )
2,265✔
74
                            if (value === undefined) return
2,265✔
75

1,038✔
76
                            if (
1,038✔
77
                                loadRelationPropertyPaths.indexOf(
1,038✔
78
                                    relation.propertyPath,
1,038✔
79
                                ) === -1
1,038✔
80
                            )
1,038✔
81
                                loadRelationPropertyPaths.push(
1,616✔
82
                                    relation.propertyPath,
1,019✔
83
                                )
1,019✔
84
                        })
4,222✔
85
                    })
3,926✔
86
                } else {
6,597✔
87
                    // remove
44✔
88

44✔
89
                    // for remove operation
44✔
90
                    // we only need to load junction relation ids since only they are removed by cascades
44✔
91
                    loadRelationPropertyPaths.push(
44✔
92
                        ...subjectGroup.subjects[0].metadata.manyToManyRelations.map(
44✔
93
                            (relation) => relation.propertyPath,
44✔
94
                        ),
44✔
95
                    )
44✔
96
                }
44✔
97

3,970✔
98
                const findOptions: FindManyOptions<any> = {
3,970✔
99
                    loadEagerRelations: false,
3,970✔
100
                    loadRelationIds: {
3,970✔
101
                        relations: loadRelationPropertyPaths,
3,970✔
102
                        disableMixedMap: true,
3,970✔
103
                    },
3,970✔
104
                    // the soft-deleted entities should be included in the loaded entities for recover operation
3,970✔
105
                    withDeleted: true,
3,970✔
106
                }
3,970✔
107

3,970✔
108
                // load database entities for all given ids
3,970✔
109
                let entities: any[]
3,970✔
110
                if (
3,970✔
111
                    this.queryRunner.connection.driver.options.type ===
3,970✔
112
                    "mongodb"
3,970✔
113
                ) {
6,597!
114
                    const mongoRepo =
×
115
                        this.queryRunner.manager.getRepository<ObjectLiteral>(
×
116
                            subjectGroup.target,
×
117
                        ) as MongoRepository<ObjectLiteral>
×
118
                    entities = await mongoRepo.findByIds(allIds, findOptions)
×
119
                } else {
6,597✔
120
                    entities = await this.queryRunner.manager
3,970✔
121
                        .getRepository<ObjectLiteral>(subjectGroup.target)
3,970✔
122
                        .createQueryBuilder()
3,970✔
123
                        .setFindOptions(findOptions)
3,970✔
124
                        .whereInIds(allIds)
3,970✔
125
                        .getMany()
3,970✔
126
                }
3,970✔
127

3,970✔
128
                // Now when we have entities we need to find subject of each entity
3,970✔
129
                // and insert that entity into database entity of the found subjects.
3,970✔
130
                // A single entity can be applied to many subjects as there might be duplicates.
3,970✔
131
                // This will likely result in the same row being updated multiple times during a transaction.
3,970✔
132
                entities.forEach((entity) => {
3,970✔
133
                    const entityId =
560✔
134
                        allSubjects[0].metadata.getEntityIdMap(entity)
560✔
135
                    allSubjects.forEach((subject) => {
560✔
136
                        if (subject.databaseEntity) return
1,192✔
137
                        if (OrmUtils.compareIds(subject.identifier, entityId))
877✔
138
                            subject.databaseEntity = entity
1,192✔
139
                    })
560✔
140
                })
3,970✔
141

3,970✔
142
                // this way we tell what subjects we tried to load database entities of
3,970✔
143
                for (const subject of allSubjects) {
6,597✔
144
                    subject.databaseEntityLoaded = true
4,246✔
145
                }
4,246✔
146
            },
6,109✔
147
        )
6,109✔
148

6,109✔
149
        await Promise.all(promises)
6,109✔
150
    }
6,109✔
151

28✔
152
    // ---------------------------------------------------------------------
28✔
153
    // Protected Methods
28✔
154
    // ---------------------------------------------------------------------
28✔
155

28✔
156
    /**
28✔
157
     * Groups given Subject objects into groups separated by entity targets.
28✔
158
     */
28✔
159
    protected groupByEntityTargets(): {
28✔
160
        target: Function | string
6,109✔
161
        subjects: Subject[]
6,109✔
162
    }[] {
6,109✔
163
        return this.subjects.reduce(
6,109✔
164
            (groups, operatedEntity) => {
6,109✔
165
                let group = groups.find(
8,113✔
166
                    (group) => group.target === operatedEntity.metadata.target,
8,113✔
167
                )
8,113✔
168
                if (!group) {
8,113✔
169
                    group = {
6,597✔
170
                        target: operatedEntity.metadata.target,
6,597✔
171
                        subjects: [],
6,597✔
172
                    }
6,597✔
173
                    groups.push(group)
6,597✔
174
                }
6,597✔
175
                group.subjects.push(operatedEntity)
8,113✔
176
                return groups
8,113✔
177
            },
6,109✔
178
            [] as { target: Function | string; subjects: Subject[] }[],
6,109✔
179
        )
6,109✔
180
    }
6,109✔
181
}
28✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc