• 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

63.41
/src/persistence/Subject.ts
1
import { ObjectLiteral } from "../common/ObjectLiteral"
2
import { EntityMetadata } from "../metadata/EntityMetadata"
3
import { SubjectChangeMap } from "./SubjectChangeMap"
4
import { OrmUtils } from "../util/OrmUtils"
1✔
5
import { RelationMetadata } from "../metadata/RelationMetadata"
6
import { ColumnMetadata } from "../metadata/ColumnMetadata"
7
import { ObjectUtils } from "../util/ObjectUtils"
1✔
8
import { InstanceChecker } from "../util/InstanceChecker"
1✔
9

10
/**
11
 * Subject is a subject of persistence.
12
 * It holds information about each entity that needs to be persisted:
13
 * - what entity should be persisted
14
 * - what is database representation of the persisted entity
15
 * - what entity metadata of the persisted entity
16
 * - what is allowed to with persisted entity (insert/update/remove)
17
 *
18
 * Having this collection of subjects we can perform database queries.
19
 */
20
export class Subject {
1✔
21
    readonly "@instanceof" = Symbol.for("Subject")
271✔
22

23
    // -------------------------------------------------------------------------
24
    // Properties
25
    // -------------------------------------------------------------------------
26

27
    /**
28
     * Entity metadata of the subject entity.
29
     */
30
    metadata: EntityMetadata
31

32
    /**
33
     * Subject identifier.
34
     * This identifier is not limited to table entity primary columns.
35
     * This can be entity id or ids as well as some unique entity properties, like name or title.
36
     * Insert / Update / Remove operation will be executed by a given identifier.
37
     */
38
    identifier: ObjectLiteral | undefined = undefined
271✔
39

40
    /**
41
     * Copy of entity but with relational ids fulfilled.
42
     */
43
    entityWithFulfilledIds: ObjectLiteral | undefined = undefined
271✔
44

45
    /**
46
     * If subject was created by cascades this property will contain subject
47
     * from where this subject was created.
48
     */
49
    parentSubject?: Subject
50

51
    /**
52
     * Gets entity sent to the persistence (e.g. changed entity).
53
     * If entity is not set then this subject is created only for the entity loaded from the database,
54
     * or this subject is used for the junction operation (junction operations are relying only on identifier).
55
     */
56
    entity?: ObjectLiteral
57

58
    /**
59
     * Database entity.
60
     * THIS IS NOT RAW ENTITY DATA, its a real entity.
61
     */
62
    databaseEntity?: ObjectLiteral
63

64
    /**
65
     * Indicates if database entity was loaded.
66
     * No matter if it was found or not, it indicates the fact of loading.
67
     */
68
    databaseEntityLoaded: boolean = false
271✔
69

70
    /**
71
     * Changes needs to be applied in the database for the given subject.
72
     */
73
    changeMaps: SubjectChangeMap[] = []
271✔
74

75
    /**
76
     * Generated values returned by a database (for example generated id or default values).
77
     * Used in insert and update operations.
78
     * Has entity-like structure (not just column database name and values).
79
     */
80
    generatedMap?: ObjectLiteral
81

82
    /**
83
     * Inserted values with updated values of special and default columns.
84
     * Has entity-like structure (not just column database name and values).
85
     */
86
    insertedValueSet?: ObjectLiteral
87

88
    /**
89
     * Indicates if this subject can be inserted into the database.
90
     * This means that this subject either is newly persisted, either can be inserted by cascades.
91
     */
92
    canBeInserted: boolean = false
271✔
93

94
    /**
95
     * Indicates if this subject can be updated in the database.
96
     * This means that this subject either was persisted, either can be updated by cascades.
97
     */
98
    canBeUpdated: boolean = false
271✔
99

100
    /**
101
     * Indicates if this subject MUST be removed from the database.
102
     * This means that this subject either was removed, either was removed by cascades.
103
     */
104
    mustBeRemoved: boolean = false
271✔
105

106
    /**
107
     * Indicates if this subject can be soft-removed from the database.
108
     * This means that this subject either was soft-removed, either was soft-removed by cascades.
109
     */
110
    canBeSoftRemoved: boolean = false
271✔
111

112
    /**
113
     * Indicates if this subject can be recovered from the database.
114
     * This means that this subject either was recovered, either was recovered by cascades.
115
     */
116
    canBeRecovered: boolean = false
271✔
117

118
    /**
119
     * Relations updated by the change maps.
120
     */
121
    updatedRelationMaps: {
271✔
122
        relation: RelationMetadata
123
        value: ObjectLiteral
124
    }[] = []
125

126
    /**
127
     * List of updated columns
128
     */
129
    diffColumns: ColumnMetadata[] = []
271✔
130

131
    /**
132
     * List of updated relations
133
     */
134
    diffRelations: RelationMetadata[] = []
271✔
135

136
    // -------------------------------------------------------------------------
137
    // Constructor
138
    // -------------------------------------------------------------------------
139

140
    constructor(options: {
141
        metadata: EntityMetadata
142
        parentSubject?: Subject
143
        entity?: ObjectLiteral
144
        canBeInserted?: boolean
145
        canBeUpdated?: boolean
146
        mustBeRemoved?: boolean
147
        canBeSoftRemoved?: boolean
148
        canBeRecovered?: boolean
149
        identifier?: ObjectLiteral
150
        changeMaps?: SubjectChangeMap[]
151
    }) {
152
        this.metadata = options.metadata
271✔
153
        this.entity = options.entity
271✔
154
        this.parentSubject = options.parentSubject
271✔
155
        if (options.canBeInserted !== undefined)
271✔
156
            this.canBeInserted = options.canBeInserted
271✔
157
        if (options.canBeUpdated !== undefined)
271✔
158
            this.canBeUpdated = options.canBeUpdated
271✔
159
        if (options.mustBeRemoved !== undefined)
271✔
160
            this.mustBeRemoved = options.mustBeRemoved
271✔
161
        if (options.canBeSoftRemoved !== undefined)
271✔
162
            this.canBeSoftRemoved = options.canBeSoftRemoved
271✔
163
        if (options.canBeRecovered !== undefined)
271✔
164
            this.canBeRecovered = options.canBeRecovered
271✔
165
        if (options.identifier !== undefined)
271!
UNCOV
166
            this.identifier = options.identifier
×
167
        if (options.changeMaps !== undefined)
271!
168
            this.changeMaps.push(...options.changeMaps)
×
169

170
        this.recompute()
271✔
171
    }
172

173
    // -------------------------------------------------------------------------
174
    // Accessors
175
    // -------------------------------------------------------------------------
176

177
    /**
178
     * Checks if this subject must be inserted into the database.
179
     * Subject can be inserted into the database if it is allowed to be inserted (explicitly persisted or by cascades)
180
     * and if it does not have database entity set.
181
     */
182
    get mustBeInserted() {
183
        return this.canBeInserted && !this.databaseEntity
280✔
184
    }
185

186
    /**
187
     * Checks if this subject must be updated into the database.
188
     * Subject can be updated in the database if it is allowed to be updated (explicitly persisted or by cascades)
189
     * and if it does have differentiated columns or relations.
190
     */
191
    get mustBeUpdated() {
192
        return (
822✔
193
            this.canBeUpdated &&
2,295✔
194
            this.identifier &&
195
            (this.databaseEntityLoaded === false ||
196
                (this.databaseEntityLoaded && this.databaseEntity)) &&
197
            // ((this.entity && this.databaseEntity) || (!this.entity && !this.databaseEntity)) &&
198
            // ensure there are one or more changes for updatable columns
199
            this.changeMaps.some(
200
                (change) => !change.column || change.column.isUpdate,
30✔
201
            )
202
        )
203
    }
204

205
    /**
206
     * Checks if this subject must be soft-removed into the database.
207
     * Subject can be updated in the database if it is allowed to be soft-removed (explicitly persisted or by cascades)
208
     * and if it does have differentiated columns or relations.
209
     */
210
    get mustBeSoftRemoved() {
211
        return (
551✔
212
            this.canBeSoftRemoved &&
575✔
213
            this.identifier &&
214
            (this.databaseEntityLoaded === false ||
215
                (this.databaseEntityLoaded && this.databaseEntity))
216
        )
217
    }
218

219
    /**
220
     * Checks if this subject must be recovered into the database.
221
     * Subject can be updated in the database if it is allowed to be recovered (explicitly persisted or by cascades)
222
     * and if it does have differentiated columns or relations.
223
     */
224
    get mustBeRecovered() {
225
        return (
551✔
226
            this.canBeRecovered &&
551!
227
            this.identifier &&
228
            (this.databaseEntityLoaded === false ||
229
                (this.databaseEntityLoaded && this.databaseEntity))
230
        )
231
    }
232

233
    // -------------------------------------------------------------------------
234
    // Public Methods
235
    // -------------------------------------------------------------------------
236

237
    /**
238
     * Creates a value set needs to be inserted / updated in the database.
239
     * Value set is based on the entity and change maps of the subject.
240
     * Important note: this method pops data from this subject's change maps.
241
     */
242
    createValueSetAndPopChangeMap(): ObjectLiteral {
243
        const changeMapsWithoutValues: SubjectChangeMap[] = []
252✔
244
        const changeSet = this.changeMaps.reduce((updateMap, changeMap) => {
252✔
245
            let value = changeMap.value
551✔
246
            if (InstanceChecker.isSubject(value)) {
551!
247
                // referenced columns can refer on values both which were just inserted and which were present in the model
248
                // if entity was just inserted valueSets must contain all values from the entity and values just inserted in the database
249
                // so, here we check if we have a value set then we simply use it as value to get our reference column values
250
                // otherwise simply use an entity which cannot be just inserted at the moment and have all necessary data
UNCOV
251
                value = value.insertedValueSet
×
252
                    ? value.insertedValueSet
253
                    : value.entity
254
            }
255
            // value = changeMap.valueFactory ? changeMap.valueFactory(value) : changeMap.column.createValueMap(value);
256

257
            let valueMap: ObjectLiteral | undefined
258
            if (this.metadata.isJunction && changeMap.column) {
551!
UNCOV
259
                valueMap = changeMap.column.createValueMap(
×
260
                    changeMap.column.referencedColumn!.getEntityValue(value),
261
                )
262
            } else if (changeMap.column) {
551!
263
                valueMap = changeMap.column.createValueMap(value)
551✔
UNCOV
264
            } else if (changeMap.relation) {
×
265
                // value can be a related object, for example: post.question = { id: 1 }
266
                // or value can be a null or direct relation id, e.g. post.question = 1
267
                // if its a direction relation id then we just set it to the valueMap,
268
                // however if its an object then we need to extract its relation id map and set it to the valueMap
UNCOV
269
                if (ObjectUtils.isObject(value) && !Buffer.isBuffer(value)) {
×
270
                    // get relation id, e.g. referenced column name and its value,
271
                    // for example: { id: 1 } which then will be set to relation, e.g. post.category = { id: 1 }
272
                    const relationId =
UNCOV
273
                        changeMap.relation!.getRelationIdMap(value)
×
274

275
                    // but relation id can be empty, for example in the case when you insert a new post with category
276
                    // and both post and category are newly inserted objects (by cascades) and in this case category will not have id
277
                    // this means we need to insert post without question id and update post's questionId once question be inserted
278
                    // that's why we create a new changeMap operation for future updation of the post entity
UNCOV
279
                    if (relationId === undefined) {
×
UNCOV
280
                        changeMapsWithoutValues.push(changeMap)
×
UNCOV
281
                        this.canBeUpdated = true
×
UNCOV
282
                        return updateMap
×
283
                    }
UNCOV
284
                    valueMap = changeMap.relation!.createValueMap(relationId)
×
UNCOV
285
                    this.updatedRelationMaps.push({
×
286
                        relation: changeMap.relation,
287
                        value: relationId,
288
                    })
289
                } else {
290
                    // value can be "null" or direct relation id here
UNCOV
291
                    valueMap = changeMap.relation!.createValueMap(value)
×
UNCOV
292
                    this.updatedRelationMaps.push({
×
293
                        relation: changeMap.relation,
294
                        value: value,
295
                    })
296
                }
297
            }
298

299
            OrmUtils.mergeDeep(updateMap, valueMap)
551✔
300
            return updateMap
551✔
301
        }, {} as ObjectLiteral)
302
        this.changeMaps = changeMapsWithoutValues
252✔
303
        return changeSet
252✔
304
    }
305

306
    /**
307
     * Recomputes entityWithFulfilledIds and identifier when entity changes.
308
     */
309
    recompute(): void {
310
        if (this.entity) {
280!
311
            this.entityWithFulfilledIds = Object.assign({}, this.entity)
280✔
312
            if (this.parentSubject) {
280!
UNCOV
313
                this.metadata.primaryColumns.forEach((primaryColumn) => {
×
UNCOV
314
                    if (
×
315
                        primaryColumn.relationMetadata &&
×
316
                        primaryColumn.relationMetadata.inverseEntityMetadata ===
317
                            this.parentSubject!.metadata
318
                    ) {
319
                        const value =
UNCOV
320
                            primaryColumn.referencedColumn!.getEntityValue(
×
321
                                this.parentSubject!.entity!,
322
                            )
UNCOV
323
                        primaryColumn.setEntityValue(
×
324
                            this.entityWithFulfilledIds!,
325
                            value,
326
                        )
327
                    }
328
                })
329
            }
330
            this.identifier = this.metadata.getEntityIdMap(
280✔
331
                this.entityWithFulfilledIds,
332
            )
UNCOV
333
        } else if (this.databaseEntity) {
×
334
            this.identifier = this.metadata.getEntityIdMap(this.databaseEntity)
×
335
        }
336
    }
337
}
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