• 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

89.23
/src/find-options/FindOptionsUtils.ts
1
import { FindManyOptions } from "./FindManyOptions"
2
import { FindOneOptions } from "./FindOneOptions"
3
import { SelectQueryBuilder } from "../query-builder/SelectQueryBuilder"
4
import { FindRelationsNotFoundError } from "../error"
4✔
5
import { EntityMetadata } from "../metadata/EntityMetadata"
6
import { DriverUtils } from "../driver/DriverUtils"
4✔
7
import { FindTreeOptions } from "./FindTreeOptions"
8
import { ObjectLiteral } from "../common/ObjectLiteral"
9
import { RelationMetadata } from "../metadata/RelationMetadata"
10
import { EntityPropertyNotFoundError } from "../error"
4✔
11

12
/**
13
 * Utilities to work with FindOptions.
14
 */
15
export class FindOptionsUtils {
4✔
16
    // -------------------------------------------------------------------------
17
    // Public Static Methods
18
    // -------------------------------------------------------------------------
19

20
    /**
21
     * Checks if given object is really instance of FindOneOptions interface.
22
     */
23
    static isFindOneOptions<Entity = any>(
24
        obj: any,
25
    ): obj is FindOneOptions<Entity> {
26
        const possibleOptions: FindOneOptions<Entity> = obj
435✔
27
        return (
435✔
28
            possibleOptions &&
3,450✔
29
            (Array.isArray(possibleOptions.select) ||
30
                Array.isArray(possibleOptions.relations) ||
31
                typeof possibleOptions.select === "object" ||
32
                typeof possibleOptions.relations === "object" ||
33
                typeof possibleOptions.where === "object" ||
34
                // typeof possibleOptions.where === "string" ||
35
                typeof possibleOptions.join === "object" ||
36
                typeof possibleOptions.order === "object" ||
37
                typeof possibleOptions.cache === "object" ||
38
                typeof possibleOptions.cache === "boolean" ||
39
                typeof possibleOptions.cache === "number" ||
40
                typeof possibleOptions.comment === "string" ||
41
                typeof possibleOptions.lock === "object" ||
42
                typeof possibleOptions.loadRelationIds === "object" ||
43
                typeof possibleOptions.loadRelationIds === "boolean" ||
44
                typeof possibleOptions.loadEagerRelations === "boolean" ||
45
                typeof possibleOptions.withDeleted === "boolean" ||
46
                typeof possibleOptions.relationLoadStrategy === "string" ||
47
                typeof possibleOptions.transaction === "boolean")
48
        )
49
    }
50

51
    /**
52
     * Checks if given object is really instance of FindManyOptions interface.
53
     */
54
    static isFindManyOptions<Entity = any>(
55
        obj: any,
56
    ): obj is FindManyOptions<Entity> {
57
        const possibleOptions: FindManyOptions<Entity> = obj
710✔
58
        return (
710✔
59
            possibleOptions &&
1,229✔
60
            (this.isFindOneOptions(possibleOptions) ||
61
                typeof (possibleOptions as FindManyOptions<any>).skip ===
62
                    "number" ||
63
                typeof (possibleOptions as FindManyOptions<any>).take ===
64
                    "number" ||
65
                typeof (possibleOptions as FindManyOptions<any>).skip ===
66
                    "string" ||
67
                typeof (possibleOptions as FindManyOptions<any>).take ===
68
                    "string")
69
        )
70
    }
71

72
    /**
73
     * Checks if given object is really instance of FindOptions interface.
74
     */
75
    static extractFindManyOptionsAlias(object: any): string | undefined {
76
        if (this.isFindManyOptions(object) && object.join)
710✔
77
            return object.join.alias
20✔
78

79
        return undefined
690✔
80
    }
81

82
    /**
83
     * Applies give find many options to the given query builder.
84

85
    static applyFindManyOptionsOrConditionsToQueryBuilder<T>(qb: SelectQueryBuilder<T>, options: FindManyOptions<T>|Partial<T>|undefined): SelectQueryBuilder<T> {
86
        if (this.isFindManyOptions(options))
87
            return this.applyOptionsToQueryBuilder(qb, options);
88

89
        if (options)
90
            return qb.where(options);
91

92
        return qb;
93
    }*/
94

95
    /**
96
     * Applies give find options to the given query builder.
97

98
    static applyOptionsToQueryBuilder<T>(qb: SelectQueryBuilder<T>, options: FindOneOptions<T>|FindManyOptions<T>|undefined): SelectQueryBuilder<T> {
99

100
        // if options are not set then simply return query builder. This is made for simplicity of usage.
101
        if (!options || (!this.isFindOneOptions(options) && !this.isFindManyOptions(options)))
102
            return qb;
103

104
        if (options.transaction === true) {
105
            qb.expressionMap.useTransaction = true;
106
        }
107

108
        if (!qb.expressionMap.mainAlias || !qb.expressionMap.mainAlias.hasMetadata)
109
            return qb;
110

111
        const metadata = qb.expressionMap.mainAlias!.metadata;
112

113
        // apply all options from FindOptions
114
        if (options.comment) {
115
            qb.comment(options.comment);
116
        }
117

118
        if (options.withDeleted) {
119
            qb.withDeleted();
120
        }
121

122
        if (options.select) {
123
            qb.select([]);
124
            options.select.forEach(select => {
125
                if (!metadata.hasColumnWithPropertyPath(`${select}`))
126
                    throw new TypeORMError(`${select} column was not found in the ${metadata.name} entity.`);
127

128
                const columns = metadata.findColumnsWithPropertyPath(`${select}`);
129

130
                for (const column of columns) {
131
                    qb.addSelect(qb.alias + "." + column.propertyPath);
132
                }
133
            });
134
        }
135

136
        if (options.relations) {
137
            // Copy because `applyRelationsRecursively` modifies it
138
            const allRelations = [...options.relations];
139
            this.applyRelationsRecursively(qb, allRelations, qb.expressionMap.mainAlias!.name, qb.expressionMap.mainAlias!.metadata, "");
140
            // recursive removes found relations from allRelations array
141
            // if there are relations left in this array it means those relations were not found in the entity structure
142
            // so, we give an exception about not found relations
143
            if (allRelations.length > 0)
144
                throw new FindRelationsNotFoundError(allRelations);
145
        }
146

147
        if (options.join) {
148
            if (options.join.leftJoin)
149
                Object.keys(options.join.leftJoin).forEach(key => {
150
                    qb.leftJoin(options.join!.leftJoin![key], key);
151
                });
152

153
            if (options.join.innerJoin)
154
                Object.keys(options.join.innerJoin).forEach(key => {
155
                    qb.innerJoin(options.join!.innerJoin![key], key);
156
                });
157

158
            if (options.join.leftJoinAndSelect)
159
                Object.keys(options.join.leftJoinAndSelect).forEach(key => {
160
                    qb.leftJoinAndSelect(options.join!.leftJoinAndSelect![key], key);
161
                });
162

163
            if (options.join.innerJoinAndSelect)
164
                Object.keys(options.join.innerJoinAndSelect).forEach(key => {
165
                    qb.innerJoinAndSelect(options.join!.innerJoinAndSelect![key], key);
166
                });
167
        }
168

169
        if (options.cache) {
170
            if (options.cache instanceof Object) {
171
                const cache = options.cache as { id: any, milliseconds: number };
172
                qb.cache(cache.id, cache.milliseconds);
173
            } else {
174
                qb.cache(options.cache);
175
            }
176
        }
177

178
        if (options.lock) {
179
            if (options.lock.mode === "optimistic") {
180
                qb.setLock(options.lock.mode, options.lock.version);
181
            } else if (
182
                options.lock.mode === "pessimistic_read" ||
183
                options.lock.mode === "pessimistic_write" ||
184
                options.lock.mode === "dirty_read" ||
185
                options.lock.mode === "pessimistic_partial_write" ||
186
                options.lock.mode === "pessimistic_write_or_fail" ||
187
                options.lock.mode === "for_no_key_update" ||
188
                options.lock.mode === "for_key_share"
189
            ) {
190
                const tableNames = options.lock.tables ? options.lock.tables.map((table) => {
191
                    const tableAlias = qb.expressionMap.aliases.find((alias) => {
192
                        return alias.metadata.tableNameWithoutPrefix === table;
193
                    });
194
                    if (!tableAlias) {
195
                        throw new TypeORMError(`"${table}" is not part of this query`);
196
                    }
197
                    return qb.escape(tableAlias.name);
198
                }) : undefined;
199
                qb.setLock(options.lock.mode, undefined, tableNames);
200
            }
201
        }
202

203
        if (options.loadRelationIds === true) {
204
            qb.loadAllRelationIds();
205

206
        } else if (options.loadRelationIds instanceof Object) {
207
            qb.loadAllRelationIds(options.loadRelationIds as any);
208
        }
209

210
        if (options.where)
211
            qb.where(options.where);
212

213
        if ((options as FindManyOptions<T>).skip)
214
            qb.skip((options as FindManyOptions<T>).skip!);
215

216
        if ((options as FindManyOptions<T>).take)
217
            qb.take((options as FindManyOptions<T>).take!);
218

219
        if (options.order)
220
            Object.keys(options.order).forEach(key => {
221
                const order = ((options as FindOneOptions<T>).order as any)[key as any];
222

223
                if (!metadata.findColumnWithPropertyPath(key))
224
                    throw new Error(`${key} column was not found in the ${metadata.name} entity.`);
225

226
                switch (order) {
227
                    case 1:
228
                        qb.addOrderBy(qb.alias + "." + key, "ASC");
229
                        break;
230
                    case -1:
231
                        qb.addOrderBy(qb.alias + "." + key, "DESC");
232
                        break;
233
                    case "ASC":
234
                        qb.addOrderBy(qb.alias + "." + key, "ASC");
235
                        break;
236
                    case "DESC":
237
                        qb.addOrderBy(qb.alias + "." + key, "DESC");
238
                        break;
239
                }
240
            });
241

242
        return qb;
243
    }*/
244

245
    static applyOptionsToTreeQueryBuilder<T extends ObjectLiteral>(
246
        qb: SelectQueryBuilder<T>,
247
        options?: FindTreeOptions,
248
    ): SelectQueryBuilder<T> {
249
        if (options?.relations) {
917✔
250
            // Copy because `applyRelationsRecursively` modifies it
251
            const allRelations = [...options.relations]
44✔
252

253
            FindOptionsUtils.applyRelationsRecursively(
44✔
254
                qb,
255
                allRelations,
256
                qb.expressionMap.mainAlias!.name,
257
                qb.expressionMap.mainAlias!.metadata,
258
                "",
259
            )
260

261
            // recursive removes found relations from allRelations array
262
            // if there are relations left in this array it means those relations were not found in the entity structure
263
            // so, we give an exception about not found relations
264
            if (allRelations.length > 0)
44!
265
                throw new FindRelationsNotFoundError(allRelations)
×
266
        }
267

268
        return qb
917✔
269
    }
270

271
    // -------------------------------------------------------------------------
272
    // Protected Static Methods
273
    // -------------------------------------------------------------------------
274

275
    /**
276
     * Adds joins for all relations and sub-relations of the given relations provided in the find options.
277
     */
278
    public static applyRelationsRecursively(
279
        qb: SelectQueryBuilder<any>,
280
        allRelations: string[],
281
        alias: string,
282
        metadata: EntityMetadata,
283
        prefix: string,
284
    ): void {
285
        // find all relations that match given prefix
286
        let matchedBaseRelations: RelationMetadata[] = []
88✔
287
        if (prefix) {
88✔
288
            const regexp = new RegExp("^" + prefix.replace(".", "\\.") + "\\.")
44✔
289
            matchedBaseRelations = allRelations
44✔
290
                .filter((relation) => relation.match(regexp))
×
291
                .map((relation) =>
292
                    metadata.findRelationWithPropertyPath(
×
293
                        relation.replace(regexp, ""),
294
                    ),
295
                )
296
                .filter((entity) => entity) as RelationMetadata[]
×
297
        } else {
298
            matchedBaseRelations = allRelations
44✔
299
                .map((relation) =>
300
                    metadata.findRelationWithPropertyPath(relation),
44✔
301
                )
302
                .filter((entity) => entity) as RelationMetadata[]
44✔
303
        }
304

305
        // go through all matched relations and add join for them
306
        matchedBaseRelations.forEach((relation) => {
88✔
307
            // generate a relation alias
308
            const relationAlias: string = DriverUtils.buildAlias(
44✔
309
                qb.connection.driver,
310
                { joiner: "__" },
311
                alias,
312
                relation.propertyPath,
313
            )
314

315
            // add a join for the found relation
316
            const selection = alias + "." + relation.propertyPath
44✔
317
            if (qb.expressionMap.relationLoadStrategy === "query") {
44!
318
                qb.concatRelationMetadata(relation)
×
319
            } else {
320
                qb.leftJoinAndSelect(selection, relationAlias)
44✔
321
            }
322

323
            // remove added relations from the allRelations array, this is needed to find all not found relations at the end
324
            allRelations.splice(
44✔
325
                allRelations.indexOf(
326
                    prefix
44!
327
                        ? prefix + "." + relation.propertyPath
328
                        : relation.propertyPath,
329
                ),
330
                1,
331
            )
332

333
            // try to find sub-relations
334
            let relationMetadata: EntityMetadata | undefined
335
            let relationName: string | undefined
336

337
            if (qb.expressionMap.relationLoadStrategy === "query") {
44!
338
                relationMetadata = relation.inverseEntityMetadata
×
339
                relationName = relationAlias
×
340
            } else {
341
                const join = qb.expressionMap.joinAttributes.find(
44✔
342
                    (join) => join.entityOrProperty === selection,
44✔
343
                )
344
                relationMetadata = join!.metadata!
44✔
345
                relationName = join!.alias.name
44✔
346
            }
347

348
            if (!relationName || !relationMetadata) {
44!
349
                throw new EntityPropertyNotFoundError(
×
350
                    relation.propertyPath,
351
                    metadata,
352
                )
353
            }
354

355
            this.applyRelationsRecursively(
44✔
356
                qb,
357
                allRelations,
358
                relationName,
359
                relationMetadata,
360
                prefix
44!
361
                    ? prefix + "." + relation.propertyPath
362
                    : relation.propertyPath,
363
            )
364

365
            // join the eager relations of the found relation
366
            // Only supported for "join" relationLoadStrategy
367
            if (qb.expressionMap.relationLoadStrategy === "join") {
44✔
368
                const relMetadata = metadata.relations.find(
44✔
369
                    (metadata) =>
370
                        metadata.propertyName === relation.propertyPath,
152✔
371
                )
372
                if (relMetadata) {
44✔
373
                    this.joinEagerRelations(
44✔
374
                        qb,
375
                        relationAlias,
376
                        relMetadata.inverseEntityMetadata,
377
                    )
378
                }
379
            }
380
        })
381
    }
382

383
    public static joinEagerRelations(
384
        qb: SelectQueryBuilder<any>,
385
        alias: string,
386
        metadata: EntityMetadata,
387
    ) {
388
        metadata.eagerRelations.forEach((relation) => {
3,711✔
389
            // generate a relation alias
390
            let relationAlias: string = DriverUtils.buildAlias(
320✔
391
                qb.connection.driver,
392
                { joiner: "__" },
393
                alias,
394
                relation.propertyName,
395
            )
396

397
            // add a join for the relation
398
            // Checking whether the relation wasn't joined yet.
399
            let addJoin = true
320✔
400
            // TODO: Review this validation
401
            for (const join of qb.expressionMap.joinAttributes) {
320✔
402
                if (
400✔
403
                    join.condition !== undefined ||
1,888✔
404
                    join.mapToProperty !== undefined ||
405
                    join.isMappingMany !== undefined ||
406
                    join.direction !== "LEFT" ||
407
                    join.entityOrProperty !==
408
                        `${alias}.${relation.propertyPath}`
409
                ) {
410
                    continue
372✔
411
                }
412
                addJoin = false
28✔
413
                relationAlias = join.alias.name
28✔
414
                break
28✔
415
            }
416

417
            const joinAlreadyAdded = Boolean(
320✔
418
                qb.expressionMap.joinAttributes.find(
419
                    (joinAttribute) =>
420
                        joinAttribute.alias.name === relationAlias,
400✔
421
                ),
422
            )
423

424
            if (addJoin && !joinAlreadyAdded) {
320✔
425
                qb.leftJoin(alias + "." + relation.propertyPath, relationAlias)
292✔
426
            }
427

428
            // Checking whether the relation wasn't selected yet.
429
            // This check shall be after the join check to detect relationAlias.
430
            let addSelect = true
320✔
431
            for (const select of qb.expressionMap.selects) {
320✔
432
                if (
692✔
433
                    select.aliasName !== undefined ||
2,076✔
434
                    select.virtual !== undefined ||
435
                    select.selection !== relationAlias
436
                ) {
437
                    continue
664✔
438
                }
439
                addSelect = false
28✔
440
                break
28✔
441
            }
442

443
            if (addSelect) {
320✔
444
                qb.addSelect(relationAlias)
292✔
445
            }
446

447
            // (recursive) join the eager relations
448
            this.joinEagerRelations(
320✔
449
                qb,
450
                relationAlias,
451
                relation.inverseEntityMetadata,
452
            )
453
        })
454
    }
455
}
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