• 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

72.01
/src/persistence/SubjectChangedColumnsComputer.ts
1
import type { ObjectLiteral } from "../common/ObjectLiteral"
28✔
2
import { ApplyValueTransformers } from "../util/ApplyValueTransformers"
28✔
3
import { DateUtils } from "../util/DateUtils"
28✔
4
import { ObjectUtils } from "../util/ObjectUtils"
28✔
5
import { OrmUtils } from "../util/OrmUtils"
28✔
6
import type { Subject } from "./Subject"
28✔
7
import { areUint8ArraysEqual, isUint8Array } from "../util/Uint8ArrayUtils"
28✔
8

28✔
9
/**
28✔
10
 * Finds what columns are changed in the subject entities.
28✔
11
 */
28✔
12
export class SubjectChangedColumnsComputer {
28✔
13
    // -------------------------------------------------------------------------
28✔
14
    // Public Methods
28✔
15
    // -------------------------------------------------------------------------
28✔
16

28✔
17
    /**
28✔
18
     * Finds what columns are changed in the subject entities.
28✔
19
     * @param subjects List of subjects for which to compute changed columns.
28✔
20
     */
28✔
21
    compute(subjects: Subject[]) {
28✔
22
        subjects.forEach((subject) => {
6,129✔
23
            this.computeDiffColumns(subject)
10,177✔
24
            this.computeDiffRelationalColumns(subjects, subject)
10,177✔
25
        })
6,129✔
26
    }
6,129✔
27

28✔
28
    // -------------------------------------------------------------------------
28✔
29
    // Protected Methods
28✔
30
    // -------------------------------------------------------------------------
28✔
31

28✔
32
    /**
28✔
33
     * Differentiate columns from the updated entity and entity stored in the database.
28✔
34
     * @param subject Subject for which to compute differentiated columns.
28✔
35
     */
28✔
36
    protected computeDiffColumns(subject: Subject): void {
28✔
37
        // if there is no persisted entity then nothing to compute changed in it
10,177✔
38
        if (!subject.entity) return
10,177✔
39

8,137✔
40
        subject.metadata.columns.forEach((column) => {
8,137✔
41
            // ignore special columns
26,945✔
42
            if (
26,945✔
43
                column.isVirtual ||
26,945✔
44
                column.isDiscriminator // ||
26,945✔
45
                // column.isUpdateDate ||
26,945✔
46
                // column.isVersion ||
26,945✔
47
                // column.isCreateDate
26,945✔
48
            )
26,945✔
49
                return
26,945✔
50

23,879✔
51
            const changeMap = subject.changeMaps.find(
23,879✔
52
                (changeMap) => changeMap.column === column,
23,879✔
53
            )
23,879✔
54
            if (changeMap) {
26,945✔
55
                subject.changeMaps.splice(
43✔
56
                    subject.changeMaps.indexOf(changeMap),
43✔
57
                    1,
43✔
58
                )
43✔
59
            }
43✔
60

23,879✔
61
            // get user provided value - column value from the user provided persisted entity
23,879✔
62
            const entityValue = column.getEntityValue(subject.entity!)
23,879✔
63

23,879✔
64
            // we don't perform operation over undefined properties (but we DO need null properties!)
23,879✔
65
            if (entityValue === undefined) return
23,879✔
66

19,167✔
67
            // if there is no database entity then all columns are treated as new, e.g. changed
19,167✔
68
            if (subject.databaseEntity) {
26,945✔
69
                // skip transform database value for json / jsonb for comparison later on
1,821✔
70
                const shouldTransformDatabaseEntity =
1,821✔
71
                    column.type !== "json" && column.type !== "jsonb"
1,821✔
72

1,821✔
73
                // get database value of the column
1,821✔
74
                let databaseValue = column.getEntityValue(
1,821✔
75
                    subject.databaseEntity,
1,821✔
76
                    shouldTransformDatabaseEntity,
1,821✔
77
                )
1,821✔
78

1,821✔
79
                // filter out "relational columns" only in the case if there is a relation object in entity
1,821✔
80
                if (column.relationMetadata) {
1,821✔
81
                    const value = column.relationMetadata.getEntityValue(
17✔
82
                        subject.entity!,
17✔
83
                    )
17✔
84
                    if (value !== null && value !== undefined) return
17!
85
                }
17✔
86
                let normalizedValue = entityValue
1,819✔
87

1,819✔
88
                if (
1,819✔
89
                    column.transformer &&
1,819!
90
                    shouldTransformDatabaseEntity &&
1,821!
91
                    entityValue !== null
12✔
92
                ) {
1,821!
93
                    normalizedValue = ApplyValueTransformers.transformTo(
10✔
94
                        column.transformer,
10✔
95
                        entityValue,
10✔
96
                    )
10✔
97
                }
10✔
98

1,819✔
99
                // if both values are not null, normalize special values to make proper comparision
1,819✔
100
                if (normalizedValue !== null && databaseValue !== null) {
1,821✔
101
                    switch (column.type) {
1,562✔
102
                        case "date":
1,562!
103
                            normalizedValue = column.isArray
×
104
                                ? normalizedValue.map((date: Date) =>
×
105
                                      DateUtils.mixedDateToDateString(date),
×
106
                                  )
×
107
                                : DateUtils.mixedDateToDateString(
×
108
                                      normalizedValue,
×
109
                                  )
×
110
                            databaseValue = column.isArray
×
111
                                ? databaseValue.map((date: Date) =>
×
112
                                      DateUtils.mixedDateToDateString(date),
×
113
                                  )
×
114
                                : DateUtils.mixedDateToDateString(databaseValue)
×
115
                            break
×
116

1,562✔
117
                        case "time":
1,562!
118
                        case "time with time zone":
1,562!
119
                        case "time without time zone":
1,562!
120
                        case "timetz":
1,562!
121
                            normalizedValue = column.isArray
×
122
                                ? normalizedValue.map((date: Date) =>
×
123
                                      DateUtils.mixedDateToTimeString(date),
×
124
                                  )
×
125
                                : DateUtils.mixedDateToTimeString(
×
126
                                      normalizedValue,
×
127
                                  )
×
128
                            databaseValue = column.isArray
×
129
                                ? databaseValue.map((date: Date) =>
×
130
                                      DateUtils.mixedDateToTimeString(date),
×
131
                                  )
×
132
                                : DateUtils.mixedDateToTimeString(databaseValue)
×
133
                            break
×
134

1,562✔
135
                        case "datetime":
1,562!
136
                        case "datetime2":
1,562!
137
                        case Date:
1,562!
138
                        case "timestamp":
1,562!
139
                        case "timestamp without time zone":
1,562!
140
                        case "timestamp with time zone":
1,562!
141
                        case "timestamp with local time zone":
1,562!
142
                        case "timestamptz":
1,562✔
143
                            normalizedValue = column.isArray
103✔
144
                                ? normalizedValue.map((date: Date) =>
103!
145
                                      DateUtils.mixedDateToUtcDatetimeString(
×
146
                                          date,
×
147
                                      ),
×
148
                                  )
103✔
149
                                : DateUtils.mixedDateToUtcDatetimeString(
103✔
150
                                      normalizedValue,
103✔
151
                                  )
103✔
152

103✔
153
                            databaseValue = column.isArray
103✔
154
                                ? databaseValue.map((date: Date) =>
103!
155
                                      DateUtils.mixedDateToUtcDatetimeString(
×
156
                                          date,
×
157
                                      ),
×
158
                                  )
103✔
159
                                : DateUtils.mixedDateToUtcDatetimeString(
103✔
160
                                      databaseValue,
103✔
161
                                  )
103✔
162

103✔
163
                            break
103✔
164

1,562✔
165
                        case "json":
1,562!
166
                        case "jsonb":
1,562!
167
                            // JSON.stringify doesn't work because postgresql sorts jsonb before save.
×
168
                            // If you try to save json '[{"messages": "", "attribute Key": "", "level":""}] ' as jsonb,
×
169
                            // then postgresql will save it as '[{"level": "", "message":"", "attributeKey": ""}]'
×
170
                            if (
×
171
                                OrmUtils.deepCompare(
×
172
                                    normalizedValue,
×
173
                                    databaseValue,
×
174
                                )
×
175
                            )
×
176
                                return
×
177
                            break
×
178

1,562✔
179
                        case "simple-array":
1,562!
180
                            normalizedValue =
×
181
                                DateUtils.simpleArrayToString(normalizedValue)
×
182
                            databaseValue =
×
183
                                DateUtils.simpleArrayToString(databaseValue)
×
184
                            break
×
185
                        case "simple-enum":
1,562!
186
                            normalizedValue =
×
187
                                DateUtils.simpleEnumToString(normalizedValue)
×
188
                            databaseValue =
×
189
                                DateUtils.simpleEnumToString(databaseValue)
×
190
                            break
×
191
                        case "simple-json":
1,562!
192
                            normalizedValue =
×
193
                                DateUtils.simpleJsonToString(normalizedValue)
×
194
                            databaseValue =
×
195
                                DateUtils.simpleJsonToString(databaseValue)
×
196
                            break
×
197
                    }
1,562✔
198
                }
1,562✔
199

1,819✔
200
                // if value is not changed - then do nothing
1,819✔
201
                if (column.isArray) {
1,821!
202
                    if (OrmUtils.deepCompare(normalizedValue, databaseValue))
×
203
                        return
×
204
                } else if (
1,821✔
205
                    isUint8Array(normalizedValue) &&
1,819!
206
                    isUint8Array(databaseValue)
×
207
                ) {
1,819!
208
                    if (areUint8ArraysEqual(normalizedValue, databaseValue)) {
×
209
                        return
×
210
                    }
×
211
                } else {
1,819✔
212
                    if (normalizedValue === databaseValue) return
1,819✔
213
                }
1,819✔
214
            }
1,821✔
215

17,500✔
216
            if (!subject.diffColumns.includes(column))
17,500✔
217
                subject.diffColumns.push(column)
25,170✔
218

17,500✔
219
            subject.changeMaps.push({
17,500✔
220
                column: column,
17,500✔
221
                value: entityValue,
17,500✔
222
            })
17,500✔
223
        })
8,137✔
224
    }
8,137✔
225

28✔
226
    /**
28✔
227
     * Difference columns of the owning one-to-one and many-to-one columns.
28✔
228
     * @param allSubjects List of all subjects in the current operation.
28✔
229
     * @param subject Subject for which to compute differentiated relational columns.
28✔
230
     */
28✔
231
    protected computeDiffRelationalColumns(
28✔
232
        allSubjects: Subject[],
10,177✔
233
        subject: Subject,
10,177✔
234
    ): void {
10,177✔
235
        // if there is no persisted entity then nothing to compute changed in it
10,177✔
236
        if (!subject.entity) return
10,177✔
237

8,137✔
238
        subject.metadata.relationsWithJoinColumns.forEach((relation) => {
8,137✔
239
            if (relation.persistenceEnabled === false) return
2,707!
240

2,707✔
241
            // get the related entity from the persisted entity
2,707✔
242
            let relatedEntity = relation.getEntityValue(subject.entity!)
2,707✔
243

2,707✔
244
            // we don't perform operation over undefined properties (but we DO need null properties!)
2,707✔
245
            if (relatedEntity === undefined) return
2,707✔
246

1,070!
247
            // if there is no database entity then all relational columns are treated as new, e.g. changed
1,070✔
248
            if (subject.databaseEntity) {
2,282✔
249
                // here we cover two scenarios:
34✔
250
                // 1. related entity can be another entity which is natural way
34✔
251
                // 2. related entity can be just an entity id
34✔
252
                // if relation entity is just a relation id set (for example post.tag = 1)
34✔
253
                // then we create an id map from it to make a proper comparision
34✔
254
                let relatedEntityRelationIdMap: ObjectLiteral = relatedEntity
34✔
255
                if (
34✔
256
                    relatedEntityRelationIdMap !== null &&
34✔
257
                    ObjectUtils.isObject(relatedEntityRelationIdMap)
32✔
258
                )
34✔
259
                    relatedEntityRelationIdMap = relation.getRelationIdMap(
34✔
260
                        relatedEntityRelationIdMap,
31✔
261
                    )!
31✔
262

34✔
263
                // get database related entity. Since loadRelationIds are used on databaseEntity
34✔
264
                // related entity will contain only its relation ids
34✔
265
                const databaseRelatedEntityRelationIdMap =
34✔
266
                    relation.getEntityValue(subject.databaseEntity)
34✔
267

34✔
268
                // if relation ids are equal then we don't need to update anything
34✔
269
                const areRelatedIdsEqual = OrmUtils.compareIds(
34✔
270
                    relatedEntityRelationIdMap,
34✔
271
                    databaseRelatedEntityRelationIdMap,
34✔
272
                )
34✔
273
                if (areRelatedIdsEqual) {
34!
274
                    return
8✔
275
                } else {
34✔
276
                    subject.diffRelations.push(relation)
26✔
277
                }
26✔
278
            }
34✔
279

1,062✔
280
            // if there is an inserted subject for the related entity of the persisted entity then use it as related entity
1,062✔
281
            // this code is used for related entities without ids to be properly inserted (and then updated if needed)
1,062✔
282
            const valueSubject = allSubjects.find(
1,062✔
283
                (subject) =>
1,062✔
284
                    subject.mustBeInserted && subject.entity === relatedEntity,
1,062✔
285
            )
1,062✔
286
            if (valueSubject) relatedEntity = valueSubject
2,282✔
287

1,062✔
288
            // find if there is already a relation to be changed
1,062✔
289
            const changeMap = subject.changeMaps.find(
1,062✔
290
                (changeMap) => changeMap.relation === relation,
1,062✔
291
            )
1,062✔
292
            if (changeMap) {
2,282!
293
                // and update its value if it was found
5✔
294
                changeMap.value = relatedEntity
5✔
295
            } else {
2,282✔
296
                // if it wasn't found add a new relation for change
1,057✔
297
                subject.changeMaps.push({
1,057✔
298
                    relation: relation,
1,057✔
299
                    value: relatedEntity,
1,057✔
300
                })
1,057✔
301
            }
1,057✔
302
        })
8,137✔
303
    }
8,137✔
304
}
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