import { FindManyOptions } from "./FindManyOptions" import { FindOneOptions } from "./FindOneOptions" import { SelectQueryBuilder } from "../query-builder/SelectQueryBuilder" import { FindRelationsNotFoundError } from "../error" import { EntityMetadata } from "../metadata/EntityMetadata" import { DriverUtils } from "../driver/DriverUtils" import { FindTreeOptions } from "./FindTreeOptions" import { ObjectLiteral } from "../common/ObjectLiteral" import { RelationMetadata } from "../metadata/RelationMetadata" import { EntityPropertyNotFoundError } from "../error" /** * Utilities to work with FindOptions. */ export class FindOptionsUtils { // ------------------------------------------------------------------------- // Public Static Methods // ------------------------------------------------------------------------- /** * Checks if given object is really instance of FindOneOptions interface. */ static isFindOneOptions( obj: any, ): obj is FindOneOptions { const possibleOptions: FindOneOptions = obj return ( possibleOptions && (Array.isArray(possibleOptions.select) || Array.isArray(possibleOptions.relations) || typeof possibleOptions.select === "object" || typeof possibleOptions.relations === "object" || typeof possibleOptions.where === "object" || // typeof possibleOptions.where === "string" || typeof possibleOptions.join === "object" || typeof possibleOptions.order === "object" || typeof possibleOptions.cache === "object" || typeof possibleOptions.cache === "boolean" || typeof possibleOptions.cache === "number" || typeof possibleOptions.comment === "string" || typeof possibleOptions.lock === "object" || typeof possibleOptions.loadRelationIds === "object" || typeof possibleOptions.loadRelationIds === "boolean" || typeof possibleOptions.loadEagerRelations === "boolean" || typeof possibleOptions.withDeleted === "boolean" || typeof possibleOptions.relationLoadStrategy === "string" || typeof possibleOptions.transaction === "boolean") ) } /** * Checks if given object is really instance of FindManyOptions interface. */ static isFindManyOptions( obj: any, ): obj is FindManyOptions { const possibleOptions: FindManyOptions = obj return ( possibleOptions && (this.isFindOneOptions(possibleOptions) || typeof (possibleOptions as FindManyOptions).skip === "number" || typeof (possibleOptions as FindManyOptions).take === "number" || typeof (possibleOptions as FindManyOptions).skip === "string" || typeof (possibleOptions as FindManyOptions).take === "string") ) } /** * Checks if given object is really instance of FindOptions interface. */ static extractFindManyOptionsAlias(object: any): string | undefined { if (this.isFindManyOptions(object) && object.join) return object.join.alias return undefined } /** * Applies give find many options to the given query builder. static applyFindManyOptionsOrConditionsToQueryBuilder(qb: SelectQueryBuilder, options: FindManyOptions|Partial|undefined): SelectQueryBuilder { if (this.isFindManyOptions(options)) return this.applyOptionsToQueryBuilder(qb, options); if (options) return qb.where(options); return qb; }*/ /** * Applies give find options to the given query builder. static applyOptionsToQueryBuilder(qb: SelectQueryBuilder, options: FindOneOptions|FindManyOptions|undefined): SelectQueryBuilder { // if options are not set then simply return query builder. This is made for simplicity of usage. if (!options || (!this.isFindOneOptions(options) && !this.isFindManyOptions(options))) return qb; if (options.transaction === true) { qb.expressionMap.useTransaction = true; } if (!qb.expressionMap.mainAlias || !qb.expressionMap.mainAlias.hasMetadata) return qb; const metadata = qb.expressionMap.mainAlias!.metadata; // apply all options from FindOptions if (options.comment) { qb.comment(options.comment); } if (options.withDeleted) { qb.withDeleted(); } if (options.select) { qb.select([]); options.select.forEach(select => { if (!metadata.hasColumnWithPropertyPath(`${select}`)) throw new TypeORMError(`${select} column was not found in the ${metadata.name} entity.`); const columns = metadata.findColumnsWithPropertyPath(`${select}`); for (const column of columns) { qb.addSelect(qb.alias + "." + column.propertyPath); } }); } if (options.relations) { // Copy because `applyRelationsRecursively` modifies it const allRelations = [...options.relations]; this.applyRelationsRecursively(qb, allRelations, qb.expressionMap.mainAlias!.name, qb.expressionMap.mainAlias!.metadata, ""); // recursive removes found relations from allRelations array // if there are relations left in this array it means those relations were not found in the entity structure // so, we give an exception about not found relations if (allRelations.length > 0) throw new FindRelationsNotFoundError(allRelations); } if (options.join) { if (options.join.leftJoin) Object.keys(options.join.leftJoin).forEach(key => { qb.leftJoin(options.join!.leftJoin![key], key); }); if (options.join.innerJoin) Object.keys(options.join.innerJoin).forEach(key => { qb.innerJoin(options.join!.innerJoin![key], key); }); if (options.join.leftJoinAndSelect) Object.keys(options.join.leftJoinAndSelect).forEach(key => { qb.leftJoinAndSelect(options.join!.leftJoinAndSelect![key], key); }); if (options.join.innerJoinAndSelect) Object.keys(options.join.innerJoinAndSelect).forEach(key => { qb.innerJoinAndSelect(options.join!.innerJoinAndSelect![key], key); }); } if (options.cache) { if (options.cache instanceof Object) { const cache = options.cache as { id: any, milliseconds: number }; qb.cache(cache.id, cache.milliseconds); } else { qb.cache(options.cache); } } if (options.lock) { if (options.lock.mode === "optimistic") { qb.setLock(options.lock.mode, options.lock.version); } else if ( options.lock.mode === "pessimistic_read" || options.lock.mode === "pessimistic_write" || options.lock.mode === "dirty_read" || options.lock.mode === "pessimistic_partial_write" || options.lock.mode === "pessimistic_write_or_fail" || options.lock.mode === "for_no_key_update" || options.lock.mode === "for_key_share" ) { const tableNames = options.lock.tables ? options.lock.tables.map((table) => { const tableAlias = qb.expressionMap.aliases.find((alias) => { return alias.metadata.tableNameWithoutPrefix === table; }); if (!tableAlias) { throw new TypeORMError(`"${table}" is not part of this query`); } return qb.escape(tableAlias.name); }) : undefined; qb.setLock(options.lock.mode, undefined, tableNames); } } if (options.loadRelationIds === true) { qb.loadAllRelationIds(); } else if (options.loadRelationIds instanceof Object) { qb.loadAllRelationIds(options.loadRelationIds as any); } if (options.where) qb.where(options.where); if ((options as FindManyOptions).skip) qb.skip((options as FindManyOptions).skip!); if ((options as FindManyOptions).take) qb.take((options as FindManyOptions).take!); if (options.order) Object.keys(options.order).forEach(key => { const order = ((options as FindOneOptions).order as any)[key as any]; if (!metadata.findColumnWithPropertyPath(key)) throw new Error(`${key} column was not found in the ${metadata.name} entity.`); switch (order) { case 1: qb.addOrderBy(qb.alias + "." + key, "ASC"); break; case -1: qb.addOrderBy(qb.alias + "." + key, "DESC"); break; case "ASC": qb.addOrderBy(qb.alias + "." + key, "ASC"); break; case "DESC": qb.addOrderBy(qb.alias + "." + key, "DESC"); break; } }); return qb; }*/ static applyOptionsToTreeQueryBuilder( qb: SelectQueryBuilder, options?: FindTreeOptions, ): SelectQueryBuilder { if (options?.relations) { // Copy because `applyRelationsRecursively` modifies it const allRelations = [...options.relations] FindOptionsUtils.applyRelationsRecursively( qb, allRelations, qb.expressionMap.mainAlias!.name, qb.expressionMap.mainAlias!.metadata, "", ) // recursive removes found relations from allRelations array // if there are relations left in this array it means those relations were not found in the entity structure // so, we give an exception about not found relations if (allRelations.length > 0) throw new FindRelationsNotFoundError(allRelations) } return qb } // ------------------------------------------------------------------------- // Protected Static Methods // ------------------------------------------------------------------------- /** * Adds joins for all relations and sub-relations of the given relations provided in the find options. */ public static applyRelationsRecursively( qb: SelectQueryBuilder, allRelations: string[], alias: string, metadata: EntityMetadata, prefix: string, ): void { // find all relations that match given prefix let matchedBaseRelations: RelationMetadata[] = [] if (prefix) { const regexp = new RegExp("^" + prefix.replace(".", "\\.") + "\\.") matchedBaseRelations = allRelations .filter((relation) => relation.match(regexp)) .map((relation) => metadata.findRelationWithPropertyPath( relation.replace(regexp, ""), ), ) .filter((entity) => entity) as RelationMetadata[] } else { matchedBaseRelations = allRelations .map((relation) => metadata.findRelationWithPropertyPath(relation), ) .filter((entity) => entity) as RelationMetadata[] } // go through all matched relations and add join for them matchedBaseRelations.forEach((relation) => { // generate a relation alias let relationAlias: string = DriverUtils.buildAlias( qb.connection.driver, { joiner: "__" }, alias, relation.propertyPath, ) // add a join for the found relation const selection = alias + "." + relation.propertyPath if (qb.expressionMap.relationLoadStrategy === "query") { qb.concatRelationMetadata(relation) } else { qb.leftJoinAndSelect(selection, relationAlias) } // remove added relations from the allRelations array, this is needed to find all not found relations at the end allRelations.splice( allRelations.indexOf( prefix ? prefix + "." + relation.propertyPath : relation.propertyPath, ), 1, ) // try to find sub-relations let relationMetadata: EntityMetadata | undefined let relationName: string | undefined if (qb.expressionMap.relationLoadStrategy === "query") { relationMetadata = relation.inverseEntityMetadata relationName = relationAlias } else { const join = qb.expressionMap.joinAttributes.find( (join) => join.entityOrProperty === selection, ) relationMetadata = join!.metadata! relationName = join!.alias.name } if (!relationName || !relationMetadata) { throw new EntityPropertyNotFoundError( relation.propertyPath, metadata, ) } this.applyRelationsRecursively( qb, allRelations, relationName, relationMetadata, prefix ? prefix + "." + relation.propertyPath : relation.propertyPath, ) // join the eager relations of the found relation // Only supported for "join" relationLoadStrategy if (qb.expressionMap.relationLoadStrategy === "join") { const relMetadata = metadata.relations.find( (metadata) => metadata.propertyName === relation.propertyPath, ) if (relMetadata) { this.joinEagerRelations( qb, relationAlias, relMetadata.inverseEntityMetadata, ) } } }) } public static joinEagerRelations( qb: SelectQueryBuilder, alias: string, metadata: EntityMetadata, ) { metadata.eagerRelations.forEach((relation) => { // generate a relation alias let relationAlias: string = DriverUtils.buildAlias( qb.connection.driver, { joiner: "__" }, alias, relation.propertyPath, ) // add a join for the relation // Checking whether the relation wasn't joined yet. let addJoin = true // TODO: Review this validation for (const join of qb.expressionMap.joinAttributes) { if ( join.condition !== undefined || join.mapToProperty !== undefined || join.isMappingMany !== undefined || join.direction !== "LEFT" || join.entityOrProperty !== `${alias}.${relation.propertyPath}` ) { continue } addJoin = false relationAlias = join.alias.name break } const joinAlreadyAdded = Boolean( qb.expressionMap.joinAttributes.find( (joinAttribute) => joinAttribute.alias.name === relationAlias, ), ) if (addJoin && !joinAlreadyAdded) { qb.leftJoin(alias + "." + relation.propertyPath, relationAlias) } // Checking whether the relation wasn't selected yet. // This check shall be after the join check to detect relationAlias. let addSelect = true for (const select of qb.expressionMap.selects) { if ( select.aliasName !== undefined || select.virtual !== undefined || select.selection !== relationAlias ) { continue } addSelect = false break } if (addSelect) { qb.addSelect(relationAlias) } // (recursive) join the eager relations this.joinEagerRelations( qb, relationAlias, relation.inverseEntityMetadata, ) }) } }