• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

typeorm / typeorm / 14796576772

02 May 2025 01:52PM UTC coverage: 45.367% (-30.9%) from 76.309%
14796576772

Pull #11434

github

web-flow
Merge ec4ce2d00 into fadad1a74
Pull Request #11434: feat: release PR releases using pkg.pr.new

5216 of 12761 branches covered (40.87%)

Branch coverage included in aggregate %.

11439 of 23951 relevant lines covered (47.76%)

15712.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.31
/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"
4✔
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 {
4✔
13
    // -------------------------------------------------------------------------
14
    // Constructor
15
    // -------------------------------------------------------------------------
16

17
    constructor(private connection: DataSource) {}
1,852✔
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
33
        if (queryRunner && queryRunner.isReleased) queryRunner = undefined // get new one if already closed
72!
34
        if (relation.isManyToOne || relation.isOneToOneOwner) {
72✔
35
            return this.loadManyToOneOrOneToOneOwner(
48✔
36
                relation,
37
                entityOrEntities,
38
                queryRunner,
39
                queryBuilder,
40
            )
41
        } else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
24✔
42
            return this.loadOneToManyOrOneToOneNotOwner(
12✔
43
                relation,
44
                entityOrEntities,
45
                queryRunner,
46
                queryBuilder,
47
            )
48
        } else if (relation.isManyToManyOwner) {
12!
49
            return this.loadManyToManyOwner(
12✔
50
                relation,
51
                entityOrEntities,
52
                queryRunner,
53
                queryBuilder,
54
            )
55
        } else {
56
            // many-to-many non owner
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> {
80
        const entities = Array.isArray(entityOrEntities)
48!
81
            ? entityOrEntities
82
            : [entityOrEntities]
83

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

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

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

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

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

159
        return qb.getMany()
48✔
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> {
176
        const entities = Array.isArray(entityOrEntities)
12!
177
            ? entityOrEntities
178
            : [entityOrEntities]
179
        const columns = relation.inverseRelation!.joinColumns
12✔
180
        const qb = queryBuilder
12!
181
            ? queryBuilder
182
            : this.connection
183
                  .createQueryBuilder(queryRunner)
184
                  .select(relation.propertyName)
185
                  .from(
186
                      relation.inverseRelation!.entityMetadata.target,
187
                      relation.propertyName,
188
                  )
189

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

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

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

243
        return qb.getMany()
12✔
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> {
262
        const entities = Array.isArray(entityOrEntities)
12!
263
            ? entityOrEntities
264
            : [entityOrEntities]
265
        const parameters = relation.joinColumns.reduce(
12✔
266
            (parameters, joinColumn) => {
267
                parameters[joinColumn.propertyName] = entities.map((entity) =>
12✔
268
                    joinColumn.referencedColumn!.getEntityValue(entity, true),
12✔
269
                )
270
                return parameters
12✔
271
            },
272
            {} as ObjectLiteral,
273
        )
274

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

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

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

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

311
        return qb.getMany()
12✔
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> {
329
        const entities = Array.isArray(entityOrEntities)
×
330
            ? entityOrEntities
331
            : [entityOrEntities]
332

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

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

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

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

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
    ) {
391
        const relationLoader = this
258✔
392
        const dataIndex = "__" + relation.propertyName + "__" // in what property of the entity loaded data will be stored
258✔
393
        const promiseIndex = "__promise_" + relation.propertyName + "__" // in what property of the entity loading promise will be stored
258✔
394
        const resolveIndex = "__has_" + relation.propertyName + "__" // indicates if relation data already was loaded or not, we need this flag if loaded data is empty
258✔
395

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

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

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

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