• 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

1.92
/src/query-builder/RelationLoader.ts
1
import { DataSource } from "../data-source/DataSource"
2
import { ObjectLiteral } from "../common/ObjectLiteral"
3
import { QueryRunner } from "../query-runner/QueryRunner"
4
import { RelationMetadata } from "../metadata/RelationMetadata"
5
import { FindOptionsUtils } from "../find-options/FindOptionsUtils"
1✔
6
import { SelectQueryBuilder } from "./SelectQueryBuilder"
7

8
/**
9
 * Wraps entities and creates getters/setters for their relations
10
 * to be able to lazily load relations when accessing these relations.
11
 */
12
export class RelationLoader {
1✔
13
    // -------------------------------------------------------------------------
14
    // Constructor
15
    // -------------------------------------------------------------------------
16

17
    constructor(private connection: DataSource) {}
42✔
18

19
    // -------------------------------------------------------------------------
20
    // Public Methods
21
    // -------------------------------------------------------------------------
22

23
    /**
24
     * Loads relation data for the given entity and its relation.
25
     */
26
    load(
27
        relation: RelationMetadata,
28
        entityOrEntities: ObjectLiteral | ObjectLiteral[],
29
        queryRunner?: QueryRunner,
30
        queryBuilder?: SelectQueryBuilder<any>,
31
    ): Promise<any[]> {
32
        // todo: check all places where it uses non array
UNCOV
33
        if (queryRunner && queryRunner.isReleased) queryRunner = undefined // get new one if already closed
×
UNCOV
34
        if (relation.isManyToOne || relation.isOneToOneOwner) {
×
UNCOV
35
            return this.loadManyToOneOrOneToOneOwner(
×
36
                relation,
37
                entityOrEntities,
38
                queryRunner,
39
                queryBuilder,
40
            )
UNCOV
41
        } else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
×
UNCOV
42
            return this.loadOneToManyOrOneToOneNotOwner(
×
43
                relation,
44
                entityOrEntities,
45
                queryRunner,
46
                queryBuilder,
47
            )
UNCOV
48
        } else if (relation.isManyToManyOwner) {
×
UNCOV
49
            return this.loadManyToManyOwner(
×
50
                relation,
51
                entityOrEntities,
52
                queryRunner,
53
                queryBuilder,
54
            )
55
        } else {
56
            // many-to-many non owner
UNCOV
57
            return this.loadManyToManyNotOwner(
×
58
                relation,
59
                entityOrEntities,
60
                queryRunner,
61
                queryBuilder,
62
            )
63
        }
64
    }
65

66
    /**
67
     * Loads data for many-to-one and one-to-one owner relations.
68
     *
69
     * (ow) post.category<=>category.post
70
     * loaded: category from post
71
     * example: SELECT category.id AS category_id, category.name AS category_name FROM category category
72
     *              INNER JOIN post Post ON Post.category=category.id WHERE Post.id=1
73
     */
74
    loadManyToOneOrOneToOneOwner(
75
        relation: RelationMetadata,
76
        entityOrEntities: ObjectLiteral | ObjectLiteral[],
77
        queryRunner?: QueryRunner,
78
        queryBuilder?: SelectQueryBuilder<any>,
79
    ): Promise<any> {
UNCOV
80
        const entities = Array.isArray(entityOrEntities)
×
81
            ? entityOrEntities
82
            : [entityOrEntities]
83

UNCOV
84
        const joinAliasName = relation.entityMetadata.name
×
UNCOV
85
        const qb = queryBuilder
×
86
            ? queryBuilder
87
            : this.connection
88
                  .createQueryBuilder(queryRunner)
89
                  .select(relation.propertyName) // category
90
                  .from(relation.type, relation.propertyName)
91

UNCOV
92
        const mainAlias = qb.expressionMap.mainAlias!.name
×
UNCOV
93
        const columns = relation.entityMetadata.primaryColumns
×
UNCOV
94
        const joinColumns = relation.isOwning
×
95
            ? relation.joinColumns
96
            : relation.inverseRelation!.joinColumns
UNCOV
97
        const conditions = joinColumns
×
98
            .map((joinColumn) => {
UNCOV
99
                return `${relation.entityMetadata.name}.${
×
100
                    joinColumn.propertyName
101
                } = ${mainAlias}.${joinColumn.referencedColumn!.propertyName}`
102
            })
103
            .join(" AND ")
104

UNCOV
105
        qb.innerJoin(
×
106
            relation.entityMetadata.target as Function,
107
            joinAliasName,
108
            conditions,
109
        )
110

UNCOV
111
        if (columns.length === 1) {
×
UNCOV
112
            qb.where(
×
113
                `${joinAliasName}.${columns[0].propertyPath} IN (:...${
114
                    joinAliasName + "_" + columns[0].propertyName
115
                })`,
116
            )
UNCOV
117
            qb.setParameter(
×
118
                joinAliasName + "_" + columns[0].propertyName,
119
                entities.map((entity) =>
UNCOV
120
                    columns[0].getEntityValue(entity, true),
×
121
                ),
122
            )
123
        } else {
124
            const condition = entities
×
125
                .map((entity, entityIndex) => {
126
                    return columns
×
127
                        .map((column, columnIndex) => {
128
                            const paramName =
129
                                joinAliasName +
×
130
                                "_entity_" +
131
                                entityIndex +
132
                                "_" +
133
                                columnIndex
134
                            qb.setParameter(
×
135
                                paramName,
136
                                column.getEntityValue(entity, true),
137
                            )
138
                            return (
×
139
                                joinAliasName +
140
                                "." +
141
                                column.propertyPath +
142
                                " = :" +
143
                                paramName
144
                            )
145
                        })
146
                        .join(" AND ")
147
                })
148
                .map((condition) => "(" + condition + ")")
×
149
                .join(" OR ")
150
            qb.where(condition)
×
151
        }
152

UNCOV
153
        FindOptionsUtils.joinEagerRelations(
×
154
            qb,
155
            qb.alias,
156
            qb.expressionMap.mainAlias!.metadata,
157
        )
158

UNCOV
159
        return qb.getMany()
×
160
        // return qb.getOne(); todo: fix all usages
161
    }
162

163
    /**
164
     * Loads data for one-to-many and one-to-one not owner relations.
165
     *
166
     * SELECT post
167
     * FROM post post
168
     * WHERE post.[joinColumn.name] = entity[joinColumn.referencedColumn]
169
     */
170
    loadOneToManyOrOneToOneNotOwner(
171
        relation: RelationMetadata,
172
        entityOrEntities: ObjectLiteral | ObjectLiteral[],
173
        queryRunner?: QueryRunner,
174
        queryBuilder?: SelectQueryBuilder<any>,
175
    ): Promise<any> {
UNCOV
176
        const entities = Array.isArray(entityOrEntities)
×
177
            ? entityOrEntities
178
            : [entityOrEntities]
UNCOV
179
        const columns = relation.inverseRelation!.joinColumns
×
UNCOV
180
        const qb = queryBuilder
×
181
            ? queryBuilder
182
            : this.connection
183
                  .createQueryBuilder(queryRunner)
184
                  .select(relation.propertyName)
185
                  .from(
186
                      relation.inverseRelation!.entityMetadata.target,
187
                      relation.propertyName,
188
                  )
189

UNCOV
190
        const aliasName = qb.expressionMap.mainAlias!.name
×
191

UNCOV
192
        if (columns.length === 1) {
×
UNCOV
193
            qb.where(
×
194
                `${aliasName}.${columns[0].propertyPath} IN (:...${
195
                    aliasName + "_" + columns[0].propertyName
196
                })`,
197
            )
UNCOV
198
            qb.setParameter(
×
199
                aliasName + "_" + columns[0].propertyName,
200
                entities.map((entity) =>
UNCOV
201
                    columns[0].referencedColumn!.getEntityValue(entity, true),
×
202
                ),
203
            )
204
        } else {
205
            const condition = entities
×
206
                .map((entity, entityIndex) => {
207
                    return columns
×
208
                        .map((column, columnIndex) => {
209
                            const paramName =
210
                                aliasName +
×
211
                                "_entity_" +
212
                                entityIndex +
213
                                "_" +
214
                                columnIndex
215
                            qb.setParameter(
×
216
                                paramName,
217
                                column.referencedColumn!.getEntityValue(
218
                                    entity,
219
                                    true,
220
                                ),
221
                            )
222
                            return (
×
223
                                aliasName +
224
                                "." +
225
                                column.propertyPath +
226
                                " = :" +
227
                                paramName
228
                            )
229
                        })
230
                        .join(" AND ")
231
                })
232
                .map((condition) => "(" + condition + ")")
×
233
                .join(" OR ")
234
            qb.where(condition)
×
235
        }
236

UNCOV
237
        FindOptionsUtils.joinEagerRelations(
×
238
            qb,
239
            qb.alias,
240
            qb.expressionMap.mainAlias!.metadata,
241
        )
242

UNCOV
243
        return qb.getMany()
×
244
        // return relation.isOneToMany ? qb.getMany() : qb.getOne(); todo: fix all usages
245
    }
246

247
    /**
248
     * Loads data for many-to-many owner relations.
249
     *
250
     * SELECT category
251
     * FROM category category
252
     * INNER JOIN post_categories post_categories
253
     * ON post_categories.postId = :postId
254
     * AND post_categories.categoryId = category.id
255
     */
256
    loadManyToManyOwner(
257
        relation: RelationMetadata,
258
        entityOrEntities: ObjectLiteral | ObjectLiteral[],
259
        queryRunner?: QueryRunner,
260
        queryBuilder?: SelectQueryBuilder<any>,
261
    ): Promise<any> {
UNCOV
262
        const entities = Array.isArray(entityOrEntities)
×
263
            ? entityOrEntities
264
            : [entityOrEntities]
UNCOV
265
        const parameters = relation.joinColumns.reduce(
×
266
            (parameters, joinColumn) => {
UNCOV
267
                parameters[joinColumn.propertyName] = entities.map((entity) =>
×
UNCOV
268
                    joinColumn.referencedColumn!.getEntityValue(entity, true),
×
269
                )
UNCOV
270
                return parameters
×
271
            },
272
            {} as ObjectLiteral,
273
        )
274

UNCOV
275
        const qb = queryBuilder
×
276
            ? queryBuilder
277
            : this.connection
278
                  .createQueryBuilder(queryRunner)
279
                  .select(relation.propertyName)
280
                  .from(relation.type, relation.propertyName)
281

UNCOV
282
        const mainAlias = qb.expressionMap.mainAlias!.name
×
UNCOV
283
        const joinAlias = relation.junctionEntityMetadata!.tableName
×
UNCOV
284
        const joinColumnConditions = relation.joinColumns.map((joinColumn) => {
×
UNCOV
285
            return `${joinAlias}.${joinColumn.propertyName} IN (:...${joinColumn.propertyName})`
×
286
        })
UNCOV
287
        const inverseJoinColumnConditions = relation.inverseJoinColumns.map(
×
288
            (inverseJoinColumn) => {
UNCOV
289
                return `${joinAlias}.${
×
290
                    inverseJoinColumn.propertyName
291
                }=${mainAlias}.${
292
                    inverseJoinColumn.referencedColumn!.propertyName
293
                }`
294
            },
295
        )
296

UNCOV
297
        qb.innerJoin(
×
298
            joinAlias,
299
            joinAlias,
300
            [...joinColumnConditions, ...inverseJoinColumnConditions].join(
301
                " AND ",
302
            ),
303
        ).setParameters(parameters)
304

UNCOV
305
        FindOptionsUtils.joinEagerRelations(
×
306
            qb,
307
            qb.alias,
308
            qb.expressionMap.mainAlias!.metadata,
309
        )
310

UNCOV
311
        return qb.getMany()
×
312
    }
313

314
    /**
315
     * Loads data for many-to-many not owner relations.
316
     *
317
     * SELECT post
318
     * FROM post post
319
     * INNER JOIN post_categories post_categories
320
     * ON post_categories.postId = post.id
321
     * AND post_categories.categoryId = post_categories.categoryId
322
     */
323
    loadManyToManyNotOwner(
324
        relation: RelationMetadata,
325
        entityOrEntities: ObjectLiteral | ObjectLiteral[],
326
        queryRunner?: QueryRunner,
327
        queryBuilder?: SelectQueryBuilder<any>,
328
    ): Promise<any> {
UNCOV
329
        const entities = Array.isArray(entityOrEntities)
×
330
            ? entityOrEntities
331
            : [entityOrEntities]
332

UNCOV
333
        const qb = queryBuilder
×
334
            ? queryBuilder
335
            : this.connection
336
                  .createQueryBuilder(queryRunner)
337
                  .select(relation.propertyName)
338
                  .from(relation.type, relation.propertyName)
339

UNCOV
340
        const mainAlias = qb.expressionMap.mainAlias!.name
×
UNCOV
341
        const joinAlias = relation.junctionEntityMetadata!.tableName
×
UNCOV
342
        const joinColumnConditions = relation.inverseRelation!.joinColumns.map(
×
343
            (joinColumn) => {
UNCOV
344
                return `${joinAlias}.${
×
345
                    joinColumn.propertyName
346
                } = ${mainAlias}.${joinColumn.referencedColumn!.propertyName}`
347
            },
348
        )
349
        const inverseJoinColumnConditions =
UNCOV
350
            relation.inverseRelation!.inverseJoinColumns.map(
×
351
                (inverseJoinColumn) => {
UNCOV
352
                    return `${joinAlias}.${inverseJoinColumn.propertyName} IN (:...${inverseJoinColumn.propertyName})`
×
353
                },
354
            )
UNCOV
355
        const parameters = relation.inverseRelation!.inverseJoinColumns.reduce(
×
356
            (parameters, joinColumn) => {
UNCOV
357
                parameters[joinColumn.propertyName] = entities.map((entity) =>
×
UNCOV
358
                    joinColumn.referencedColumn!.getEntityValue(entity, true),
×
359
                )
UNCOV
360
                return parameters
×
361
            },
362
            {} as ObjectLiteral,
363
        )
364

UNCOV
365
        qb.innerJoin(
×
366
            joinAlias,
367
            joinAlias,
368
            [...joinColumnConditions, ...inverseJoinColumnConditions].join(
369
                " AND ",
370
            ),
371
        ).setParameters(parameters)
372

UNCOV
373
        FindOptionsUtils.joinEagerRelations(
×
374
            qb,
375
            qb.alias,
376
            qb.expressionMap.mainAlias!.metadata,
377
        )
378

UNCOV
379
        return qb.getMany()
×
380
    }
381

382
    /**
383
     * Wraps given entity and creates getters/setters for its given relation
384
     * to be able to lazily load data when accessing this relation.
385
     */
386
    enableLazyLoad(
387
        relation: RelationMetadata,
388
        entity: ObjectLiteral,
389
        queryRunner?: QueryRunner,
390
    ) {
UNCOV
391
        const relationLoader = this
×
UNCOV
392
        const dataIndex = "__" + relation.propertyName + "__" // in what property of the entity loaded data will be stored
×
UNCOV
393
        const promiseIndex = "__promise_" + relation.propertyName + "__" // in what property of the entity loading promise will be stored
×
UNCOV
394
        const resolveIndex = "__has_" + relation.propertyName + "__" // indicates if relation data already was loaded or not, we need this flag if loaded data is empty
×
395

UNCOV
396
        const setData = (entity: ObjectLiteral, value: any) => {
×
UNCOV
397
            entity[dataIndex] = value
×
UNCOV
398
            entity[resolveIndex] = true
×
UNCOV
399
            delete entity[promiseIndex]
×
UNCOV
400
            return value
×
401
        }
UNCOV
402
        const setPromise = (entity: ObjectLiteral, value: Promise<any>) => {
×
UNCOV
403
            delete entity[resolveIndex]
×
UNCOV
404
            delete entity[dataIndex]
×
UNCOV
405
            entity[promiseIndex] = value
×
UNCOV
406
            value.then(
×
407
                // ensure different value is not assigned yet
408
                (result) =>
UNCOV
409
                    entity[promiseIndex] === value
×
410
                        ? setData(entity, result)
411
                        : result,
412
            )
UNCOV
413
            return value
×
414
        }
415

UNCOV
416
        Object.defineProperty(entity, relation.propertyName, {
×
417
            get: function () {
UNCOV
418
                if (
×
419
                    this[resolveIndex] === true ||
×
420
                    this[dataIndex] !== undefined
421
                )
422
                    // if related data already was loaded then simply return it
UNCOV
423
                    return Promise.resolve(this[dataIndex])
×
424

UNCOV
425
                if (this[promiseIndex])
×
426
                    // if related data is loading then return a promise relationLoader loads it
UNCOV
427
                    return this[promiseIndex]
×
428

429
                // nothing is loaded yet, load relation data and save it in the model once they are loaded
UNCOV
430
                const loader = relationLoader
×
431
                    .load(relation, this, queryRunner)
432
                    .then((result) =>
UNCOV
433
                        relation.isOneToOne || relation.isManyToOne
×
434
                            ? result.length === 0
×
435
                                ? null
436
                                : result[0]
437
                            : result,
438
                    )
UNCOV
439
                return setPromise(this, loader)
×
440
            },
441
            set: function (value: any | Promise<any>) {
UNCOV
442
                if (value instanceof Promise) {
×
443
                    // if set data is a promise then wait for its resolve and save in the object
UNCOV
444
                    setPromise(this, value)
×
445
                } else {
446
                    // if its direct data set (non promise, probably not safe-typed)
UNCOV
447
                    setData(this, value)
×
448
                }
449
            },
450
            configurable: true,
451
            enumerable: false,
452
        })
453
    }
454
}
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