typeorm
Version:
Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, MongoDB databases.
1,137 lines (1,135 loc) • 49.5 kB
JavaScript
import { QueryExpressionMap } from "./QueryExpressionMap";
import { Brackets } from "./Brackets";
import { FindOperator } from "../find-options/FindOperator";
import { In } from "../find-options/operator/In";
import { TypeORMError } from "../error";
import { EntityPropertyNotFoundError } from "../error/EntityPropertyNotFoundError";
import { InstanceChecker } from "../util/InstanceChecker";
import { escapeRegExp } from "../util/escapeRegExp";
// todo: completely cover query builder with tests
// todo: entityOrProperty can be target name. implement proper behaviour if it is.
// todo: check in persistment if id exist on object and throw exception (can be in partial selection?)
// todo: fix problem with long aliases eg getMaxIdentifierLength
// todo: fix replacing in .select("COUNT(post.id) AS cnt") statement
// todo: implement joinAlways in relations and relationId
// todo: finish partial selection
// todo: sugar methods like: .addCount and .selectCount, selectCountAndMap, selectSum, selectSumAndMap, ...
// todo: implement @Select decorator
// todo: add select and map functions
// todo: implement relation/entity loading and setting them into properties within a separate query
// .loadAndMap("post.categories", "post.categories", qb => ...)
// .loadAndMap("post.categories", Category, qb => ...)
/**
* Allows to build complex sql queries in a fashion way and execute those queries.
*/
export class QueryBuilder {
/**
* QueryBuilder can be initialized from given Connection and QueryRunner objects or from given other QueryBuilder.
*/
constructor(connectionOrQueryBuilder, queryRunner) {
this["@instanceof"] = Symbol.for("QueryBuilder");
/**
* Memo to help keep place of current parameter index for `createParameter`
*/
this.parameterIndex = 0;
if (InstanceChecker.isDataSource(connectionOrQueryBuilder)) {
this.connection = connectionOrQueryBuilder;
this.queryRunner = queryRunner;
this.expressionMap = new QueryExpressionMap(this.connection);
}
else {
this.connection = connectionOrQueryBuilder.connection;
this.queryRunner = connectionOrQueryBuilder.queryRunner;
this.expressionMap = connectionOrQueryBuilder.expressionMap.clone();
}
}
static registerQueryBuilderClass(name, factory) {
QueryBuilder.queryBuilderRegistry[name] = factory;
}
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
/**
* Gets the main alias string used in this query builder.
*/
get alias() {
if (!this.expressionMap.mainAlias)
throw new TypeORMError(`Main alias is not set`); // todo: better exception
return this.expressionMap.mainAlias.name;
}
/**
* 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 (selection) {
this.expressionMap.selects = [
{ selection: selection, aliasName: selectionAliasName },
];
}
if (InstanceChecker.isSelectQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["SelectQueryBuilder"](this);
}
/**
* Creates INSERT query.
*/
insert() {
this.expressionMap.queryType = "insert";
if (InstanceChecker.isInsertQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["InsertQueryBuilder"](this);
}
/**
* Creates UPDATE query and applies given update values.
*/
update(entityOrTableNameUpdateSet, maybeUpdateSet) {
const updateSet = maybeUpdateSet
? maybeUpdateSet
: entityOrTableNameUpdateSet;
entityOrTableNameUpdateSet = InstanceChecker.isEntitySchema(entityOrTableNameUpdateSet)
? entityOrTableNameUpdateSet.options.name
: entityOrTableNameUpdateSet;
if (typeof entityOrTableNameUpdateSet === "function" ||
typeof entityOrTableNameUpdateSet === "string") {
const mainAlias = this.createFromAlias(entityOrTableNameUpdateSet);
this.expressionMap.setMainAlias(mainAlias);
}
this.expressionMap.queryType = "update";
this.expressionMap.valuesSet = updateSet;
if (InstanceChecker.isUpdateQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["UpdateQueryBuilder"](this);
}
/**
* Creates DELETE query.
*/
delete() {
this.expressionMap.queryType = "delete";
if (InstanceChecker.isDeleteQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["DeleteQueryBuilder"](this);
}
softDelete() {
this.expressionMap.queryType = "soft-delete";
if (InstanceChecker.isSoftDeleteQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["SoftDeleteQueryBuilder"](this);
}
restore() {
this.expressionMap.queryType = "restore";
if (InstanceChecker.isSoftDeleteQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["SoftDeleteQueryBuilder"](this);
}
/**
* Sets entity's relation with which this query builder gonna work.
*/
relation(entityTargetOrPropertyPath, maybePropertyPath) {
const entityTarget = arguments.length === 2 ? entityTargetOrPropertyPath : undefined;
const propertyPath = arguments.length === 2
? maybePropertyPath
: entityTargetOrPropertyPath;
this.expressionMap.queryType = "relation";
this.expressionMap.relationPropertyPath = propertyPath;
if (entityTarget) {
const mainAlias = this.createFromAlias(entityTarget);
this.expressionMap.setMainAlias(mainAlias);
}
if (InstanceChecker.isRelationQueryBuilder(this))
return this;
return QueryBuilder.queryBuilderRegistry["RelationQueryBuilder"](this);
}
/**
* Checks if given relation or relations exist in the entity.
* Returns true if relation exists, false otherwise.
*
* todo: move this method to manager? or create a shortcut?
*/
hasRelation(target, relation) {
const entityMetadata = this.connection.getMetadata(target);
const relations = Array.isArray(relation) ? relation : [relation];
return relations.every((relation) => {
return !!entityMetadata.findRelationWithPropertyPath(relation);
});
}
/**
* Check the existence of a parameter for this query builder.
*/
hasParameter(key) {
return (this.parentQueryBuilder?.hasParameter(key) ||
key in this.expressionMap.parameters);
}
/**
* Sets parameter name and its value.
*
* The key for this parameter may contain numbers, letters, underscores, or periods.
*/
setParameter(key, value) {
if (typeof value === "function") {
throw new TypeORMError(`Function parameter isn't supported in the parameters. Please check "${key}" parameter.`);
}
if (!key.match(/^([A-Za-z0-9_.]+)$/)) {
throw new TypeORMError("QueryBuilder parameter keys may only contain numbers, letters, underscores, or periods.");
}
if (this.parentQueryBuilder) {
this.parentQueryBuilder.setParameter(key, value);
}
this.expressionMap.parameters[key] = value;
return this;
}
/**
* Adds all parameters from the given object.
*/
setParameters(parameters) {
for (const [key, value] of Object.entries(parameters)) {
this.setParameter(key, value);
}
return this;
}
createParameter(value) {
let parameterName;
do {
parameterName = `orm_param_${this.parameterIndex++}`;
} while (this.hasParameter(parameterName));
this.setParameter(parameterName, value);
return `:${parameterName}`;
}
/**
* Adds native parameters from the given object.
*
* @deprecated Use `setParameters` instead
*/
setNativeParameters(parameters) {
// set parent query builder parameters as well in sub-query mode
if (this.parentQueryBuilder) {
this.parentQueryBuilder.setNativeParameters(parameters);
}
Object.keys(parameters).forEach((key) => {
this.expressionMap.nativeParameters[key] = parameters[key];
});
return this;
}
/**
* Gets all parameters.
*/
getParameters() {
const parameters = Object.assign({}, this.expressionMap.parameters);
// add discriminator column parameter if it exist
if (this.expressionMap.mainAlias &&
this.expressionMap.mainAlias.hasMetadata) {
const metadata = this.expressionMap.mainAlias.metadata;
if (metadata.discriminatorColumn && metadata.parentEntityMetadata) {
const values = metadata.childEntityMetadatas
.filter((childMetadata) => childMetadata.discriminatorColumn)
.map((childMetadata) => childMetadata.discriminatorValue);
values.push(metadata.discriminatorValue);
parameters["discriminatorColumnValues"] = values;
}
}
return parameters;
}
/**
* Prints sql to stdout using console.log.
*/
printSql() {
// TODO rename to logSql()
const [query, parameters] = this.getQueryAndParameters();
this.connection.logger.logQuery(query, parameters);
return this;
}
/**
* Gets generated sql that will be executed.
* Parameters in the query are escaped for the currently used driver.
*/
getSql() {
return this.getQueryAndParameters()[0];
}
/**
* Gets query to be executed with all parameters used in it.
*/
getQueryAndParameters() {
// this execution order is important because getQuery method generates this.expressionMap.nativeParameters values
const query = this.getQuery();
const parameters = this.getParameters();
return this.connection.driver.escapeQueryWithParameters(query, parameters, this.expressionMap.nativeParameters);
}
/**
* Executes sql generated by query builder and returns raw database results.
*/
async execute() {
const [sql, parameters] = this.getQueryAndParameters();
const queryRunner = this.obtainQueryRunner();
try {
return await queryRunner.query(sql, parameters); // await is needed here because we are using finally
}
finally {
if (queryRunner !== this.queryRunner) {
// means we created our own query runner
await queryRunner.release();
}
}
}
/**
* Creates a completely new query builder.
* Uses same query runner as current QueryBuilder.
*/
createQueryBuilder(queryRunner) {
return new this.constructor(this.connection, queryRunner ?? this.queryRunner);
}
/**
* Clones query builder as it is.
* Note: it uses new query runner, if you want query builder that uses exactly same query runner,
* you can create query builder using its constructor, for example new SelectQueryBuilder(queryBuilder)
* where queryBuilder is cloned QueryBuilder.
*/
clone() {
return new this.constructor(this);
}
/**
* Includes a Query comment in the query builder. This is helpful for debugging purposes,
* such as finding a specific query in the database server's logs, or for categorization using
* an APM product.
*/
comment(comment) {
this.expressionMap.comment = comment;
return this;
}
/**
* Disables escaping.
*/
disableEscaping() {
this.expressionMap.disableEscaping = false;
return this;
}
/**
* Escapes table name, column name or alias name using current database's escaping character.
*/
escape(name) {
if (!this.expressionMap.disableEscaping)
return name;
return this.connection.driver.escape(name);
}
/**
* Sets or overrides query builder's QueryRunner.
*/
setQueryRunner(queryRunner) {
this.queryRunner = queryRunner;
return this;
}
/**
* Indicates if listeners and subscribers must be called before and after query execution.
* Enabled by default.
*/
callListeners(enabled) {
this.expressionMap.callListeners = enabled;
return this;
}
/**
* If set to true the query will be wrapped into a transaction.
*/
useTransaction(enabled) {
this.expressionMap.useTransaction = enabled;
return this;
}
/**
* Adds CTE to query
*/
addCommonTableExpression(queryBuilder, alias, options) {
this.expressionMap.commonTableExpressions.push({
queryBuilder,
alias,
options: options || {},
});
return this;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Gets escaped table name with schema name if SqlServer driver used with custom
* schema name, otherwise returns escaped table name.
*/
getTableName(tablePath) {
return tablePath
.split(".")
.map((i) => {
// this condition need because in SQL Server driver when custom database name was specified and schema name was not, we got `dbName..tableName` string, and doesn't need to escape middle empty string
if (i === "")
return i;
return this.escape(i);
})
.join(".");
}
/**
* Gets name of the table where insert should be performed.
*/
getMainTableName() {
if (!this.expressionMap.mainAlias)
throw new TypeORMError(`Entity where values should be inserted is not specified. Call "qb.into(entity)" method to specify it.`);
if (this.expressionMap.mainAlias.hasMetadata)
return this.expressionMap.mainAlias.metadata.tablePath;
return this.expressionMap.mainAlias.tablePath;
}
/**
* Specifies FROM which entity's table select/update/delete will be executed.
* Also sets a main string alias of the selection data.
*/
createFromAlias(entityTarget, aliasName) {
// if table has a metadata then find it to properly escape its properties
// const metadata = this.connection.entityMetadatas.find(metadata => metadata.tableName === tableName);
if (this.connection.hasMetadata(entityTarget)) {
const metadata = this.connection.getMetadata(entityTarget);
return this.expressionMap.createAlias({
type: "from",
name: aliasName,
metadata: this.connection.getMetadata(entityTarget),
tablePath: metadata.tablePath,
});
}
else {
if (typeof entityTarget === "string") {
const isSubquery = entityTarget.substr(0, 1) === "(" &&
entityTarget.substr(-1) === ")";
return this.expressionMap.createAlias({
type: "from",
name: aliasName,
tablePath: !isSubquery
? entityTarget
: undefined,
subQuery: isSubquery ? entityTarget : undefined,
});
}
const subQueryBuilder = entityTarget(this.subQuery());
this.setParameters(subQueryBuilder.getParameters());
const subquery = subQueryBuilder.getQuery();
return this.expressionMap.createAlias({
type: "from",
name: aliasName,
subQuery: subquery,
});
}
}
/**
* @deprecated this way of replace property names is too slow.
* Instead, we'll replace property names at the end - once query is build.
*/
replacePropertyNames(statement) {
return statement;
}
/**
* Replaces all entity's propertyName to name in the given SQL string.
*/
replacePropertyNamesForTheWholeQuery(statement) {
const replacements = {};
for (const alias of this.expressionMap.aliases) {
if (!alias.hasMetadata)
continue;
const replaceAliasNamePrefix = this.expressionMap.aliasNamePrefixingEnabled && alias.name
? `${alias.name}.`
: "";
if (!replacements[replaceAliasNamePrefix]) {
replacements[replaceAliasNamePrefix] = {};
}
// Insert & overwrite the replacements from least to most relevant in our replacements object.
// To do this we iterate and overwrite in the order of relevance.
// Least to Most Relevant:
// * Relation Property Path to first join column key
// * Relation Property Path + Column Path
// * Column Database Name
// * Column Property Name
// * Column Property Path
for (const relation of alias.metadata.relations) {
if (relation.joinColumns.length > 0)
replacements[replaceAliasNamePrefix][relation.propertyPath] = relation.joinColumns[0].databaseName;
}
for (const relation of alias.metadata.relations) {
const allColumns = [
...relation.joinColumns,
...relation.inverseJoinColumns,
];
for (const joinColumn of allColumns) {
const propertyKey = `${relation.propertyPath}.${joinColumn.referencedColumn.propertyPath}`;
replacements[replaceAliasNamePrefix][propertyKey] =
joinColumn.databaseName;
}
}
for (const column of alias.metadata.columns) {
replacements[replaceAliasNamePrefix][column.databaseName] =
column.databaseName;
}
for (const column of alias.metadata.columns) {
replacements[replaceAliasNamePrefix][column.propertyName] =
column.databaseName;
}
for (const column of alias.metadata.columns) {
replacements[replaceAliasNamePrefix][column.propertyPath] =
column.databaseName;
}
}
const replacementKeys = Object.keys(replacements);
const replaceAliasNamePrefixes = replacementKeys
.map((key) => escapeRegExp(key))
.join("|");
if (replacementKeys.length > 0) {
statement = statement.replace(new RegExp(
// Avoid a lookbehind here since it's not well supported
`([ =(]|^.{0})` + // any of ' =(' or start of line
// followed by our prefix, e.g. 'tablename.' or ''
`${replaceAliasNamePrefixes
? "(" + replaceAliasNamePrefixes + ")"
: ""}([^ =(),]+)` + // a possible property name: sequence of anything but ' =(),'
// terminated by ' =),' or end of line
`(?=[ =),]|.{0}$)`, "gm"), (...matches) => {
let match, pre, p;
if (replaceAliasNamePrefixes) {
match = matches[0];
pre = matches[1];
p = matches[3];
if (replacements[matches[2]][p]) {
return `${pre}${this.escape(matches[2].substring(0, matches[2].length - 1))}.${this.escape(replacements[matches[2]][p])}`;
}
}
else {
match = matches[0];
pre = matches[1];
p = matches[2];
if (replacements[""][p]) {
return `${pre}${this.escape(replacements[""][p])}`;
}
}
return match;
});
}
return statement;
}
createComment() {
if (!this.expressionMap.comment) {
return "";
}
// ANSI SQL 2003 support C style comments - comments that start with `/*` and end with `*/`
// In some dialects query nesting is available - but not all. Because of this, we'll need
// to scrub "ending" characters from the SQL but otherwise we can leave everything else
// as-is and it should be valid.
return `/* ${this.expressionMap.comment.replace(/\*\//g, "")} */ `;
}
/**
* Time travel queries for CockroachDB
*/
createTimeTravelQuery() {
if (this.expressionMap.queryType === "select" &&
this.expressionMap.timeTravel) {
return ` AS OF SYSTEM TIME ${this.expressionMap.timeTravel}`;
}
return "";
}
/**
* Creates "WHERE" expression.
*/
createWhereExpression() {
const conditionsArray = [];
const whereExpression = this.createWhereClausesExpression(this.expressionMap.wheres);
if (whereExpression.length > 0 && whereExpression !== "1=1") {
conditionsArray.push(this.replacePropertyNames(whereExpression));
}
if (this.expressionMap.mainAlias.hasMetadata) {
const metadata = this.expressionMap.mainAlias.metadata;
// Adds the global condition of "non-deleted" for the entity with delete date columns in select query.
if (this.expressionMap.queryType === "select" &&
!this.expressionMap.withDeleted &&
metadata.deleteDateColumn) {
const column = this.expressionMap.aliasNamePrefixingEnabled
? this.expressionMap.mainAlias.name +
"." +
metadata.deleteDateColumn.propertyName
: metadata.deleteDateColumn.propertyName;
const condition = `${this.replacePropertyNames(column)} IS NULL`;
conditionsArray.push(condition);
}
if (metadata.discriminatorColumn && metadata.parentEntityMetadata) {
const column = this.expressionMap.aliasNamePrefixingEnabled
? this.expressionMap.mainAlias.name +
"." +
metadata.discriminatorColumn.databaseName
: metadata.discriminatorColumn.databaseName;
const condition = `${this.replacePropertyNames(column)} IN (:...discriminatorColumnValues)`;
conditionsArray.push(condition);
}
}
if (this.expressionMap.extraAppendedAndWhereCondition) {
const condition = this.replacePropertyNames(this.expressionMap.extraAppendedAndWhereCondition);
conditionsArray.push(condition);
}
let condition = "";
// time travel
condition += this.createTimeTravelQuery();
if (!conditionsArray.length) {
condition += "";
}
else if (conditionsArray.length === 1) {
condition += ` WHERE ${conditionsArray[0]}`;
}
else {
condition += ` WHERE ( ${conditionsArray.join(" ) AND ( ")} )`;
}
return condition;
}
/**
* Creates "RETURNING" / "OUTPUT" expression.
*/
createReturningExpression(returningType) {
const columns = this.getReturningColumns();
const driver = this.connection.driver;
// also add columns we must auto-return to perform entity updation
// if user gave his own returning
if (typeof this.expressionMap.returning !== "string" &&
this.expressionMap.extraReturningColumns.length > 0 &&
driver.isReturningSqlSupported(returningType)) {
columns.push(...this.expressionMap.extraReturningColumns.filter((column) => {
return columns.indexOf(column) === -1;
}));
}
if (columns.length) {
let columnsExpression = columns
.map((column) => {
const name = this.escape(column.databaseName);
if (driver.options.type === "mssql") {
if (this.expressionMap.queryType === "insert" ||
this.expressionMap.queryType === "update" ||
this.expressionMap.queryType === "soft-delete" ||
this.expressionMap.queryType === "restore") {
return "INSERTED." + name;
}
else {
return (this.escape(this.getMainTableName()) +
"." +
name);
}
}
else {
return name;
}
})
.join(", ");
if (driver.options.type === "oracle") {
columnsExpression +=
" INTO " +
columns
.map((column) => {
return this.createParameter({
type: driver.columnTypeToNativeParameter(column.type),
dir: driver.oracle.BIND_OUT,
});
})
.join(", ");
}
if (driver.options.type === "mssql") {
if (this.expressionMap.queryType === "insert" ||
this.expressionMap.queryType === "update") {
columnsExpression += " INTO @OutputTable";
}
}
return columnsExpression;
}
else if (typeof this.expressionMap.returning === "string") {
return this.expressionMap.returning;
}
return "";
}
/**
* If returning / output cause is set to array of column names,
* then this method will return all column metadatas of those column names.
*/
getReturningColumns() {
const columns = [];
if (Array.isArray(this.expressionMap.returning)) {
;
this.expressionMap.returning.forEach((columnName) => {
if (this.expressionMap.mainAlias.hasMetadata) {
columns.push(...this.expressionMap.mainAlias.metadata.findColumnsWithPropertyPath(columnName));
}
});
}
return columns;
}
createWhereClausesExpression(clauses) {
return clauses
.map((clause, index) => {
const expression = this.createWhereConditionExpression(clause.condition);
switch (clause.type) {
case "and":
return ((index > 0 ? "AND " : "") +
`${this.connection.options.isolateWhereStatements
? "("
: ""}${expression}${this.connection.options.isolateWhereStatements
? ")"
: ""}`);
case "or":
return ((index > 0 ? "OR " : "") +
`${this.connection.options.isolateWhereStatements
? "("
: ""}${expression}${this.connection.options.isolateWhereStatements
? ")"
: ""}`);
}
return expression;
})
.join(" ")
.trim();
}
/**
* Computes given where argument - transforms to a where string all forms it can take.
*/
createWhereConditionExpression(condition, alwaysWrap = false) {
if (typeof condition === "string")
return condition;
if (Array.isArray(condition)) {
if (condition.length === 0) {
return "1=1";
}
// In the future we should probably remove this entire condition
// but for now to prevent any breaking changes it exists.
if (condition.length === 1 && !alwaysWrap) {
return this.createWhereClausesExpression(condition);
}
return "(" + this.createWhereClausesExpression(condition) + ")";
}
const { driver } = this.connection;
switch (condition.operator) {
case "lessThan":
return `${condition.parameters[0]} < ${condition.parameters[1]}`;
case "lessThanOrEqual":
return `${condition.parameters[0]} <= ${condition.parameters[1]}`;
case "arrayContains":
return `${condition.parameters[0]} @> ${condition.parameters[1]}`;
case "jsonContains":
return `${condition.parameters[0]} ::jsonb @> ${condition.parameters[1]}`;
case "arrayContainedBy":
return `${condition.parameters[0]} <@ ${condition.parameters[1]}`;
case "arrayOverlap":
return `${condition.parameters[0]} && ${condition.parameters[1]}`;
case "moreThan":
return `${condition.parameters[0]} > ${condition.parameters[1]}`;
case "moreThanOrEqual":
return `${condition.parameters[0]} >= ${condition.parameters[1]}`;
case "notEqual":
return `${condition.parameters[0]} != ${condition.parameters[1]}`;
case "equal":
return `${condition.parameters[0]} = ${condition.parameters[1]}`;
case "ilike":
if (driver.options.type === "postgres" ||
driver.options.type === "cockroachdb") {
return `${condition.parameters[0]} ILIKE ${condition.parameters[1]}`;
}
return `UPPER(${condition.parameters[0]}) LIKE UPPER(${condition.parameters[1]})`;
case "like":
return `${condition.parameters[0]} LIKE ${condition.parameters[1]}`;
case "between":
return `${condition.parameters[0]} BETWEEN ${condition.parameters[1]} AND ${condition.parameters[2]}`;
case "in":
if (condition.parameters.length <= 1) {
return "0=1";
}
return `${condition.parameters[0]} IN (${condition.parameters
.slice(1)
.join(", ")})`;
case "any":
if (driver.options.type === "cockroachdb") {
return `${condition.parameters[0]}::STRING = ANY(${condition.parameters[1]}::STRING[])`;
}
return `${condition.parameters[0]} = ANY(${condition.parameters[1]})`;
case "isNull":
return `${condition.parameters[0]} IS NULL`;
case "not":
return `NOT(${this.createWhereConditionExpression(condition.condition)})`;
case "brackets":
return `${this.createWhereConditionExpression(condition.condition, true)}`;
case "and":
return "(" + condition.parameters.join(" AND ") + ")";
case "or":
return "(" + condition.parameters.join(" OR ") + ")";
}
throw new TypeError(`Unsupported FindOperator ${FindOperator.constructor.name}`);
}
createCteExpression() {
if (!this.hasCommonTableExpressions()) {
return "";
}
const databaseRequireRecusiveHint = this.connection.driver.cteCapabilities.requiresRecursiveHint;
const cteStrings = this.expressionMap.commonTableExpressions.map((cte) => {
const cteBodyExpression = typeof cte.queryBuilder === "string"
? cte.queryBuilder
: cte.queryBuilder.getQuery();
if (typeof cte.queryBuilder !== "string") {
if (cte.queryBuilder.hasCommonTableExpressions()) {
throw new TypeORMError(`Nested CTEs aren't supported (CTE: ${cte.alias})`);
}
if (!this.connection.driver.cteCapabilities.writable &&
!InstanceChecker.isSelectQueryBuilder(cte.queryBuilder)) {
throw new TypeORMError(`Only select queries are supported in CTEs in ${this.connection.options.type} (CTE: ${cte.alias})`);
}
this.setParameters(cte.queryBuilder.getParameters());
}
let cteHeader = this.escape(cte.alias);
if (cte.options.columnNames) {
const escapedColumnNames = cte.options.columnNames.map((column) => this.escape(column));
if (InstanceChecker.isSelectQueryBuilder(cte.queryBuilder)) {
if (cte.queryBuilder.expressionMap.selects.length &&
cte.options.columnNames.length !==
cte.queryBuilder.expressionMap.selects.length) {
throw new TypeORMError(`cte.options.columnNames length (${cte.options.columnNames.length}) doesn't match subquery select list length ${cte.queryBuilder.expressionMap.selects.length} (CTE: ${cte.alias})`);
}
}
cteHeader += `(${escapedColumnNames.join(", ")})`;
}
const recursiveClause = cte.options.recursive && databaseRequireRecusiveHint
? "RECURSIVE"
: "";
let materializeClause = "";
if (this.connection.driver.cteCapabilities.materializedHint &&
cte.options.materialized !== undefined) {
materializeClause = cte.options.materialized
? "MATERIALIZED"
: "NOT MATERIALIZED";
}
return [
recursiveClause,
cteHeader,
"AS",
materializeClause,
`(${cteBodyExpression})`,
]
.filter(Boolean)
.join(" ");
});
return "WITH " + cteStrings.join(", ") + " ";
}
/**
* Creates "WHERE" condition for an in-ids condition.
*/
getWhereInIdsCondition(ids) {
const metadata = this.expressionMap.mainAlias.metadata;
const normalized = (Array.isArray(ids) ? ids : [ids]).map((id) => metadata.ensureEntityIdMap(id));
// using in(...ids) for single primary key entities
if (!metadata.hasMultiplePrimaryKeys) {
const primaryColumn = metadata.primaryColumns[0];
// getEntityValue will try to transform `In`, it is a bug
// todo: remove this transformer check after #2390 is fixed
// This also fails for embedded & relation, so until that is fixed skip it.
if (!primaryColumn.transformer &&
!primaryColumn.relationMetadata &&
!primaryColumn.embeddedMetadata) {
return {
[primaryColumn.propertyName]: In(normalized.map((id) => primaryColumn.getEntityValue(id, false))),
};
}
}
return new Brackets((qb) => {
for (const data of normalized) {
qb.orWhere(new Brackets((qb) => qb.where(data)));
}
});
}
getExistsCondition(subQuery) {
const query = subQuery
.clone()
.orderBy()
.groupBy()
.offset(undefined)
.limit(undefined)
.skip(undefined)
.take(undefined)
.select("1")
.setOption("disable-global-order");
return [`EXISTS (${query.getQuery()})`, query.getParameters()];
}
findColumnsForPropertyPath(propertyPath) {
// Make a helper to iterate the entity & relations?
// Use that to set the correct alias? Or the other way around?
// Start with the main alias with our property paths
let alias = this.expressionMap.mainAlias;
const root = [];
const propertyPathParts = propertyPath.split(".");
while (propertyPathParts.length > 1) {
const part = propertyPathParts[0];
if (!alias?.hasMetadata) {
// If there's no metadata, we're wasting our time
// and can't actually look any of this up.
break;
}
if (alias.metadata.hasEmbeddedWithPropertyPath(part)) {
// If this is an embedded then we should combine the two as part of our lookup.
// Instead of just breaking, we keep going with this in case there's an embedded/relation
// inside an embedded.
propertyPathParts.unshift(`${propertyPathParts.shift()}.${propertyPathParts.shift()}`);
continue;
}
if (alias.metadata.hasRelationWithPropertyPath(part)) {
// If this is a relation then we should find the aliases
// that match the relation & then continue further down
// the property path
const joinAttr = this.expressionMap.joinAttributes.find((joinAttr) => joinAttr.relationPropertyPath === part);
if (!joinAttr?.alias) {
const fullRelationPath = root.length > 0 ? `${root.join(".")}.${part}` : part;
throw new Error(`Cannot find alias for relation at ${fullRelationPath}`);
}
alias = joinAttr.alias;
root.push(...part.split("."));
propertyPathParts.shift();
continue;
}
break;
}
if (!alias) {
throw new Error(`Cannot find alias for property ${propertyPath}`);
}
// Remaining parts are combined back and used to find the actual property path
const aliasPropertyPath = propertyPathParts.join(".");
const columns = alias.metadata.findColumnsWithPropertyPath(aliasPropertyPath);
if (!columns.length) {
throw new EntityPropertyNotFoundError(propertyPath, alias.metadata);
}
return [alias, root, columns];
}
/**
* Creates a property paths for a given ObjectLiteral.
*/
createPropertyPath(metadata, entity, prefix = "") {
const paths = [];
for (const key of Object.keys(entity)) {
const path = prefix ? `${prefix}.${key}` : key;
// There's times where we don't actually want to traverse deeper.
// If the value is a `FindOperator`, or null, or not an object, then we don't, for example.
if (entity[key] === null ||
typeof entity[key] !== "object" ||
InstanceChecker.isFindOperator(entity[key])) {
paths.push(path);
continue;
}
if (metadata.hasEmbeddedWithPropertyPath(path)) {
const subPaths = this.createPropertyPath(metadata, entity[key], path);
paths.push(...subPaths);
continue;
}
if (metadata.hasRelationWithPropertyPath(path)) {
const relation = metadata.findRelationWithPropertyPath(path);
// There's also cases where we don't want to return back all of the properties.
// These handles the situation where someone passes the model & we don't need to make
// a HUGE `where` to uniquely look up the entity.
// In the case of a *-to-one, there's only ever one possible entity on the other side
// so if the join columns are all defined we can return just the relation itself
// because it will fetch only the join columns and do the lookup.
if (relation.relationType === "one-to-one" ||
relation.relationType === "many-to-one") {
const joinColumns = relation.joinColumns
.map((j) => j.referencedColumn)
.filter((j) => !!j);
const hasAllJoinColumns = joinColumns.length > 0 &&
joinColumns.every((column) => column.getEntityValue(entity[key], false));
if (hasAllJoinColumns) {
paths.push(path);
continue;
}
}
if (relation.relationType === "one-to-many" ||
relation.relationType === "many-to-many") {
throw new Error(`Cannot query across ${relation.relationType} for property ${path}`);
}
// For any other case, if the `entity[key]` contains all of the primary keys we can do a
// lookup via these. We don't need to look up via any other values 'cause these are
// the unique primary keys.
// This handles the situation where someone passes the model & we don't need to make
// a HUGE where.
const primaryColumns = relation.inverseEntityMetadata.primaryColumns;
const hasAllPrimaryKeys = primaryColumns.length > 0 &&
primaryColumns.every((column) => column.getEntityValue(entity[key], false));
if (hasAllPrimaryKeys) {
const subPaths = primaryColumns.map((column) => `${path}.${column.propertyPath}`);
paths.push(...subPaths);
continue;
}
// If nothing else, just return every property that's being passed to us.
const subPaths = this.createPropertyPath(relation.inverseEntityMetadata, entity[key]).map((p) => `${path}.${p}`);
paths.push(...subPaths);
continue;
}
paths.push(path);
}
return paths;
}
*getPredicates(where) {
if (this.expressionMap.mainAlias.hasMetadata) {
const propertyPaths = this.createPropertyPath(this.expressionMap.mainAlias.metadata, where);
for (const propertyPath of propertyPaths) {
const [alias, aliasPropertyPath, columns] = this.findColumnsForPropertyPath(propertyPath);
for (const column of columns) {
let containedWhere = where;
for (const part of aliasPropertyPath) {
if (!containedWhere || !(part in containedWhere)) {
containedWhere = {};
break;
}
containedWhere = containedWhere[part];
}
// Use the correct alias & the property path from the column
const aliasPath = this.expressionMap
.aliasNamePrefixingEnabled
? `${alias.name}.${column.propertyPath}`
: column.propertyPath;
const parameterValue = column.getEntityValue(containedWhere, true);
yield [aliasPath, parameterValue];
}
}
}
else {
for (const key of Object.keys(where)) {
const parameterValue = where[key];
const aliasPath = this.expressionMap.aliasNamePrefixingEnabled
? `${this.alias}.${key}`
: key;
yield [aliasPath, parameterValue];
}
}
}
getWherePredicateCondition(aliasPath, parameterValue) {
if (InstanceChecker.isFindOperator(parameterValue)) {
let parameters = [];
if (parameterValue.useParameter) {
if (parameterValue.objectLiteralParameters) {
this.setParameters(parameterValue.objectLiteralParameters);
}
else if (parameterValue.multipleParameters) {
for (const v of parameterValue.value) {
parameters.push(this.createParameter(v));
}
}
else {
parameters.push(this.createParameter(parameterValue.value));
}
}
if (parameterValue.type === "raw") {
if (parameterValue.getSql) {
return parameterValue.getSql(aliasPath);
}
else {
return {
operator: "equal",
parameters: [aliasPath, parameterValue.value],
};
}
}
else if (parameterValue.type === "not") {
if (parameterValue.child) {
return {
operator: parameterValue.type,
condition: this.getWherePredicateCondition(aliasPath, parameterValue.child),
};
}
else {
return {
operator: "notEqual",
parameters: [aliasPath, ...parameters],
};
}
}
else if (parameterValue.type === "and") {
const values = parameterValue.value;
return {
operator: parameterValue.type,
parameters: values.map((operator) => this.createWhereConditionExpression(this.getWherePredicateCondition(aliasPath, operator))),
};
}
else if (parameterValue.type === "or") {
const values = parameterValue.value;
return {
operator: parameterValue.type,
parameters: values.map((operator) => this.createWhereConditionExpression(this.getWherePredicateCondition(aliasPath, operator))),
};
}
else {
return {
operator: parameterValue.type,
parameters: [aliasPath, ...parameters],
};
}
// } else if (parameterValue === null) {
// return {
// operator: "isNull",
// parameters: [
// aliasPath,
// ]
// };
}
else {
return {
operator: "equal",
parameters: [aliasPath, this.createParameter(parameterValue)],
};
}
}
getWhereCondition(where) {
if (typeof where === "string") {
return where;
}
if (InstanceChecker.isBrackets(where)) {
const whereQueryBuilder = this.createQueryBuilder();
whereQueryBuilder.parentQueryBuilder = this;
whereQueryBuilder.expressionMap.mainAlias =
this.expressionMap.mainAlias;
whereQueryBuilder.expressionMap.aliasNamePrefixingEnabled =
this.expressionMap.aliasNamePrefixingEnabled;
whereQueryBuilder.expressionMap.parameters =
this.expressionMap.parameters;
whereQueryBuilder.expressionMap.nativeParameters =
this.expressionMap.nativeParameters;
whereQueryBuilder.expressionMap.wheres = [];
where.whereFactory(whereQueryBuilder);
return {
operator: InstanceChecker.isNotBrackets(where)
? "not"
: "brackets",
condition: whereQueryBuilder.expressionMap.wheres,
};
}
if (typeof where === "function") {
return where(this);
}
const wheres = Array.isArray(where) ? where : [where];
const clauses = [];
for (const where of wheres) {
const conditions = [];
// Filter the conditions and set up the parameter values
for (const [aliasPath, parameterValue] of this.getPredicates(where)) {
conditions.push({
type: "and",
condition: this.getWherePredicateCondition(aliasPath, parameterValue),
});
}
clauses.push({ type: "or", condition: conditions });
}
if (clauses.length === 1) {
return clauses[0].condition;
}
return clauses;
}
/**
* Creates a query builder used to execute sql queries inside this query builder.
*/
obtainQueryRunner() {
return this.queryRunner || this.connection.createQueryRunner();
}
hasCommonTableExpressions() {
return this.expressionMap.commonTableExpressions.length > 0;
}
}
/**
* Contains all registered query builder classes.
*/
QueryBuilder.queryBuilderRegistry = {};
//# sourceMappingURL=QueryBuilder.js.map