• 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

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

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

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

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

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

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

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

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

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

412✔
111
        if (columns.length === 1) {
412✔
112
            qb.where(
412✔
113
                `${joinAliasName}.${columns[0].propertyPath} IN (:...${
412✔
114
                    joinAliasName + "_" + columns[0].propertyName
412✔
115
                })`,
412✔
116
            )
412✔
117
            qb.setParameter(
412✔
118
                joinAliasName + "_" + columns[0].propertyName,
412✔
119
                entities.map((entity) =>
412✔
120
                    columns[0].getEntityValue(entity, true),
412✔
121
                ),
412✔
122
            )
412✔
123
        } else {
412!
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

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

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

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

164✔
190
        const aliasName = qb.expressionMap.mainAlias!.name
164✔
191

164✔
192
        if (columns.length === 1) {
164✔
193
            qb.where(
164✔
194
                `${aliasName}.${columns[0].propertyPath} IN (:...${
164✔
195
                    aliasName + "_" + columns[0].propertyName
164✔
196
                })`,
164✔
197
            )
164✔
198
            qb.setParameter(
164✔
199
                aliasName + "_" + columns[0].propertyName,
164✔
200
                entities.map((entity) =>
164✔
201
                    columns[0].referencedColumn!.getEntityValue(entity, true),
164✔
202
                ),
164✔
203
            )
164✔
204
        } else {
164!
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

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

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

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

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

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

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

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

172✔
311
        return qb.getMany()
172✔
312
    }
172✔
313

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

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

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

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

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

24✔
379
        return qb.getMany()
24✔
380
    }
24✔
381

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

4,546✔
396
        const setData = (entity: ObjectLiteral, value: any) => {
4,546✔
397
            entity[dataIndex] = value
904✔
398
            entity[resolveIndex] = true
904✔
399
            delete entity[promiseIndex]
904✔
400
            return value
904✔
401
        }
904✔
402
        const setPromise = (entity: ObjectLiteral, value: Promise<any>) => {
4,546✔
403
            delete entity[resolveIndex]
904✔
404
            delete entity[dataIndex]
904✔
405
            entity[promiseIndex] = value
904✔
406
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
904✔
407
            value.then(
904✔
408
                // ensure different value is not assigned yet
904✔
409
                (result) =>
904✔
410
                    entity[promiseIndex] === value
904✔
411
                        ? setData(entity, result)
904✔
412
                        : result,
904!
413
            )
904✔
414
            return value
904✔
415
        }
904✔
416

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

588✔
426
                if (this[promiseIndex])
588✔
427
                    // if related data is loading then return a promise relationLoader loads it
588✔
428
                    return this[promiseIndex]
748!
429

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