• 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

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

6
/**
7
 * Builds operations needs to be executed for many-to-many relations of the given subjects.
8
 *
9
 * by example: post contains owner many-to-many relation with categories in the property called "categories", e.g.
10
 *             @ManyToMany(type => Category, category => category.posts) categories: Category[]
11
 *             If user adds categories into the post and saves post we need to bind them.
12
 *             This operation requires updation of junction table.
13
 */
14
export class ManyToManySubjectBuilder {
1✔
15
    // ---------------------------------------------------------------------
16
    // Constructor
17
    // ---------------------------------------------------------------------
18

19
    constructor(protected subjects: Subject[]) {}
111✔
20

21
    // ---------------------------------------------------------------------
22
    // Public Methods
23
    // ---------------------------------------------------------------------
24

25
    /**
26
     * Builds operations for any changes in the many-to-many relations of the subjects.
27
     */
28
    build(): void {
29
        this.subjects.forEach((subject) => {
107✔
30
            // if subject doesn't have entity then no need to find something that should be inserted or removed
31
            if (!subject.entity) return
267!
32

33
            // go through all persistence enabled many-to-many relations and build subject operations for them
34
            subject.metadata.manyToManyRelations.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
     * Builds operations for removal of all many-to-many records of all many-to-many relations of the given subject.
45
     */
46
    buildForAllRemoval(subject: Subject) {
47
        // if subject does not have a database entity then it means it does not exist in the database
48
        // if it does not exist in the database then we don't have anything for deletion
49
        if (!subject.databaseEntity) return
4!
50

51
        // go through all persistence enabled many-to-many relations and build subject operations for them
52
        subject.metadata.manyToManyRelations.forEach((relation) => {
4✔
53
            // skip relations for which persistence is disabled
UNCOV
54
            if (relation.persistenceEnabled === false) return
×
55

56
            // get all related entities (actually related entity relation ids) bind to this subject entity
57
            // by example: returns category ids of the post we are currently working with (subject.entity is post)
58
            const relatedEntityRelationIdsInDatabase: ObjectLiteral[] =
UNCOV
59
                relation.getEntityValue(subject.databaseEntity!)
×
60

61
            // go through all related entities and create a new junction subject for each row in junction table
UNCOV
62
            relatedEntityRelationIdsInDatabase.forEach((relationId) => {
×
UNCOV
63
                const junctionSubject = new Subject({
×
64
                    metadata: relation.junctionEntityMetadata!,
65
                    parentSubject: subject,
66
                    mustBeRemoved: true,
67
                    identifier: this.buildJunctionIdentifier(
68
                        subject,
69
                        relation,
70
                        relationId,
71
                    ),
72
                })
73

74
                // we use unshift because we need to perform those operations before post deletion is performed
75
                // but post deletion was already added as an subject
76
                // this is temporary solution, later we need to implement proper sorting of subjects before their removal
UNCOV
77
                this.subjects.push(junctionSubject)
×
78
            })
79
        })
80
    }
81

82
    // ---------------------------------------------------------------------
83
    // Protected Methods
84
    // ---------------------------------------------------------------------
85

86
    /**
87
     * Builds operations for a given subject and relation.
88
     *
89
     * by example: subject is "post" entity we are saving here and relation is "categories" inside it here.
90
     */
91
    protected buildForSubjectRelation(
92
        subject: Subject,
93
        relation: RelationMetadata,
94
    ) {
95
        // load from db all relation ids of inverse entities that are "bind" to the subject's entity
96
        // this way we gonna check which relation ids are missing and which are new (e.g. inserted or removed)
UNCOV
97
        let databaseRelatedEntityIds: ObjectLiteral[] = []
×
98

99
        // if subject don't have database entity it means all related entities in persisted subject are new and must be bind
100
        // and we don't need to remove something that is not exist
UNCOV
101
        if (subject.databaseEntity) {
×
UNCOV
102
            const databaseRelatedEntityValue = relation.getEntityValue(
×
103
                subject.databaseEntity,
104
            )
UNCOV
105
            if (databaseRelatedEntityValue) {
×
UNCOV
106
                databaseRelatedEntityIds = databaseRelatedEntityValue.map(
×
107
                    (e: any) =>
UNCOV
108
                        relation.inverseEntityMetadata.getEntityIdMap(e),
×
109
                )
110
            }
111
        }
112

113
        // extract entity's relation value
114
        // by example: categories inside our post (subject.entity is post)
UNCOV
115
        let relatedEntities: ObjectLiteral[] = relation.getEntityValue(
×
116
            subject.entity!,
117
        )
UNCOV
118
        if (relatedEntities === null)
×
119
            // if value set to null its equal if we set it to empty array - all items must be removed from the database
UNCOV
120
            relatedEntities = []
×
UNCOV
121
        if (!Array.isArray(relatedEntities)) return
×
122

123
        // from all related entities find only those which aren't found in the db - for them we will create operation subjects
UNCOV
124
        relatedEntities.forEach((relatedEntity) => {
×
125
            // by example: relatedEntity is category from categories saved with post
126

127
            // todo: check how it will work for entities which are saved by cascades, but aren't saved in the database yet
128

129
            // extract only relation id from the related entities, since we only need it for comparison
130
            // by example: extract from category only relation id (category id, or let's say category title, depend on join column options)
131
            let relatedEntityRelationIdMap =
UNCOV
132
                relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity)
×
133

134
            // try to find a subject of this related entity, maybe it was loaded or was marked for persistence
UNCOV
135
            const relatedEntitySubject = this.subjects.find((subject) => {
×
UNCOV
136
                return subject.entity === relatedEntity
×
137
            })
138

139
            // if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
UNCOV
140
            if (relatedEntitySubject)
×
UNCOV
141
                relatedEntityRelationIdMap = relatedEntitySubject.identifier
×
142

143
            // if related entity relation id map is empty it means related entity is newly persisted
UNCOV
144
            if (!relatedEntityRelationIdMap) {
×
145
                // we decided to remove this error because it brings complications when saving object with non-saved entities
146
                // if related entity does not have a subject then it means user tries to bind entity which wasn't saved
147
                // in this persistence because he didn't pass this entity for save or he did not set cascades
148
                // but without entity being inserted we cannot bind it in the relation operation, so we throw an exception here
149
                // we decided to remove this error because it brings complications when saving object with non-saved entities
150
                // if (!relatedEntitySubject)
151
                //     throw new TypeORMError(`Many-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
152
                //         `entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
153
                //         `Please setup cascade insertion or save entities before binding it.`);
UNCOV
154
                if (!relatedEntitySubject) return
×
155
            }
156

157
            // try to find related entity in the database
158
            // by example: find post's category in the database post's categories
UNCOV
159
            const relatedEntityExistInDatabase = databaseRelatedEntityIds.find(
×
160
                (databaseRelatedEntityRelationId) => {
UNCOV
161
                    return OrmUtils.compareIds(
×
162
                        databaseRelatedEntityRelationId,
163
                        relatedEntityRelationIdMap,
164
                    )
165
                },
166
            )
167

168
            // if entity is found then don't do anything - it means binding in junction table already exist, we don't need to add anything
UNCOV
169
            if (relatedEntityExistInDatabase) return
×
170

UNCOV
171
            const ownerValue = relation.isOwning
×
172
                ? subject
173
                : relatedEntitySubject || relatedEntity // by example: ownerEntityMap is post from subject here
×
UNCOV
174
            const inverseValue = relation.isOwning
×
175
                ? relatedEntitySubject || relatedEntity
×
176
                : subject // by example: inverseEntityMap is category from categories array here
177

178
            // create a new subject for insert operation of junction rows
UNCOV
179
            const junctionSubject = new Subject({
×
180
                metadata: relation.junctionEntityMetadata!,
181
                parentSubject: subject,
182
                canBeInserted: true,
183
            })
UNCOV
184
            this.subjects.push(junctionSubject)
×
185

UNCOV
186
            relation.junctionEntityMetadata!.ownerColumns.forEach((column) => {
×
UNCOV
187
                junctionSubject.changeMaps.push({
×
188
                    column: column,
189
                    value: ownerValue,
190
                    // valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(ownerEntityMap),
191
                })
192
            })
193

UNCOV
194
            relation.junctionEntityMetadata!.inverseColumns.forEach(
×
195
                (column) => {
UNCOV
196
                    junctionSubject.changeMaps.push({
×
197
                        column: column,
198
                        value: inverseValue,
199
                        // valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(inverseEntityMap),
200
                    })
201
                },
202
            )
203
        })
204

205
        // get all inverse entities relation ids that are "bind" to the currently persisted entity
UNCOV
206
        const changedInverseEntityRelationIds: ObjectLiteral[] = []
×
UNCOV
207
        relatedEntities.forEach((relatedEntity) => {
×
208
            // relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity)
209
            let relatedEntityRelationIdMap =
UNCOV
210
                relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity)
×
211

212
            // try to find a subject of this related entity, maybe it was loaded or was marked for persistence
UNCOV
213
            const relatedEntitySubject = this.subjects.find((subject) => {
×
UNCOV
214
                return subject.entity === relatedEntity
×
215
            })
216

217
            // if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
UNCOV
218
            if (relatedEntitySubject)
×
UNCOV
219
                relatedEntityRelationIdMap = relatedEntitySubject.identifier
×
220

UNCOV
221
            if (
×
222
                relatedEntityRelationIdMap !== undefined &&
×
223
                relatedEntityRelationIdMap !== null
224
            )
UNCOV
225
                changedInverseEntityRelationIds.push(relatedEntityRelationIdMap)
×
226
        })
227

228
        // now from all entities in the persisted entity find only those which aren't found in the db
UNCOV
229
        const removedJunctionEntityIds = databaseRelatedEntityIds.filter(
×
230
            (existRelationId) => {
UNCOV
231
                return !changedInverseEntityRelationIds.find(
×
232
                    (changedRelationId) => {
UNCOV
233
                        return OrmUtils.compareIds(
×
234
                            changedRelationId,
235
                            existRelationId,
236
                        )
237
                    },
238
                )
239
            },
240
        )
241

242
        // finally create a new junction remove operations for missing related entities
UNCOV
243
        removedJunctionEntityIds.forEach((removedEntityRelationId) => {
×
UNCOV
244
            const junctionSubject = new Subject({
×
245
                metadata: relation.junctionEntityMetadata!,
246
                parentSubject: subject,
247
                mustBeRemoved: true,
248
                identifier: this.buildJunctionIdentifier(
249
                    subject,
250
                    relation,
251
                    removedEntityRelationId,
252
                ),
253
            })
UNCOV
254
            this.subjects.push(junctionSubject)
×
255
        })
256
    }
257

258
    /**
259
     * Creates identifiers for junction table.
260
     * Example: { postId: 1, categoryId: 2 }
261
     */
262
    protected buildJunctionIdentifier(
263
        subject: Subject,
264
        relation: RelationMetadata,
265
        relationId: ObjectLiteral,
266
    ) {
UNCOV
267
        const ownerEntityMap = relation.isOwning ? subject.entity! : relationId
×
UNCOV
268
        const inverseEntityMap = relation.isOwning
×
269
            ? relationId
270
            : subject.entity!
271

UNCOV
272
        const identifier: ObjectLiteral = {}
×
UNCOV
273
        relation.junctionEntityMetadata!.ownerColumns.forEach((column) => {
×
UNCOV
274
            OrmUtils.mergeDeep(
×
275
                identifier,
276
                column.createValueMap(
277
                    column.referencedColumn!.getEntityValue(ownerEntityMap),
278
                ),
279
            )
280
        })
UNCOV
281
        relation.junctionEntityMetadata!.inverseColumns.forEach((column) => {
×
UNCOV
282
            OrmUtils.mergeDeep(
×
283
                identifier,
284
                column.createValueMap(
285
                    column.referencedColumn!.getEntityValue(inverseEntityMap),
286
                ),
287
            )
288
        })
UNCOV
289
        return identifier
×
290
    }
291
}
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