• 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

10.29
/src/persistence/subject-builder/OneToManySubjectBuilder.ts
1
import { Subject } from "../Subject"
1✔
2
import { OrmUtils } from "../../util/OrmUtils"
1✔
3
import { ObjectLiteral } from "../../common/ObjectLiteral"
4
import { EntityMetadata } from "../../metadata/EntityMetadata"
1✔
5
import { RelationMetadata } from "../../metadata/RelationMetadata"
6

7
/**
8
 * Builds operations needs to be executed for one-to-many relations of the given subjects.
9
 *
10
 * by example: post contains one-to-many relation with category in the property called "categories", e.g.
11
 *             @OneToMany(type => Category, category => category.post) categories: Category[]
12
 *             If user adds categories into the post and saves post we need to bind them.
13
 *             This operation requires updation of category table since its owner of the relation and contains a join column.
14
 *
15
 * note: this class shares lot of things with OneToOneInverseSideOperationBuilder, so when you change this class
16
 *       make sure to reflect changes there as well.
17
 */
18
export class OneToManySubjectBuilder {
1✔
19
    // ---------------------------------------------------------------------
20
    // Constructor
21
    // ---------------------------------------------------------------------
22

23
    constructor(protected subjects: Subject[]) {}
107✔
24

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

29
    /**
30
     * Builds all required operations.
31
     */
32
    build(): void {
33
        this.subjects.forEach((subject) => {
107✔
34
            subject.metadata.oneToManyRelations.forEach((relation) => {
267✔
35
                // skip relations for which persistence is disabled
UNCOV
36
                if (relation.persistenceEnabled === false) return
×
37

UNCOV
38
                this.buildForSubjectRelation(subject, relation)
×
39
            })
40
        })
41
    }
42

43
    // ---------------------------------------------------------------------
44
    // Protected Methods
45
    // ---------------------------------------------------------------------
46

47
    /**
48
     * Builds operations for a given subject and relation.
49
     *
50
     * by example: subject is "post" entity we are saving here and relation is "categories" inside it here.
51
     */
52
    protected buildForSubjectRelation(
53
        subject: Subject,
54
        relation: RelationMetadata,
55
    ) {
56
        // prepare objects (relation id maps) for the database entity
57
        // by example: since subject is a post, we are expecting to get all post's categories saved in the database here,
58
        //             particularly their relation ids, e.g. category ids stored in the database
59

60
        // in most cases relatedEntityDatabaseValues will contain only the entity key properties.
61
        // this is because subject.databaseEntity contains relations with loaded relation ids only.
62
        // however if the entity uses the afterLoad hook to calculate any properties, the fetched "key object" might include ADDITIONAL properties.
63
        // to handle such situations, we pass the data to relation.inverseEntityMetadata.getEntityIdMap to extract the key without any other properties.
64

UNCOV
65
        let relatedEntityDatabaseRelationIds: ObjectLiteral[] = []
×
UNCOV
66
        if (subject.databaseEntity) {
×
67
            // related entities in the database can exist only if this entity (post) is saved
68
            const relatedEntityDatabaseRelation: ObjectLiteral[] | undefined =
UNCOV
69
                relation.getEntityValue(subject.databaseEntity)
×
UNCOV
70
            if (relatedEntityDatabaseRelation) {
×
UNCOV
71
                relatedEntityDatabaseRelationIds =
×
72
                    relatedEntityDatabaseRelation.map(
73
                        (entity) =>
UNCOV
74
                            relation.inverseEntityMetadata.getEntityIdMap(
×
75
                                entity,
76
                            )!,
77
                    )
78
            }
79
        }
80

81
        // get related entities of persisted entity
82
        // by example: get categories from the passed to persist post entity
UNCOV
83
        let relatedEntities: ObjectLiteral[] = relation.getEntityValue(
×
84
            subject.entity!,
85
        )
UNCOV
86
        if (relatedEntities === null)
×
87
            // we treat relations set to null as removed, so we don't skip it
UNCOV
88
            relatedEntities = [] as ObjectLiteral[]
×
UNCOV
89
        if (relatedEntities === undefined)
×
90
            // if relation is undefined then nothing to update
UNCOV
91
            return
×
92

93
        // extract only relation ids from the related entities, since we only need them for comparison
94
        // by example: extract from categories only relation ids (category id, or let's say category title, depend on join column options)
UNCOV
95
        const relatedPersistedEntityRelationIds: ObjectLiteral[] = []
×
UNCOV
96
        relatedEntities.forEach((relatedEntity) => {
×
97
            // by example: relatedEntity is a category here
98
            let relationIdMap =
UNCOV
99
                relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity) // by example: relationIdMap is category.id map here, e.g. { id: ... }
×
100

101
            // try to find a subject of this related entity, maybe it was loaded or was marked for persistence
UNCOV
102
            let relatedEntitySubject = this.subjects.find((subject) => {
×
UNCOV
103
                return subject.entity === relatedEntity
×
104
            })
105

106
            // if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
UNCOV
107
            if (relatedEntitySubject)
×
UNCOV
108
                relationIdMap = relatedEntitySubject.identifier
×
109

110
            // if relationIdMap is undefined then it means user binds object which is not saved in the database yet
111
            // by example: if post contains categories which does not have ids yet (because they are new)
112
            //             it means they are always newly inserted and relation update operation always must be created for them
113
            //             it does not make sense to perform difference operation for them for both add and remove actions
UNCOV
114
            if (!relationIdMap) {
×
115
                // we decided to remove this error because it brings complications when saving object with non-saved entities
116
                // if (!relatedEntitySubject)
117
                //     throw new TypeORMError(`One-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
118
                //         `entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
119
                //         `Please setup cascade insertion or save entities before binding it.`);
UNCOV
120
                if (!relatedEntitySubject) return
×
121

122
                // okay, so related subject exist and its marked for insertion, then add a new change map
123
                // by example: this will tell category to insert into its post relation our post we are working with
124
                //             relatedEntitySubject is newly inserted CategorySubject
125
                //             relation.inverseRelation is ManyToOne relation inside Category
126
                //             subject is Post needs to be inserted into Category
UNCOV
127
                relatedEntitySubject.changeMaps.push({
×
128
                    relation: relation.inverseRelation!,
129
                    value: subject,
130
                })
131

UNCOV
132
                return
×
133
            }
134

135
            // check if this binding really exist in the database
136
            // by example: find our category if its already bind in the database
137
            const relationIdInDatabaseSubjectRelation =
UNCOV
138
                relatedEntityDatabaseRelationIds.find(
×
139
                    (relatedDatabaseEntityRelationId) => {
UNCOV
140
                        return OrmUtils.compareIds(
×
141
                            relationIdMap,
142
                            relatedDatabaseEntityRelationId,
143
                        )
144
                    },
145
                )
146

147
            // if relationIdMap DOES NOT exist in the subject's relation in the database it means its a new relation and we need to "bind" them
148
            // by example: this will tell category to insert into its post relation our post we are working with
149
            //             relatedEntitySubject is newly inserted CategorySubject
150
            //             relation.inverseRelation is ManyToOne relation inside Category
151
            //             subject is Post needs to be inserted into Category
UNCOV
152
            if (!relationIdInDatabaseSubjectRelation) {
×
153
                // if there is no relatedEntitySubject then it means "category" wasn't persisted,
154
                // but since we are going to update "category" table (since its an owning side of relation with join column)
155
                // we create a new subject here:
UNCOV
156
                if (!relatedEntitySubject) {
×
UNCOV
157
                    relatedEntitySubject = new Subject({
×
158
                        metadata: relation.inverseEntityMetadata,
159
                        parentSubject: subject,
160
                        canBeUpdated: true,
161
                        identifier: relationIdMap,
162
                    })
UNCOV
163
                    this.subjects.push(relatedEntitySubject)
×
164
                }
165

UNCOV
166
                relatedEntitySubject.changeMaps.push({
×
167
                    relation: relation.inverseRelation!,
168
                    value: subject,
169
                })
170
            }
171

172
            // if related entity has relation id then we add it to the list of relation ids
173
            // this list will be used later to compare with database relation ids to find a difference
174
            // what exist in this array and does not exist in the database are newly inserted relations
175
            // what does not exist in this array, but exist in the database are removed relations
176
            // removed relations are set to null from inverse side of relation
UNCOV
177
            relatedPersistedEntityRelationIds.push(relationIdMap)
×
178
        })
179

180
        // find what related entities were added and what were removed based on difference between what we save and what database has
UNCOV
181
        if (relation.inverseRelation?.orphanedRowAction !== "disable") {
×
UNCOV
182
            EntityMetadata.difference(
×
183
                relatedEntityDatabaseRelationIds,
184
                relatedPersistedEntityRelationIds,
185
            ).forEach((removedRelatedEntityRelationId) => {
186
                // by example: removedRelatedEntityRelationId is category that was bind in the database before, but now its unbind
187

188
                // todo: probably we can improve this in the future by finding entity with column those values,
189
                // todo: maybe it was already in persistence process. This is possible due to unique requirements of join columns
190
                // we create a new subject which operations will be executed in subject operation executor
UNCOV
191
                const removedRelatedEntitySubject = new Subject({
×
192
                    metadata: relation.inverseEntityMetadata,
193
                    parentSubject: subject,
194
                    identifier: removedRelatedEntityRelationId,
195
                })
196

UNCOV
197
                if (
×
198
                    !relation.inverseRelation ||
×
199
                    relation.inverseRelation.orphanedRowAction === "nullify"
200
                ) {
UNCOV
201
                    removedRelatedEntitySubject.canBeUpdated = true
×
UNCOV
202
                    removedRelatedEntitySubject.changeMaps = [
×
203
                        {
204
                            relation: relation.inverseRelation!,
205
                            value: null,
206
                        },
207
                    ]
UNCOV
208
                } else if (
×
209
                    relation.inverseRelation.orphanedRowAction === "delete"
210
                ) {
UNCOV
211
                    removedRelatedEntitySubject.mustBeRemoved = true
×
UNCOV
212
                } else if (
×
213
                    relation.inverseRelation.orphanedRowAction === "soft-delete"
214
                ) {
UNCOV
215
                    removedRelatedEntitySubject.canBeSoftRemoved = true
×
216
                }
217

UNCOV
218
                this.subjects.push(removedRelatedEntitySubject)
×
219
            })
220
        }
221
    }
222
}
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