UNPKG

typeorm

Version:

Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, MongoDB databases.

1,200 lines • 124 kB
import { RawSqlResultsToEntityTransformer } from "./transformer/RawSqlResultsToEntityTransformer"; import { PessimisticLockTransactionRequiredError } from "../error/PessimisticLockTransactionRequiredError"; import { NoVersionOrUpdateDateColumnError } from "../error/NoVersionOrUpdateDateColumnError"; import { OptimisticLockVersionMismatchError } from "../error/OptimisticLockVersionMismatchError"; import { OptimisticLockCanNotBeUsedError } from "../error/OptimisticLockCanNotBeUsedError"; import { JoinAttribute } from "./JoinAttribute"; import { RelationIdAttribute } from "./relation-id/RelationIdAttribute"; import { RelationCountAttribute } from "./relation-count/RelationCountAttribute"; import { RelationIdLoader } from "./relation-id/RelationIdLoader"; import { RelationIdLoader as QueryStrategyRelationIdLoader } from "./RelationIdLoader"; import { RelationIdMetadataToAttributeTransformer } from "./relation-id/RelationIdMetadataToAttributeTransformer"; import { RelationCountLoader } from "./relation-count/RelationCountLoader"; import { RelationCountMetadataToAttributeTransformer } from "./relation-count/RelationCountMetadataToAttributeTransformer"; import { QueryBuilder } from "./QueryBuilder"; import { LockNotSupportedOnGivenDriverError } from "../error/LockNotSupportedOnGivenDriverError"; import { OffsetWithoutLimitNotSupportedError } from "../error/OffsetWithoutLimitNotSupportedError"; import { ObjectUtils } from "../util/ObjectUtils"; import { DriverUtils } from "../driver/DriverUtils"; import { EntityNotFoundError } from "../error/EntityNotFoundError"; import { TypeORMError } from "../error"; import { FindOptionsUtils } from "../find-options/FindOptionsUtils"; import { OrmUtils } from "../util/OrmUtils"; import { EntityPropertyNotFoundError } from "../error/EntityPropertyNotFoundError"; import { InstanceChecker } from "../util/InstanceChecker"; import { FindOperator } from "../find-options/FindOperator"; import { ApplyValueTransformers } from "../util/ApplyValueTransformers"; /** * Allows to build complex sql queries in a fashion way and execute those queries. */ export class SelectQueryBuilder extends QueryBuilder { constructor() { super(...arguments); this["@instanceof"] = Symbol.for("SelectQueryBuilder"); this.findOptions = {}; this.selects = []; this.joins = []; this.conditions = ""; this.orderBys = []; this.relationMetadatas = []; } // ------------------------------------------------------------------------- // Public Implemented Methods // ------------------------------------------------------------------------- /** * Gets generated SQL query without parameters being replaced. */ getQuery() { let sql = this.createComment(); sql += this.createCteExpression(); sql += this.createSelectExpression(); sql += this.createJoinExpression(); sql += this.createWhereExpression(); sql += this.createGroupByExpression(); sql += this.createHavingExpression(); sql += this.createOrderByExpression(); sql += this.createLimitOffsetExpression(); sql += this.createLockExpression(); sql = sql.trim(); if (this.expressionMap.subQuery) sql = "(" + sql + ")"; return this.replacePropertyNamesForTheWholeQuery(sql); } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- setFindOptions(findOptions) { this.findOptions = findOptions; this.applyFindOptions(); return this; } /** * Creates a subquery - query that can be used inside other queries. */ subQuery() { const qb = this.createQueryBuilder(); qb.expressionMap.subQuery = true; qb.parentQueryBuilder = this; return qb; } /** * Creates SELECT query and selects given data. * Replaces all previous selections if they exist. */ select(selection, selectionAliasName) { this.expressionMap.queryType = "select"; if (Array.isArray(selection)) { this.expressionMap.selects = selection.map((selection) => ({ selection: selection, })); } else if (typeof selection === "function") { const subQueryBuilder = selection(this.subQuery()); this.setParameters(subQueryBuilder.getParameters()); this.expressionMap.selects.push({ selection: subQueryBuilder.getQuery(), aliasName: selectionAliasName, }); } else if (selection) { this.expressionMap.selects = [ { selection: selection, aliasName: selectionAliasName }, ]; } return this; } /** * Adds new selection to the SELECT query. */ addSelect(selection, selectionAliasName) { if (!selection) return this; if (Array.isArray(selection)) { this.expressionMap.selects = this.expressionMap.selects.concat(selection.map((selection) => ({ selection: selection }))); } else if (typeof selection === "function") { const subQueryBuilder = selection(this.subQuery()); this.setParameters(subQueryBuilder.getParameters()); this.expressionMap.selects.push({ selection: subQueryBuilder.getQuery(), aliasName: selectionAliasName, }); } else if (selection) { this.expressionMap.selects.push({ selection: selection, aliasName: selectionAliasName, }); } return this; } /** * Set max execution time. * @param milliseconds */ maxExecutionTime(milliseconds) { this.expressionMap.maxExecutionTime = milliseconds; return this; } /** * Sets whether the selection is DISTINCT. */ distinct(distinct = true) { this.expressionMap.selectDistinct = distinct; return this; } /** * Sets the distinct on clause for Postgres. */ distinctOn(distinctOn) { this.expressionMap.selectDistinctOn = distinctOn; return this; } fromDummy() { return this.from(this.connection.driver.dummyTableName ?? "(SELECT 1 AS dummy_column)", "dummy_table"); } /** * Specifies FROM which entity's table select/update/delete will be executed. * Also sets a main string alias of the selection data. * Removes all previously set from-s. */ from(entityTarget, aliasName) { const mainAlias = this.createFromAlias(entityTarget, aliasName); this.expressionMap.setMainAlias(mainAlias); return this; } /** * Specifies FROM which entity's table select/update/delete will be executed. * Also sets a main string alias of the selection data. */ addFrom(entityTarget, aliasName) { const alias = this.createFromAlias(entityTarget, aliasName); if (!this.expressionMap.mainAlias) this.expressionMap.setMainAlias(alias); return this; } /** * INNER JOINs (without selection). * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ innerJoin(entityOrProperty, alias, condition, parameters) { this.join("INNER", entityOrProperty, alias, condition, parameters); return this; } /** * LEFT JOINs (without selection). * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ leftJoin(entityOrProperty, alias, condition, parameters) { this.join("LEFT", entityOrProperty, alias, condition, parameters); return this; } /** * INNER JOINs and adds all selection properties to SELECT. * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ innerJoinAndSelect(entityOrProperty, alias, condition, parameters) { this.addSelect(alias); this.innerJoin(entityOrProperty, alias, condition, parameters); return this; } /** * LEFT JOINs and adds all selection properties to SELECT. * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ leftJoinAndSelect(entityOrProperty, alias, condition, parameters) { this.addSelect(alias); this.leftJoin(entityOrProperty, alias, condition, parameters); return this; } /** * INNER JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property. * This is extremely useful when you want to select some data and map it to some virtual property. * It will assume that there are multiple rows of selecting data, and mapped result will be an array. * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ innerJoinAndMapMany(mapToProperty, entityOrProperty, alias, condition, parameters) { this.addSelect(alias); this.join("INNER", entityOrProperty, alias, condition, parameters, mapToProperty, true); return this; } /** * INNER JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property. * This is extremely useful when you want to select some data and map it to some virtual property. * It will assume that there is a single row of selecting data, and mapped result will be a single selected value. * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ innerJoinAndMapOne(mapToProperty, entityOrProperty, alias, condition, parameters, mapAsEntity) { this.addSelect(alias); this.join("INNER", entityOrProperty, alias, condition, parameters, mapToProperty, false, mapAsEntity); return this; } /** * LEFT JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property. * This is extremely useful when you want to select some data and map it to some virtual property. * It will assume that there are multiple rows of selecting data, and mapped result will be an array. * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ leftJoinAndMapMany(mapToProperty, entityOrProperty, alias, condition, parameters) { this.addSelect(alias); this.join("LEFT", entityOrProperty, alias, condition, parameters, mapToProperty, true); return this; } /** * LEFT JOINs, SELECTs the data returned by a join and MAPs all that data to some entity's property. * This is extremely useful when you want to select some data and map it to some virtual property. * It will assume that there is a single row of selecting data, and mapped result will be a single selected value. * You also need to specify an alias of the joined data. * Optionally, you can add condition and parameters used in condition. */ leftJoinAndMapOne(mapToProperty, entityOrProperty, alias, condition, parameters, mapAsEntity) { this.addSelect(alias); this.join("LEFT", entityOrProperty, alias, condition, parameters, mapToProperty, false, mapAsEntity); return this; } /** * LEFT JOINs relation id and maps it into some entity's property. * Optionally, you can add condition and parameters used in condition. */ loadRelationIdAndMap(mapToProperty, relationName, aliasNameOrOptions, queryBuilderFactory) { const relationIdAttribute = new RelationIdAttribute(this.expressionMap); relationIdAttribute.mapToProperty = mapToProperty; relationIdAttribute.relationName = relationName; if (typeof aliasNameOrOptions === "string") relationIdAttribute.alias = aliasNameOrOptions; if (typeof aliasNameOrOptions === "object" && aliasNameOrOptions.disableMixedMap) relationIdAttribute.disableMixedMap = true; relationIdAttribute.queryBuilderFactory = queryBuilderFactory; this.expressionMap.relationIdAttributes.push(relationIdAttribute); if (relationIdAttribute.relation.junctionEntityMetadata) { this.expressionMap.createAlias({ type: "other", name: relationIdAttribute.junctionAlias, metadata: relationIdAttribute.relation.junctionEntityMetadata, }); } return this; } /** * Counts number of entities of entity's relation and maps the value into some entity's property. * Optionally, you can add condition and parameters used in condition. */ loadRelationCountAndMap(mapToProperty, relationName, aliasName, queryBuilderFactory) { const relationCountAttribute = new RelationCountAttribute(this.expressionMap); relationCountAttribute.mapToProperty = mapToProperty; relationCountAttribute.relationName = relationName; relationCountAttribute.alias = aliasName; relationCountAttribute.queryBuilderFactory = queryBuilderFactory; this.expressionMap.relationCountAttributes.push(relationCountAttribute); this.expressionMap.createAlias({ type: "other", name: relationCountAttribute.junctionAlias, }); if (relationCountAttribute.relation.junctionEntityMetadata) { this.expressionMap.createAlias({ type: "other", name: relationCountAttribute.junctionAlias, metadata: relationCountAttribute.relation.junctionEntityMetadata, }); } return this; } /** * Loads all relation ids for all relations of the selected entity. * All relation ids will be mapped to relation property themself. * If array of strings is given then loads only relation ids of the given properties. */ loadAllRelationIds(options) { // todo: add skip relations this.expressionMap.mainAlias.metadata.relations.forEach((relation) => { if (options !== undefined && options.relations !== undefined && options.relations.indexOf(relation.propertyPath) === -1) return; this.loadRelationIdAndMap(this.expressionMap.mainAlias.name + "." + relation.propertyPath, this.expressionMap.mainAlias.name + "." + relation.propertyPath, options); }); return this; } /** * Sets WHERE condition in the query builder. * If you had previously WHERE expression defined, * calling this function will override previously set WHERE conditions. * Additionally you can add parameters used in where expression. */ where(where, parameters) { this.expressionMap.wheres = []; // don't move this block below since computeWhereParameter can add where expressions const condition = this.getWhereCondition(where); if (condition) { this.expressionMap.wheres = [ { type: "simple", condition: condition }, ]; } if (parameters) this.setParameters(parameters); return this; } /** * Adds new AND WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ andWhere(where, parameters) { this.expressionMap.wheres.push({ type: "and", condition: this.getWhereCondition(where), }); if (parameters) this.setParameters(parameters); return this; } /** * Adds new OR WHERE condition in the query builder. * Additionally you can add parameters used in where expression. */ orWhere(where, parameters) { this.expressionMap.wheres.push({ type: "or", condition: this.getWhereCondition(where), }); if (parameters) this.setParameters(parameters); return this; } /** * Sets a new where EXISTS clause */ whereExists(subQuery) { return this.where(...this.getExistsCondition(subQuery)); } /** * Adds a new AND where EXISTS clause */ andWhereExists(subQuery) { return this.andWhere(...this.getExistsCondition(subQuery)); } /** * Adds a new OR where EXISTS clause */ orWhereExists(subQuery) { return this.orWhere(...this.getExistsCondition(subQuery)); } /** * Adds new AND WHERE with conditions for the given ids. * * Ids are mixed. * It means if you have single primary key you can pass a simple id values, for example [1, 2, 3]. * If you have multiple primary keys you need to pass object with property names and values specified, * for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...] */ whereInIds(ids) { return this.where(this.getWhereInIdsCondition(ids)); } /** * Adds new AND WHERE with conditions for the given ids. * * Ids are mixed. * It means if you have single primary key you can pass a simple id values, for example [1, 2, 3]. * If you have multiple primary keys you need to pass object with property names and values specified, * for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...] */ andWhereInIds(ids) { return this.andWhere(this.getWhereInIdsCondition(ids)); } /** * Adds new OR WHERE with conditions for the given ids. * * Ids are mixed. * It means if you have single primary key you can pass a simple id values, for example [1, 2, 3]. * If you have multiple primary keys you need to pass object with property names and values specified, * for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...] */ orWhereInIds(ids) { return this.orWhere(this.getWhereInIdsCondition(ids)); } /** * Sets HAVING condition in the query builder. * If you had previously HAVING expression defined, * calling this function will override previously set HAVING conditions. * Additionally you can add parameters used in where expression. */ having(having, parameters) { this.expressionMap.havings.push({ type: "simple", condition: having }); if (parameters) this.setParameters(parameters); return this; } /** * Adds new AND HAVING condition in the query builder. * Additionally you can add parameters used in where expression. */ andHaving(having, parameters) { this.expressionMap.havings.push({ type: "and", condition: having }); if (parameters) this.setParameters(parameters); return this; } /** * Adds new OR HAVING condition in the query builder. * Additionally you can add parameters used in where expression. */ orHaving(having, parameters) { this.expressionMap.havings.push({ type: "or", condition: having }); if (parameters) this.setParameters(parameters); return this; } /** * Sets GROUP BY condition in the query builder. * If you had previously GROUP BY expression defined, * calling this function will override previously set GROUP BY conditions. */ groupBy(groupBy) { if (groupBy) { this.expressionMap.groupBys = [groupBy]; } else { this.expressionMap.groupBys = []; } return this; } /** * Adds GROUP BY condition in the query builder. */ addGroupBy(groupBy) { this.expressionMap.groupBys.push(groupBy); return this; } /** * Enables time travelling for the current query (only supported by cockroach currently) */ timeTravelQuery(timeTravelFn) { if (this.connection.driver.options.type === "cockroachdb") { if (timeTravelFn === undefined) { this.expressionMap.timeTravel = "follower_read_timestamp()"; } else { this.expressionMap.timeTravel = timeTravelFn; } } return this; } /** * Sets ORDER BY condition in the query builder. * If you had previously ORDER BY expression defined, * calling this function will override previously set ORDER BY conditions. */ orderBy(sort, order = "ASC", nulls) { if (order !== undefined && order !== "ASC" && order !== "DESC") throw new TypeORMError(`SelectQueryBuilder.addOrderBy "order" can accept only "ASC" and "DESC" values.`); if (nulls !== undefined && nulls !== "NULLS FIRST" && nulls !== "NULLS LAST") throw new TypeORMError(`SelectQueryBuilder.addOrderBy "nulls" can accept only "NULLS FIRST" and "NULLS LAST" values.`); if (sort) { if (typeof sort === "object") { this.expressionMap.orderBys = sort; } else { if (nulls) { this.expressionMap.orderBys = { [sort]: { order, nulls }, }; } else { this.expressionMap.orderBys = { [sort]: order }; } } } else { this.expressionMap.orderBys = {}; } return this; } /** * Adds ORDER BY condition in the query builder. */ addOrderBy(sort, order = "ASC", nulls) { if (order !== undefined && order !== "ASC" && order !== "DESC") throw new TypeORMError(`SelectQueryBuilder.addOrderBy "order" can accept only "ASC" and "DESC" values.`); if (nulls !== undefined && nulls !== "NULLS FIRST" && nulls !== "NULLS LAST") throw new TypeORMError(`SelectQueryBuilder.addOrderBy "nulls" can accept only "NULLS FIRST" and "NULLS LAST" values.`); if (nulls) { this.expressionMap.orderBys[sort] = { order, nulls }; } else { this.expressionMap.orderBys[sort] = order; } return this; } /** * Sets LIMIT - maximum number of rows to be selected. * NOTE that it may not work as you expect if you are using joins. * If you want to implement pagination, and you are having join in your query, * then use the take method instead. */ limit(limit) { this.expressionMap.limit = this.normalizeNumber(limit); if (this.expressionMap.limit !== undefined && isNaN(this.expressionMap.limit)) throw new TypeORMError(`Provided "limit" value is not a number. Please provide a numeric value.`); return this; } /** * Sets OFFSET - selection offset. * NOTE that it may not work as you expect if you are using joins. * If you want to implement pagination, and you are having join in your query, * then use the skip method instead. */ offset(offset) { this.expressionMap.offset = this.normalizeNumber(offset); if (this.expressionMap.offset !== undefined && isNaN(this.expressionMap.offset)) throw new TypeORMError(`Provided "offset" value is not a number. Please provide a numeric value.`); return this; } /** * Sets maximal number of entities to take. */ take(take) { this.expressionMap.take = this.normalizeNumber(take); if (this.expressionMap.take !== undefined && isNaN(this.expressionMap.take)) throw new TypeORMError(`Provided "take" value is not a number. Please provide a numeric value.`); return this; } /** * Sets number of entities to skip. */ skip(skip) { this.expressionMap.skip = this.normalizeNumber(skip); if (this.expressionMap.skip !== undefined && isNaN(this.expressionMap.skip)) throw new TypeORMError(`Provided "skip" value is not a number. Please provide a numeric value.`); return this; } /** * Set certain index to be used by the query. * * @param index Name of index to be used. */ useIndex(index) { this.expressionMap.useIndex = index; return this; } /** * Sets locking mode. */ setLock(lockMode, lockVersion, lockTables) { this.expressionMap.lockMode = lockMode; this.expressionMap.lockVersion = lockVersion; this.expressionMap.lockTables = lockTables; return this; } /** * Sets lock handling by adding NO WAIT or SKIP LOCKED. */ setOnLocked(onLocked) { this.expressionMap.onLocked = onLocked; return this; } /** * Disables the global condition of "non-deleted" for the entity with delete date columns. */ withDeleted() { this.expressionMap.withDeleted = true; return this; } /** * Gets first raw result returned by execution of generated query builder sql. */ async getRawOne() { return (await this.getRawMany())[0]; } /** * Gets all raw results returned by execution of generated query builder sql. */ async getRawMany() { if (this.expressionMap.lockMode === "optimistic") throw new OptimisticLockCanNotBeUsedError(); this.expressionMap.queryEntity = false; const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } const results = await this.loadRawResults(queryRunner); // close transaction if we started it if (transactionStartedByUs) { await queryRunner.commitTransaction(); } return results; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } finally { if (queryRunner !== this.queryRunner) { // means we created our own query runner await queryRunner.release(); } } } /** * Executes sql generated by query builder and returns object with raw results and entities created from them. */ async getRawAndEntities() { const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } this.expressionMap.queryEntity = true; const results = await this.executeEntitiesAndRawResults(queryRunner); // close transaction if we started it if (transactionStartedByUs) { await queryRunner.commitTransaction(); } return results; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } finally { if (queryRunner !== this.queryRunner) // means we created our own query runner await queryRunner.release(); } } /** * Gets single entity returned by execution of generated query builder sql. */ async getOne() { const results = await this.getRawAndEntities(); const result = results.entities[0]; if (result && this.expressionMap.lockMode === "optimistic" && this.expressionMap.lockVersion) { const metadata = this.expressionMap.mainAlias.metadata; if (this.expressionMap.lockVersion instanceof Date) { const actualVersion = metadata.updateDateColumn.getEntityValue(result); // what if columns arent set? if (actualVersion.getTime() !== this.expressionMap.lockVersion.getTime()) throw new OptimisticLockVersionMismatchError(metadata.name, this.expressionMap.lockVersion, actualVersion); } else { const actualVersion = metadata.versionColumn.getEntityValue(result); // what if columns arent set? if (actualVersion !== this.expressionMap.lockVersion) throw new OptimisticLockVersionMismatchError(metadata.name, this.expressionMap.lockVersion, actualVersion); } } if (result === undefined) { return null; } return result; } /** * Gets the first entity returned by execution of generated query builder sql or rejects the returned promise on error. */ async getOneOrFail() { const entity = await this.getOne(); if (!entity) { throw new EntityNotFoundError(this.expressionMap.mainAlias.target, this.expressionMap.parameters); } return entity; } /** * Gets entities returned by execution of generated query builder sql. */ async getMany() { if (this.expressionMap.lockMode === "optimistic") throw new OptimisticLockCanNotBeUsedError(); const results = await this.getRawAndEntities(); return results.entities; } /** * Gets count - number of entities selected by sql generated by this query builder. * Count excludes all limitations set by offset, limit, skip, and take. */ async getCount() { if (this.expressionMap.lockMode === "optimistic") throw new OptimisticLockCanNotBeUsedError(); const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } this.expressionMap.queryEntity = false; const results = await this.executeCountQuery(queryRunner); // close transaction if we started it if (transactionStartedByUs) { await queryRunner.commitTransaction(); } return results; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } finally { if (queryRunner !== this.queryRunner) // means we created our own query runner await queryRunner.release(); } } /** * Gets exists * Returns whether any rows exists matching current query. */ async getExists() { if (this.expressionMap.lockMode === "optimistic") throw new OptimisticLockCanNotBeUsedError(); const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } this.expressionMap.queryEntity = false; const results = await this.executeExistsQuery(queryRunner); // close transaction if we started it if (transactionStartedByUs) { await queryRunner.commitTransaction(); } return results; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } finally { if (queryRunner !== this.queryRunner) // means we created our own query runner await queryRunner.release(); } } /** * Executes built SQL query and returns entities and overall entities count (without limitation). * This method is useful to build pagination. */ async getManyAndCount() { if (this.expressionMap.lockMode === "optimistic") throw new OptimisticLockCanNotBeUsedError(); const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } this.expressionMap.queryEntity = true; const entitiesAndRaw = await this.executeEntitiesAndRawResults(queryRunner); this.expressionMap.queryEntity = false; const cacheId = this.expressionMap.cacheId; // Creates a new cacheId for the count query, or it will retreive the above query results // and count will return 0. this.expressionMap.cacheId = cacheId ? `${cacheId}-count` : cacheId; const count = await this.executeCountQuery(queryRunner); const results = [entitiesAndRaw.entities, count]; // close transaction if we started it if (transactionStartedByUs) { await queryRunner.commitTransaction(); } return results; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } finally { if (queryRunner !== this.queryRunner) // means we created our own query runner await queryRunner.release(); } } /** * Executes built SQL query and returns raw data stream. */ async stream() { this.expressionMap.queryEntity = false; const [sql, parameters] = this.getQueryAndParameters(); const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } const releaseFn = () => { if (queryRunner !== this.queryRunner) // means we created our own query runner return queryRunner.release(); return; }; const results = queryRunner.stream(sql, parameters, releaseFn, releaseFn); // close transaction if we started it if (transactionStartedByUs) { await queryRunner.commitTransaction(); } return results; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } } /** * Enables or disables query result caching. */ cache(enabledOrMillisecondsOrId, maybeMilliseconds) { if (typeof enabledOrMillisecondsOrId === "boolean") { this.expressionMap.cache = enabledOrMillisecondsOrId; } else if (typeof enabledOrMillisecondsOrId === "number") { this.expressionMap.cache = true; this.expressionMap.cacheDuration = enabledOrMillisecondsOrId; } else if (typeof enabledOrMillisecondsOrId === "string" || typeof enabledOrMillisecondsOrId === "number") { this.expressionMap.cache = true; this.expressionMap.cacheId = enabledOrMillisecondsOrId; } if (maybeMilliseconds) { this.expressionMap.cacheDuration = maybeMilliseconds; } return this; } /** * Sets extra options that can be used to configure how query builder works. */ setOption(option) { this.expressionMap.options.push(option); return this; } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- join(direction, entityOrProperty, aliasName, condition, parameters, mapToProperty, isMappingMany, mapAsEntity) { if (parameters) { this.setParameters(parameters); } const joinAttribute = new JoinAttribute(this.connection, this.expressionMap); joinAttribute.direction = direction; joinAttribute.mapAsEntity = mapAsEntity; joinAttribute.mapToProperty = mapToProperty; joinAttribute.isMappingMany = isMappingMany; joinAttribute.entityOrProperty = entityOrProperty; // relationName joinAttribute.condition = condition; // joinInverseSideCondition // joinAttribute.junctionAlias = joinAttribute.relation.isOwning ? parentAlias + "_" + destinationTableAlias : destinationTableAlias + "_" + parentAlias; this.expressionMap.joinAttributes.push(joinAttribute); const joinAttributeMetadata = joinAttribute.metadata; if (joinAttributeMetadata) { if (joinAttributeMetadata.deleteDateColumn && !this.expressionMap.withDeleted) { const conditionDeleteColumn = `${aliasName}.${joinAttributeMetadata.deleteDateColumn.propertyName} IS NULL`; joinAttribute.condition = joinAttribute.condition ? ` ${joinAttribute.condition} AND ${conditionDeleteColumn}` : `${conditionDeleteColumn}`; } // todo: find and set metadata right there? joinAttribute.alias = this.expressionMap.createAlias({ type: "join", name: aliasName, metadata: joinAttributeMetadata, }); if (joinAttribute.relation && joinAttribute.relation.junctionEntityMetadata) { this.expressionMap.createAlias({ type: "join", name: joinAttribute.junctionAlias, metadata: joinAttribute.relation.junctionEntityMetadata, }); } } else { let subQuery = ""; if (typeof entityOrProperty === "function") { const subQueryBuilder = entityOrProperty(this.subQuery()); this.setParameters(subQueryBuilder.getParameters()); subQuery = subQueryBuilder.getQuery(); } else { subQuery = entityOrProperty; } const isSubQuery = typeof entityOrProperty === "function" || (entityOrProperty.substr(0, 1) === "(" && entityOrProperty.substr(-1) === ")"); joinAttribute.alias = this.expressionMap.createAlias({ type: "join", name: aliasName, tablePath: isSubQuery === false ? entityOrProperty : undefined, subQuery: isSubQuery === true ? subQuery : undefined, }); } } /** * Creates "SELECT FROM" part of SQL query. */ createSelectExpression() { if (!this.expressionMap.mainAlias) throw new TypeORMError("Cannot build query because main alias is not set (call qb#from method)"); // todo throw exception if selects or from is missing const allSelects = []; const excludedSelects = []; if (this.expressionMap.mainAlias.hasMetadata) { const metadata = this.expressionMap.mainAlias.metadata; allSelects.push(...this.buildEscapedEntityColumnSelects(this.expressionMap.mainAlias.name, metadata)); excludedSelects.push(...this.findEntityColumnSelects(this.expressionMap.mainAlias.name, metadata)); } // add selects from joins this.expressionMap.joinAttributes.forEach((join) => { if (join.metadata) { allSelects.push(...this.buildEscapedEntityColumnSelects(join.alias.name, join.metadata)); excludedSelects.push(...this.findEntityColumnSelects(join.alias.name, join.metadata)); } else { const hasMainAlias = this.expressionMap.selects.some((select) => select.selection === join.alias.name); if (hasMainAlias) { allSelects.push({ selection: this.escape(join.alias.name) + ".*", }); const excludedSelect = this.expressionMap.selects.find((select) => select.selection === join.alias.name); excludedSelects.push(excludedSelect); } } }); // add all other selects this.expressionMap.selects .filter((select) => excludedSelects.indexOf(select) === -1) .forEach((select) => allSelects.push({ selection: this.replacePropertyNames(select.selection), aliasName: select.aliasName, })); // if still selection is empty, then simply set it to all (*) if (allSelects.length === 0) allSelects.push({ selection: "*" }); // Use certain index let useIndex = ""; if (this.expressionMap.useIndex) { if (DriverUtils.isMySQLFamily(this.connection.driver)) { useIndex = ` USE INDEX (${this.expressionMap.useIndex})`; } } // create a selection query const froms = this.expressionMap.aliases .filter((alias) => alias.type === "from" && (alias.tablePath || alias.subQuery)) .map((alias) => { if (alias.subQuery) return alias.subQuery + " " + this.escape(alias.name); return (this.getTableName(alias.tablePath) + " " + this.escape(alias.name)); }); const select = this.createSelectDistinctExpression(); const selection = allSelects .map((select) => select.selection + (select.aliasName ? " AS " + this.escape(select.aliasName) : "")) .join(", "); return (select + selection + " FROM " + froms.join(", ") + this.createTableLockExpression() + useIndex); } /** * Creates select | select distinct part of SQL query. */ createSelectDistinctExpression() { const { selectDistinct, selectDistinctOn, maxExecutionTime } = this.expressionMap; const { driver } = this.connection; let select = "SELECT "; if (maxExecutionTime > 0) { if (DriverUtils.isMySQLFamily(driver)) { select += `/*+ MAX_EXECUTION_TIME(${this.expressionMap.maxExecutionTime}) */ `; } } if (DriverUtils.isPostgresFamily(driver) && selectDistinctOn.length > 0) { const selectDistinctOnMap = selectDistinctOn .map((on) => this.replacePropertyNames(on)) .join(", "); select = `SELECT DISTINCT ON (${selectDistinctOnMap}) `; } else if (selectDistinct) { select = "SELECT DISTINCT "; } return select; } /** * Creates "JOIN" part of SQL query. */ createJoinExpression() { // examples: // select from owning side // qb.select("post") // .leftJoinAndSelect("post.category", "category"); // select from non-owning side // qb.select("category") // .leftJoinAndSelect("category.post", "post"); const joins = this.expressionMap.joinAttributes.map((joinAttr) => { const relation = joinAttr.relation; const destinationTableName = joinAttr.tablePath; const destinationTableAlias = joinAttr.alias.name; let appendedCondition = joinAttr.condition ? " AND (" + joinAttr.condition + ")" : ""; const parentAlias = joinAttr.parentAlias; // if join was build without relation (e.g. without "post.category") then it means that we have direct // table to join, without junction table involved. This means we simply join direct table. if (!parentAlias || !relation) { const destinationJoin = joinAttr.alias.subQuery ? joinAttr.alias.subQuery : this.getTableName(destinationTableName); return (" " + joinAttr.direction + " JOIN " + destinationJoin + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() + (joinAttr.condition ? " ON " + this.replacePropertyNames(joinAttr.condition) : "")); } // if real entity relation is involved if (relation.isManyToOne || relation.isOneToOneOwner) { // JOIN `category` `category` ON `category`.`id` = `post`.`categoryId` const condition = relation.joinColumns .map((joinColumn) => { return (destinationTableAlias + "." + joinColumn.referencedColumn.propertyPath + "=" + parentAlias + "." + relation.propertyPath + "." + joinColumn.referencedColumn.propertyPath); }) .join(" AND "); return (" " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() + " ON " + this.replacePropertyNames(condition + appendedCondition)); } else if (relation.isOneToMany || relation.isOneToOneNotOwner) { // JOIN `post` `post` ON `post`.`categoryId` = `category`.`id` const condition = relation .inverseRelation.joinColumns.map((joinColumn) => { if (relation.inverseEntityMetadata.tableType === "entity-child" && relation.inverseEntityMetadata.discriminatorColumn) { appendedCondition += " AND " + destinationTableAlias + "." + relation.inverseEntityMetadata .discriminatorColumn.databaseName + "='" + relation.inverseEntityMetadata .discriminatorValue + "'"; } return (destinationTableAlias + "." + relation.inverseRelation.propertyPath + "." + joinColumn.referencedColumn.propertyPath + "=" + parentAlias + "." + joinColumn.referencedColumn.propertyPath); }) .join(" AND "); if (!condition) throw new TypeORMError(`Relation ${relation.entityMetadata.name}.${relation.propertyName} does not have join columns.`); return (" " + joinAttr.direction + " JOIN " + this.getTable