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

typeorm / typeorm / 19549987525

20 Nov 2025 08:11PM UTC coverage: 80.769% (+4.3%) from 76.433%
19549987525

push

github

web-flow
ci: run tests on commits to master and next (#11783)

Co-authored-by: Oleg "OSA413" Sokolov <OSA413@users.noreply.github.com>

26500 of 32174 branches covered (82.36%)

Branch coverage included in aggregate %.

91252 of 113615 relevant lines covered (80.32%)

88980.79 hits per line

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

80.94
/src/persistence/SubjectChangedColumnsComputer.ts
1
import { ObjectLiteral } from "../common/ObjectLiteral"
26✔
2
import { ApplyValueTransformers } from "../util/ApplyValueTransformers"
26✔
3
import { DateUtils } from "../util/DateUtils"
26✔
4
import { ObjectUtils } from "../util/ObjectUtils"
26✔
5
import { OrmUtils } from "../util/OrmUtils"
26✔
6
import { Subject } from "./Subject"
26✔
7

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

26✔
16
    /**
26✔
17
     * Finds what columns are changed in the subject entities.
26✔
18
     */
26✔
19
    compute(subjects: Subject[]) {
26✔
20
        subjects.forEach((subject) => {
182,592✔
21
            this.computeDiffColumns(subject)
503,803✔
22
            this.computeDiffRelationalColumns(subjects, subject)
503,803✔
23
        })
182,592✔
24
    }
182,592✔
25

26✔
26
    // -------------------------------------------------------------------------
26✔
27
    // Protected Methods
26✔
28
    // -------------------------------------------------------------------------
26✔
29

26✔
30
    /**
26✔
31
     * Differentiate columns from the updated entity and entity stored in the database.
26✔
32
     */
26✔
33
    protected computeDiffColumns(subject: Subject): void {
26✔
34
        // if there is no persisted entity then nothing to compute changed in it
503,803✔
35
        if (!subject.entity) return
503,803✔
36

442,803✔
37
        subject.metadata.columns.forEach((column) => {
442,803✔
38
            // ignore special columns
1,605,507✔
39
            if (
1,605,507✔
40
                column.isVirtual ||
1,605,507✔
41
                column.isDiscriminator // ||
1,605,507✔
42
                // column.isUpdateDate ||
1,605,507✔
43
                // column.isVersion ||
1,605,507✔
44
                // column.isCreateDate
1,605,507✔
45
            )
1,605,507✔
46
                return
1,605,507!
47

1,516,329✔
48
            const changeMap = subject.changeMaps.find(
1,516,329✔
49
                (changeMap) => changeMap.column === column,
1,516,329✔
50
            )
1,516,329✔
51
            if (changeMap) {
1,605,507✔
52
                subject.changeMaps.splice(
1,246✔
53
                    subject.changeMaps.indexOf(changeMap),
1,246✔
54
                    1,
1,246✔
55
                )
1,246✔
56
            }
1,246✔
57

1,516,329✔
58
            // get user provided value - column value from the user provided persisted entity
1,516,329✔
59
            const entityValue = column.getEntityValue(subject.entity!)
1,516,329✔
60

1,516,329✔
61
            // we don't perform operation over undefined properties (but we DO need null properties!)
1,516,329✔
62
            if (entityValue === undefined) return
1,516,329✔
63

1,243,820✔
64
            // if there is no database entity then all columns are treated as new, e.g. changed
1,243,820✔
65
            if (subject.databaseEntity) {
1,605,507✔
66
                // skip transform database value for json / jsonb for comparison later on
27,605✔
67
                const shouldTransformDatabaseEntity =
27,605✔
68
                    column.type !== "json" && column.type !== "jsonb"
27,605✔
69

27,605✔
70
                // get database value of the column
27,605✔
71
                let databaseValue = column.getEntityValue(
27,605✔
72
                    subject.databaseEntity,
27,605✔
73
                    shouldTransformDatabaseEntity,
27,605✔
74
                )
27,605✔
75

27,605✔
76
                // filter out "relational columns" only in the case if there is a relation object in entity
27,605✔
77
                if (column.relationMetadata) {
27,605!
78
                    const value = column.relationMetadata.getEntityValue(
474✔
79
                        subject.entity!,
474✔
80
                    )
474✔
81
                    if (value !== null && value !== undefined) return
474✔
82
                }
474✔
83
                let normalizedValue = entityValue
27,535✔
84
                // if both values are not null, normalize special values to make proper comparision
27,535✔
85
                if (entityValue !== null && databaseValue !== null) {
27,605✔
86
                    switch (column.type) {
26,158✔
87
                        case "date":
26,158!
88
                            normalizedValue = column.isArray
18✔
89
                                ? entityValue.map((date: Date) =>
18!
90
                                      DateUtils.mixedDateToDateString(date),
12✔
91
                                  )
18✔
92
                                : DateUtils.mixedDateToDateString(entityValue)
18!
93
                            databaseValue = column.isArray
18✔
94
                                ? databaseValue.map((date: Date) =>
18!
95
                                      DateUtils.mixedDateToDateString(date),
12✔
96
                                  )
18✔
97
                                : DateUtils.mixedDateToDateString(databaseValue)
18!
98
                            break
18✔
99

26,158✔
100
                        case "time":
26,158!
101
                        case "time with time zone":
26,158!
102
                        case "time without time zone":
26,158!
103
                        case "timetz":
26,158!
104
                            normalizedValue = column.isArray
×
105
                                ? entityValue.map((date: Date) =>
×
106
                                      DateUtils.mixedDateToTimeString(date),
×
107
                                  )
×
108
                                : DateUtils.mixedDateToTimeString(entityValue)
×
109
                            databaseValue = column.isArray
×
110
                                ? databaseValue.map((date: Date) =>
×
111
                                      DateUtils.mixedDateToTimeString(date),
×
112
                                  )
×
113
                                : DateUtils.mixedDateToTimeString(databaseValue)
×
114
                            break
×
115

26,158✔
116
                        case "datetime":
26,158!
117
                        case "datetime2":
26,158!
118
                        case Date:
26,158!
119
                        case "timestamp":
26,158!
120
                        case "timestamp without time zone":
26,158!
121
                        case "timestamp with time zone":
26,158!
122
                        case "timestamp with local time zone":
26,158!
123
                        case "timestamptz":
26,158!
124
                            normalizedValue = column.isArray
811✔
125
                                ? entityValue.map((date: Date) =>
811!
126
                                      DateUtils.mixedDateToUtcDatetimeString(
×
127
                                          date,
×
128
                                      ),
×
129
                                  )
811✔
130
                                : DateUtils.mixedDateToUtcDatetimeString(
811✔
131
                                      entityValue,
811✔
132
                                  )
811✔
133

811✔
134
                            databaseValue = column.isArray
811✔
135
                                ? databaseValue.map((date: Date) =>
811!
136
                                      DateUtils.mixedDateToUtcDatetimeString(
×
137
                                          date,
×
138
                                      ),
×
139
                                  )
811✔
140
                                : DateUtils.mixedDateToUtcDatetimeString(
811✔
141
                                      databaseValue,
811✔
142
                                  )
811✔
143

811✔
144
                            break
811✔
145

26,158✔
146
                        case "json":
26,158!
147
                        case "jsonb":
26,158!
148
                            // JSON.stringify doesn't work because postgresql sorts jsonb before save.
16✔
149
                            // If you try to save json '[{"messages": "", "attribute Key": "", "level":""}] ' as jsonb,
16✔
150
                            // then postgresql will save it as '[{"level": "", "message":"", "attributeKey": ""}]'
16✔
151
                            if (
16✔
152
                                OrmUtils.deepCompare(entityValue, databaseValue)
16✔
153
                            )
16✔
154
                                return
16✔
155
                            break
4✔
156

26,158✔
157
                        case "simple-array":
26,158!
158
                            normalizedValue =
×
159
                                DateUtils.simpleArrayToString(entityValue)
×
160
                            databaseValue =
×
161
                                DateUtils.simpleArrayToString(databaseValue)
×
162
                            break
×
163
                        case "simple-enum":
26,158!
164
                            normalizedValue =
×
165
                                DateUtils.simpleEnumToString(entityValue)
×
166
                            databaseValue =
×
167
                                DateUtils.simpleEnumToString(databaseValue)
×
168
                            break
×
169
                        case "simple-json":
26,158!
170
                            normalizedValue =
×
171
                                DateUtils.simpleJsonToString(entityValue)
×
172
                            databaseValue =
×
173
                                DateUtils.simpleJsonToString(databaseValue)
×
174
                            break
×
175
                    }
26,158✔
176

26,146✔
177
                    if (column.transformer) {
26,158!
178
                        normalizedValue = ApplyValueTransformers.transformTo(
228✔
179
                            column.transformer,
228✔
180
                            entityValue,
228✔
181
                        )
228✔
182
                    }
228✔
183
                }
26,158✔
184

27,523✔
185
                // if value is not changed - then do nothing
27,523✔
186
                if (column.isArray) {
27,605!
187
                    if (OrmUtils.deepCompare(normalizedValue, databaseValue))
36✔
188
                        return
36✔
189
                } else if (
27,605✔
190
                    Buffer.isBuffer(normalizedValue) &&
27,487!
191
                    Buffer.isBuffer(databaseValue)
8✔
192
                ) {
27,487!
193
                    if (normalizedValue.equals(databaseValue)) {
8✔
194
                        return
8✔
195
                    }
8✔
196
                } else {
27,487✔
197
                    if (normalizedValue === databaseValue) return
27,479✔
198
                }
27,479✔
199
            }
27,605✔
200

1,220,545✔
201
            if (!subject.diffColumns.includes(column))
1,220,545✔
202
                subject.diffColumns.push(column)
1,605,507✔
203

1,220,545✔
204
            subject.changeMaps.push({
1,220,545✔
205
                column: column,
1,220,545✔
206
                value: entityValue,
1,220,545✔
207
            })
1,220,545✔
208
        })
442,803✔
209
    }
442,803✔
210

26✔
211
    /**
26✔
212
     * Difference columns of the owning one-to-one and many-to-one columns.
26✔
213
     */
26✔
214
    protected computeDiffRelationalColumns(
26✔
215
        allSubjects: Subject[],
503,803✔
216
        subject: Subject,
503,803✔
217
    ): void {
503,803✔
218
        // if there is no persisted entity then nothing to compute changed in it
503,803✔
219
        if (!subject.entity) return
503,803!
220

442,803✔
221
        subject.metadata.relationsWithJoinColumns.forEach((relation) => {
442,803✔
222
            // get the related entity from the persisted entity
72,649✔
223
            let relatedEntity = relation.getEntityValue(subject.entity!)
72,649✔
224

72,649✔
225
            // we don't perform operation over undefined properties (but we DO need null properties!)
72,649✔
226
            if (relatedEntity === undefined) return
72,649✔
227

23,828✔
228
            // if there is no database entity then all relational columns are treated as new, e.g. changed
23,828✔
229
            if (subject.databaseEntity) {
72,649✔
230
                // here we cover two scenarios:
1,575✔
231
                // 1. related entity can be another entity which is natural way
1,575✔
232
                // 2. related entity can be just an entity id
1,575✔
233
                // if relation entity is just a relation id set (for example post.tag = 1)
1,575✔
234
                // then we create an id map from it to make a proper comparision
1,575✔
235
                let relatedEntityRelationIdMap: ObjectLiteral = relatedEntity
1,575✔
236
                if (
1,575✔
237
                    relatedEntityRelationIdMap !== null &&
1,575✔
238
                    ObjectUtils.isObject(relatedEntityRelationIdMap)
1,403✔
239
                )
1,575✔
240
                    relatedEntityRelationIdMap = relation.getRelationIdMap(
1,575✔
241
                        relatedEntityRelationIdMap,
1,347✔
242
                    )!
1,347✔
243

1,575✔
244
                // get database related entity. Since loadRelationIds are used on databaseEntity
1,575✔
245
                // related entity will contain only its relation ids
1,575✔
246
                const databaseRelatedEntityRelationIdMap =
1,575✔
247
                    relation.getEntityValue(subject.databaseEntity)
1,575✔
248

1,575✔
249
                // if relation ids are equal then we don't need to update anything
1,575✔
250
                const areRelatedIdsEqual = OrmUtils.compareIds(
1,575✔
251
                    relatedEntityRelationIdMap,
1,575✔
252
                    databaseRelatedEntityRelationIdMap,
1,575✔
253
                )
1,575✔
254
                if (areRelatedIdsEqual) {
1,575✔
255
                    return
415✔
256
                } else {
1,575✔
257
                    subject.diffRelations.push(relation)
1,160✔
258
                }
1,160✔
259
            }
1,575✔
260

23,413✔
261
            // if there is an inserted subject for the related entity of the persisted entity then use it as related entity
23,413✔
262
            // this code is used for related entities without ids to be properly inserted (and then updated if needed)
23,413✔
263
            const valueSubject = allSubjects.find(
23,413✔
264
                (subject) =>
23,413✔
265
                    subject.mustBeInserted && subject.entity === relatedEntity,
23,413✔
266
            )
23,413✔
267
            if (valueSubject) relatedEntity = valueSubject
72,649✔
268

23,413✔
269
            // find if there is already a relation to be changed
23,413✔
270
            const changeMap = subject.changeMaps.find(
23,413✔
271
                (changeMap) => changeMap.relation === relation,
23,413✔
272
            )
23,413✔
273
            if (changeMap) {
72,649✔
274
                // and update its value if it was found
168✔
275
                changeMap.value = relatedEntity
168✔
276
            } else {
72,649✔
277
                // if it wasn't found add a new relation for change
23,245✔
278
                subject.changeMaps.push({
23,245✔
279
                    relation: relation,
23,245✔
280
                    value: relatedEntity,
23,245✔
281
                })
23,245✔
282
            }
23,245✔
283
        })
442,803✔
284
    }
442,803✔
285
}
26✔
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