UNPKG

ionic-orm-2

Version:

Data-mapper ORM for Ionic WebSQL and SQLite

1,041 lines (1,037 loc) 61.9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Alias } from "./alias/Alias"; import { AliasMap } from "./alias/AliasMap"; import { RawSqlResultsToEntityTransformer } from "./transformer/RawSqlResultsToEntityTransformer"; /** * Allows to build complex sql queries in a fashion way and execute those queries. */ export class QueryBuilder { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(connection, queryRunner) { this.connection = connection; this.queryRunner = queryRunner; this.type = "select"; this.selects = []; this.joins = []; this.joinRelationIds = []; this.relationCountMetas = []; this.groupBys = []; this.wheres = []; this.havings = []; this.orderBys = {}; this.parameters = {}; this.ignoreParentTablesJoins = false; this.aliasMap = new AliasMap(connection.entityMetadatas); } // ------------------------------------------------------------------------- // Accessors // ------------------------------------------------------------------------- /** * Gets the main alias string used in this query builder. */ get alias() { return this.aliasMap.mainAlias.name; } /** * Creates DELETE query. */ delete(tableNameOrEntity) { if (tableNameOrEntity instanceof Function) { const aliasName = tableNameOrEntity.name; const aliasObj = new Alias(aliasName); aliasObj.target = tableNameOrEntity; this.aliasMap.addMainAlias(aliasObj); this.fromEntity = { alias: aliasObj }; } else if (typeof tableNameOrEntity === "string") { this.fromTableName = tableNameOrEntity; } this.type = "delete"; return this; } /** * Creates UPDATE query and applies given update values. */ update(tableNameOrEntityOrUpdateSet, maybeUpdateSet) { const updateSet = maybeUpdateSet ? maybeUpdateSet : tableNameOrEntityOrUpdateSet; if (tableNameOrEntityOrUpdateSet instanceof Function) { const aliasName = tableNameOrEntityOrUpdateSet.name; const aliasObj = new Alias(aliasName); aliasObj.target = tableNameOrEntityOrUpdateSet; this.aliasMap.addMainAlias(aliasObj); this.fromEntity = { alias: aliasObj }; } else if (typeof tableNameOrEntityOrUpdateSet === "string") { this.fromTableName = tableNameOrEntityOrUpdateSet; } this.type = "update"; this.updateQuerySet = updateSet; return this; } /** * Creates SELECT query and selects given data. * Replaces all old selections if they exist. */ select(selection) { this.type = "select"; if (selection) { if (selection instanceof Array) { this.selects = selection; } else { this.selects = [selection]; } } return this; } /** * Adds new selection to the SELECT query. */ addSelect(selection) { if (selection instanceof Array) this.selects = this.selects.concat(selection); else this.selects.push(selection); return this; } /** * Specifies FROM on which table select/update/delete will be executed. * Also sets a main string alias of the selection data. */ from(entityOrTableName, alias) { if (entityOrTableName instanceof Function || this.isValueSimpleString(entityOrTableName)) { const aliasObj = new Alias(alias); aliasObj.target = entityOrTableName; this.aliasMap.addMainAlias(aliasObj); this.fromEntity = { alias: aliasObj }; } else { this.fromTableName = entityOrTableName; this.fromTableAlias = 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, conditionType = "ON", condition = "", parameters) { return this.join("INNER", entityOrProperty, alias, conditionType, condition, parameters); } /** * 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, conditionType = "ON", condition = "", parameters) { return this.join("LEFT", entityOrProperty, alias, conditionType, condition, parameters); } /** * 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, conditionType = "ON", condition = "", parameters) { this.addSelect(alias); return this.join("INNER", entityOrProperty, alias, conditionType, condition, parameters); } /** * 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, conditionType = "ON", condition = "", parameters) { this.addSelect(alias); return this.join("LEFT", entityOrProperty, alias, conditionType, condition, parameters); } /** * 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, conditionType = "ON", condition = "", parameters) { this.addSelect(alias); return this.join("INNER", entityOrProperty, alias, conditionType, condition, parameters, mapToProperty, true); } /** * 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, conditionType = "ON", condition = "", parameters) { this.addSelect(alias); return this.join("INNER", entityOrProperty, alias, conditionType, condition, parameters, mapToProperty, false); } /** * 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, conditionType = "ON", condition = "", parameters) { this.addSelect(alias); return this.join("LEFT", entityOrProperty, alias, conditionType, condition, parameters, mapToProperty, true); } /** * 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, conditionType = "ON", condition = "", parameters) { this.addSelect(alias); return this.join("LEFT", entityOrProperty, alias, conditionType, condition, parameters, mapToProperty, false); } /** * LEFT JOINs relation id. * Optionally, you can add condition and parameters used in condition. * * @experimental */ leftJoinRelationId(property, conditionType = "ON", condition, parameters) { return this.joinRelationId("LEFT", undefined, property, conditionType, condition, parameters); } /** * INNER JOINs relation id. * Optionally, you can add condition and parameters used in condition. * * @experimental */ innerJoinRelationId(property, conditionType, condition, parameters) { return this.joinRelationId("INNER", undefined, property, conditionType, condition, parameters); } /** * LEFT JOINs relation id and maps it into some entity's property. * Optionally, you can add condition and parameters used in condition. * * @experimental */ leftJoinRelationIdAndMap(mapToProperty, property, conditionType = "ON", condition = "", parameters) { return this.joinRelationId("INNER", mapToProperty, property, conditionType, condition, parameters); } /** * INNER JOINs relation id and maps it into some entity's property. * Optionally, you can add condition and parameters used in condition. * * @experimental */ innerJoinRelationIdAndMap(mapToProperty, property, conditionType = "ON", condition = "", parameters) { return this.joinRelationId("INNER", mapToProperty, property, conditionType, condition, parameters); } /** * Counts number of entities of entity's relation. * Optionally, you can add condition and parameters used in condition. * * @experimental */ countRelation(property, conditionType = "ON", condition = "", parameters) { const [parentAliasName, parentPropertyName] = property.split("."); const alias = parentAliasName + "_" + parentPropertyName + "_relation_count"; const aliasObj = new Alias(alias); this.aliasMap.addAlias(aliasObj); aliasObj.parentAliasName = parentAliasName; aliasObj.parentPropertyName = parentPropertyName; const relationCountMeta = { conditionType: conditionType, condition: condition, alias: aliasObj, entities: [] }; this.relationCountMetas.push(relationCountMeta); if (parameters) this.addParameters(parameters); 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. * * @experimental */ countRelationAndMap(mapProperty, property, conditionType = "ON", condition = "", parameters) { const [parentAliasName, parentPropertyName] = property.split("."); const alias = parentAliasName + "_" + parentPropertyName + "_relation_count"; const aliasObj = new Alias(alias); this.aliasMap.addAlias(aliasObj); aliasObj.parentAliasName = parentAliasName; aliasObj.parentPropertyName = parentPropertyName; const relationCountMeta = { mapToProperty: mapProperty, conditionType: conditionType, condition: condition, alias: aliasObj, entities: [] }; this.relationCountMetas.push(relationCountMeta); if (parameters) this.addParameters(parameters); 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.wheres.push({ type: "simple", condition: where }); if (parameters) this.addParameters(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.wheres.push({ type: "and", condition: where }); if (parameters) this.addParameters(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.wheres.push({ type: "or", condition: where }); if (parameters) this.addParameters(parameters); return this; } /** * 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.havings.push({ type: "simple", condition: having }); if (parameters) this.addParameters(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.havings.push({ type: "and", condition: having }); if (parameters) this.addParameters(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.havings.push({ type: "or", condition: having }); if (parameters) this.addParameters(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) { this.groupBys = [groupBy]; return this; } /** * Adds GROUP BY condition in the query builder. */ addGroupBy(groupBy) { this.groupBys.push(groupBy); 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") { this.orderBys = { [sort]: order }; return this; } /** * Adds ORDER BY condition in the query builder. */ addOrderBy(sort, order = "ASC") { this.orderBys[sort] = order; return this; } /** * Set's 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 instead setMaxResults instead. */ setLimit(limit) { this.limit = limit; return this; } /** * Set's 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 instead setFirstResult instead. */ setOffset(offset) { this.offset = offset; return this; } /** * Set's maximum number of entities to be selected. */ setMaxResults(maxResults) { this.maxResults = maxResults; return this; } /** * Set's offset of entities to be selected. */ setFirstResult(firstResult) { this.firstResult = firstResult; return this; } /** * Sets given parameter's value. */ setParameter(key, value) { this.parameters[key] = value; return this; } /** * Sets given object literal as parameters. * Note, that it clears all previously set parameters. */ setParameters(parameters) { this.parameters = {}; Object.keys(parameters).forEach(key => { this.parameters[key] = parameters[key]; }); return this; } /** * Adds all parameters from the given object. * Unlike setParameters method it does not clear all previously set parameters. */ addParameters(parameters) { Object.keys(parameters).forEach(key => { this.parameters[key] = parameters[key]; }); return this; } /** * Gets all parameters. */ getParameters() { const parameters = Object.assign({}, this.parameters); // add discriminator column parameter if it exist const mainMetadata = this.connection.entityMetadatas.findByTarget(this.aliasMap.mainAlias.target); if (mainMetadata.hasDiscriminatorColumn) parameters["discriminatorColumnValue"] = mainMetadata.discriminatorValue; return parameters; } /** * Gets generated sql that will be executed. * Parameters in the query are escaped for the currently used driver. */ getSql() { let sql = this.createSelectExpression(); sql += this.createJoinExpression(); sql += this.createJoinRelationIdsExpression(); sql += this.createWhereExpression(); sql += this.createGroupByExpression(); sql += this.createHavingExpression(); sql += this.createOrderByExpression(); sql += this.createLimitExpression(); sql += this.createOffsetExpression(); [sql] = this.connection.driver.escapeQueryWithParameters(sql, this.parameters); return sql; } /** * Gets generated sql without parameters being replaced. * * @experimental */ getGeneratedQuery() { let sql = this.createSelectExpression(); sql += this.createJoinExpression(); sql += this.createJoinRelationIdsExpression(); sql += this.createWhereExpression(); sql += this.createGroupByExpression(); sql += this.createHavingExpression(); sql += this.createOrderByExpression(); sql += this.createLimitExpression(); sql += this.createOffsetExpression(); return sql; } /** * Gets sql to be executed with all parameters used in it. * * @experimental */ getSqlWithParameters(options) { let sql = this.createSelectExpression(); sql += this.createJoinExpression(); sql += this.createJoinRelationIdsExpression(); sql += this.createWhereExpression(); sql += this.createGroupByExpression(); sql += this.createHavingExpression(); if (!options || !options.skipOrderBy) sql += this.createOrderByExpression(); sql += this.createLimitExpression(); sql += this.createOffsetExpression(); return this.connection.driver.escapeQueryWithParameters(sql, this.getParameters()); } /** * Executes sql generated by query builder and returns raw database results. */ execute() { return __awaiter(this, void 0, void 0, function* () { let ownQueryRunner = false; let queryRunner = this.queryRunner; if (!queryRunner) { ownQueryRunner = true; queryRunner = yield this.connection.driver.createQueryRunner(); } const [sql, parameters] = this.getSqlWithParameters(); try { return yield queryRunner.query(sql, parameters); // await is needed here because we are using finally } finally { if (ownQueryRunner) yield queryRunner.release(); } }); } /** * Executes sql generated by query builder and returns object with scalar results and entities created from them. */ getResultsAndScalarResults() { return __awaiter(this, void 0, void 0, function* () { if (!this.aliasMap.hasMainAlias) throw new Error(`Alias is not set. Looks like nothing is selected. Use select*, delete, update method to set an alias.`); let ownQueryRunner = false; let queryRunner = this.queryRunner; if (!queryRunner) { ownQueryRunner = true; queryRunner = yield this.connection.driver.createQueryRunner(); } const mainAliasName = this.aliasMap.mainAlias.name; let scalarResults; if (this.firstResult || this.maxResults) { const [sql, parameters] = this.getSqlWithParameters(); // todo: fix for sql server. We cant skip order by here! // { skipOrderBy: true } const distinctAlias = this.connection.driver.escapeTableName("distinctAlias"); const metadata = this.connection.entityMetadatas.findByTarget(this.fromEntity.alias.target); let idsQuery = `SELECT `; idsQuery += metadata.primaryColumns.map((primaryColumn, index) => { const propertyName = this.connection.driver.escapeAliasName(mainAliasName + "_" + primaryColumn.name); if (index === 0) { return `DISTINCT(${distinctAlias}.${propertyName}) as ids_${primaryColumn.name}`; } else { return `${distinctAlias}.${propertyName}) as ids_${primaryColumn.name}`; } }).join(", "); idsQuery += ` FROM (${sql}) ${distinctAlias}`; // TODO: WHAT TO DO WITH PARAMETERS HERE? DO THEY WORK? if (this.maxResults) idsQuery += " LIMIT " + this.maxResults; if (this.firstResult) idsQuery += " OFFSET " + this.firstResult; try { return yield queryRunner.query(idsQuery, parameters) .then((results) => { scalarResults = results; if (results.length === 0) return []; let condition = ""; const parameters = {}; if (metadata.hasMultiplePrimaryKeys) { condition = results.map(result => { return metadata.primaryColumns.map(primaryColumn => { parameters["ids_" + primaryColumn.propertyName] = result["ids_" + primaryColumn.propertyName]; return mainAliasName + "." + primaryColumn.propertyName + "=:ids_" + primaryColumn.propertyName; }).join(" AND "); }).join(" OR "); } else { parameters["ids"] = results.map(result => result["ids_" + metadata.firstPrimaryColumn.propertyName]); condition = mainAliasName + "." + metadata.firstPrimaryColumn.propertyName + " IN (:ids)"; } const [queryWithIdsSql, queryWithIdsParameters] = this.clone({ queryRunner: queryRunner }) .andWhere(condition, parameters) .getSqlWithParameters(); return queryRunner.query(queryWithIdsSql, queryWithIdsParameters); }) .then(results => { return this.rawResultsToEntities(results); }) .then(results => this.connection.broadcaster.broadcastLoadEventsForAll(this.aliasMap.mainAlias.target, results).then(() => results)) .then(results => { return { entities: results, scalarResults: scalarResults }; }); } finally { if (ownQueryRunner) yield queryRunner.release(); } } else { const [sql, parameters] = this.getSqlWithParameters(); try { // console.log(sql); return yield queryRunner.query(sql, parameters) .then(results => { scalarResults = results; return this.rawResultsToEntities(results); }) .then(results => { return this.loadRelationCounts(queryRunner, results) .then(counts => { // console.log("counts: ", counts); return results; }); }) .then(results => { return this.connection.broadcaster .broadcastLoadEventsForAll(this.aliasMap.mainAlias.target, results) .then(() => results); }) .then(results => { return { entities: results, scalarResults: scalarResults }; }); } finally { if (ownQueryRunner) yield queryRunner.release(); } } }); } /** * Gets count - number of entities selected by sql generated by this query builder. * Count excludes all limitations set by setFirstResult and setMaxResults methods call. */ getCount() { return __awaiter(this, void 0, void 0, function* () { let ownQueryRunner = false; let queryRunner = this.queryRunner; if (!queryRunner) { ownQueryRunner = true; queryRunner = yield this.connection.driver.createQueryRunner(); } const mainAlias = this.aliasMap.mainAlias.name; const metadata = this.connection.entityMetadatas.findByTarget(this.fromEntity.alias.target); const distinctAlias = this.connection.driver.escapeAliasName(mainAlias); let countSql = `COUNT(` + metadata.primaryColumnsWithParentIdColumns.map((primaryColumn, index) => { const propertyName = this.connection.driver.escapeColumnName(primaryColumn.name); if (index === 0) { return `DISTINCT(${distinctAlias}.${propertyName})`; } else { return `${distinctAlias}.${propertyName})`; } }).join(", ") + ") as cnt"; const countQuery = this .clone({ queryRunner: queryRunner, skipOrderBys: true, ignoreParentTablesJoins: true }) .select(countSql); const [countQuerySql, countQueryParameters] = countQuery.getSqlWithParameters(); try { const results = yield queryRunner.query(countQuerySql, countQueryParameters); if (!results || !results[0] || !results[0]["cnt"]) return 0; return parseInt(results[0]["cnt"]); } finally { if (ownQueryRunner) yield queryRunner.release(); } }); } /** * Gets all scalar results returned by execution of generated query builder sql. */ getScalarResults() { return this.execute(); } /** * Gets first scalar result returned by execution of generated query builder sql. */ getSingleScalarResult() { return this.getScalarResults().then(results => results[0]); } /** * Gets entities and count returned by execution of generated query builder sql. */ getResultsAndCount() { // todo: share database connection and counter return Promise.all([ this.getResults(), this.getCount() ]); } /** * Gets entities returned by execution of generated query builder sql. */ getResults() { return this.getResultsAndScalarResults().then(results => { return results.entities; }); } /** * Gets single entity returned by execution of generated query builder sql. */ getSingleResult() { return this.getResults().then(entities => entities[0]); } /** * Clones query builder as it is. */ clone(options) { const qb = new QueryBuilder(this.connection, options ? options.queryRunner : undefined); if (options && options.ignoreParentTablesJoins) qb.ignoreParentTablesJoins = options.ignoreParentTablesJoins; switch (this.type) { case "select": qb.select(this.selects); break; case "update": qb.update(this.updateQuerySet); break; case "delete": qb.delete(); break; } if (this.fromEntity && this.fromEntity.alias && this.fromEntity.alias.target) { qb.from(this.fromEntity.alias.target, this.fromEntity.alias.name); } else if (this.fromTableName) { qb.from(this.fromTableName, this.fromTableAlias); } this.joins.forEach(join => { const property = join.tableName || join.alias.target || (join.alias.parentAliasName + "." + join.alias.parentPropertyName); qb.join(join.type, property, join.alias.name, join.conditionType, join.condition || "", undefined, join.mapToProperty, join.isMappingMany); }); this.groupBys.forEach(groupBy => qb.addGroupBy(groupBy)); this.wheres.forEach(where => { switch (where.type) { case "simple": qb.where(where.condition); break; case "and": qb.andWhere(where.condition); break; case "or": qb.orWhere(where.condition); break; } }); this.havings.forEach(having => { switch (having.type) { case "simple": qb.having(having.condition); break; case "and": qb.andHaving(having.condition); break; case "or": qb.orHaving(having.condition); break; } }); if (!options || !options.skipOrderBys) Object.keys(this.orderBys).forEach(columnName => qb.addOrderBy(columnName, this.orderBys[columnName])); Object.keys(this.parameters).forEach(key => qb.setParameter(key, this.parameters[key])); if (!options || !options.skipLimit) qb.setLimit(this.limit); if (!options || !options.skipOffset) qb.setOffset(this.offset); qb.setFirstResult(this.firstResult) .setMaxResults(this.maxResults); return qb; } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- loadRelationCounts(queryRunner, results) { const promises = this.relationCountMetas.map(relationCountMeta => { const parentAlias = relationCountMeta.alias.parentAliasName; const foundAlias = this.aliasMap.findAliasByName(parentAlias); if (!foundAlias) throw new Error(`Alias "${parentAlias}" was not found`); const parentMetadata = this.aliasMap.getEntityMetadataByAlias(foundAlias); if (!parentMetadata) throw new Error("Cannot get entity metadata for the given alias " + foundAlias.name); const relation = parentMetadata.findRelationWithPropertyName(relationCountMeta.alias.parentPropertyName); const queryBuilder = new QueryBuilder(this.connection, queryRunner); let condition = ""; const metadata = this.aliasMap.getEntityMetadataByAlias(relationCountMeta.alias); if (!metadata) throw new Error("Cannot get entity metadata for the given alias " + relationCountMeta.alias.name); let joinTableName = metadata.table.name; const junctionMetadata = relation.junctionEntityMetadata; const appendedCondition = relationCountMeta.condition ? " AND " + this.replacePropertyNames(relationCountMeta.condition) : ""; /*if (relation.isManyToMany) { const junctionTable = junctionMetadata.table.name; const junctionAlias = relationCountMeta.alias.parentAliasName + "_" + relationCountMeta.alias.name; const joinAlias = relationCountMeta.alias.name; const joinTable = relation.isOwning ? relation.joinTable : relation.inverseRelation.joinTable; // not sure if this is correct const joinTableColumn = joinTable.referencedColumn.name; // not sure if this is correct const inverseJoinColumnName = joinTable.inverseReferencedColumn.name; // not sure if this is correct let condition1 = "", condition2 = ""; if (relation.isOwning) { condition1 = junctionAlias + "." + junctionMetadata.columns[0].name + "=" + parentAlias + "." + joinTableColumn; condition2 = joinAlias + "." + inverseJoinColumnName + "=" + junctionAlias + "." + junctionMetadata.columns[1].name; } else { condition1 = junctionAlias + "." + junctionMetadata.columns[1].name + "=" + parentAlias + "." + joinTableColumn; condition2 = joinAlias + "." + inverseJoinColumnName + "=" + junctionAlias + "." + junctionMetadata.columns[0].name; } condition = " LEFT JOIN " + junctionTable + " " + junctionAlias + " " + relationCountMeta.conditionType + " " + condition1 + " LEFT JOIN " + joinTableName + " " + joinAlias + " " + relationCountMeta.conditionType + " " + condition2 + appendedCondition; } else if (relation.isManyToOne || (relation.isOneToOne && relation.isOwning)) { const joinTableColumn = relation.joinColumn.referencedColumn.name; const condition2 = relationCountMeta.alias.name + "." + joinTableColumn + "=" + parentAlias + "." + relation.name; condition = " LEFT JOIN " + joinTableName + " " + relationCountMeta.alias.name + " " + relationCountMeta.conditionType + " " + condition2 + appendedCondition; } else { throw new Error(`Relation count can be applied only `); // this should be done on entity build }*/ // if (relationCountMeta.condition) // condition += relationCountMeta.condition; // relationCountMeta.alias.target; // todo: FIX primaryColumn usages const ids = relationCountMeta.entities .map(entityWithMetadata => entityWithMetadata.metadata.getEntityIdMap(entityWithMetadata.entity)) .filter(idMap => idMap !== undefined) .map(idMap => idMap[parentMetadata.primaryColumn.propertyName]); if (!ids || !ids.length) return Promise.resolve(); // todo: need to set zero to relationCount column in this case? return queryBuilder .select(`${parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName} AS id`) .addSelect(`COUNT(${this.connection.driver.escapeAliasName(relation.propertyName) + "." + this.connection.driver.escapeColumnName(relation.inverseEntityMetadata.primaryColumn.name)}) as cnt`) .from(parentMetadata.target, parentMetadata.name) .leftJoin(parentMetadata.name + "." + relation.propertyName, relation.propertyName, relationCountMeta.conditionType, relationCountMeta.condition) .setParameters(this.parameters) .where(`${parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName} IN (:relationCountIds)`, { relationCountIds: ids }) .groupBy(parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName) .getScalarResults() .then((results) => { // console.log(relationCountMeta.entities); relationCountMeta.entities.forEach(entityWithMetadata => { const entityId = entityWithMetadata.entity[entityWithMetadata.metadata.primaryColumn.propertyName]; const entityResult = results.find(result => { return entityId === this.connection.driver.prepareHydratedValue(result.id, entityWithMetadata.metadata.primaryColumn); }); if (entityResult) { if (relationCountMeta.mapToProperty) { const [parentName, propertyName] = relationCountMeta.mapToProperty.split("."); // todo: right now mapping is working only on the currently countRelation class, but // different properties are working. make different classes to work too entityWithMetadata.entity[propertyName] = parseInt(entityResult.cnt); } else if (relation.countField) { entityWithMetadata.entity[relation.countField] = parseInt(entityResult.cnt); } } }); }); }); return Promise.all(promises); } rawResultsToEntities(results) { const transformer = new RawSqlResultsToEntityTransformer(this.connection.driver, this.aliasMap, this.extractJoinMappings(), this.relationCountMetas); return transformer.transform(results); } createSelectExpression() { // todo throw exception if selects or from is missing let alias = "", tableName; const allSelects = []; if (this.fromTableName) { tableName = this.fromTableName; alias = this.fromTableAlias; // console.log("ALIAS F:", alias); } else if (this.fromEntity) { const metadata = this.aliasMap.getEntityMetadataByAlias(this.fromEntity.alias); if (!metadata) throw new Error("Cannot get entity metadata for the given alias " + this.fromEntity.alias.name); tableName = metadata.table.name; alias = this.fromEntity.alias.name; // console.log("ALIAS N:", this.fromEntity.alias); // console.log("ALIAS N:", alias); // add select from the main table if (this.selects.indexOf(alias) !== -1) { metadata.columns.forEach(column => { allSelects.push(this.connection.driver.escapeAliasName(alias) + "." + this.connection.driver.escapeColumnName(column.name) + " AS " + this.connection.driver.escapeAliasName(alias + "_" + column.name)); }); } } else { throw new Error("No from given"); } // add selects from joins this.joins .filter(join => this.selects.indexOf(join.alias.name) !== -1) .forEach(join => { const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias); if (joinMetadata) { joinMetadata.columns.forEach(column => { allSelects.push(this.connection.driver.escapeAliasName(join.alias.name) + "." + this.connection.driver.escapeColumnName(column.name) + " AS " + this.connection.driver.escapeAliasName(join.alias.name + "_" + column.name)); }); } else { allSelects.push(this.connection.driver.escapeAliasName(join.alias.name)); } }); if (!this.ignoreParentTablesJoins) { const metadata = this.connection.entityMetadatas.findByTarget(this.aliasMap.mainAlias.target); if (metadata.parentEntityMetadata && metadata.parentIdColumns) { const alias = "parentIdColumn_" + this.connection.driver.escapeAliasName(metadata.parentEntityMetadata.table.name); metadata.parentEntityMetadata.columns.forEach(column => { allSelects.push(alias + "." + this.connection.driver.escapeColumnName(column.name) + " AS " + alias + "_" + this.connection.driver.escapeAliasName(column.name)); }); } } // add selects from relation id joins this.joinRelationIds.forEach(join => { // const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias); const parentAlias = join.alias.parentAliasName; const foundAlias = this.aliasMap.findAliasByName(parentAlias); if (!foundAlias) throw new Error(`Alias "${parentAlias}" was not found`); const parentMetadata = this.aliasMap.getEntityMetadataByAlias(foundAlias); if (!parentMetadata) throw new Error("Cannot get entity metadata for the given alias " + foundAlias.name); const relation = parentMetadata.findRelationWithPropertyName(join.alias.parentPropertyName); const junctionMetadata = relation.junctionEntityMetadata; // const junctionTable = junctionMetadata.table.name; junctionMetadata.columns.forEach(column => { allSelects.push(this.connection.driver.escapeAliasName(join.alias.name) + "." + this.connection.driver.escapeColumnName(column.name) + " AS " + this.connection.driver.escapeAliasName(join.alias.name + "_" + column.name)); }); }); // add all other selects this.selects.filter(select => { return select !== alias && !this.joins.find(join => join.alias.name === select); }).forEach(select => allSelects.push(this.replacePropertyNames(select))); // if still selection is empty, then simply set it to all (*) if (allSelects.length === 0) allSelects.push("*"); // create a selection query switch (this.type) { case "select": return "SELECT " + allSelects.join(", ") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + this.connection.driver.escapeAliasName(alias); case "delete": return "DELETE " + (alias ? this.connection.driver.escapeAliasName(alias) : "") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + (alias ? this.connection.driver.escapeAliasName(alias) : ""); case "update": const updateSet = Object.keys(this.updateQuerySet).map(key => key + "=:updateQuerySet_" + key); const params = Object.keys(this.updateQuerySet).reduce((object, key) => { // todo: map propertyNames to names ? object["updateQuerySet_" + key] = this.updateQuerySet[key]; return object; }, {}); this.addParameters(params); return "UPDATE " + tableName + " " + (alias ? this.connection.driver.escapeAliasName(alias) : "") + " SET " + updateSet; } throw new Error("No query builder type is specified."); } createWhereExpression() { const conditions = this.wheres.map((where, index) => { switch (where.type) { case "and": return (index > 0 ? "AND " : "") + this.replacePropertyNames(where.condition); case "or": return (index > 0 ? "OR " : "") + this.replacePropertyNames(where.condition); default: return this.replacePropertyNames(where.condition); } }).join(" "); const mainMetadata = this.connection.entityMetadatas.findByTarget(this.aliasMap.mainAlias.target); if (mainMetadata.hasDiscriminatorColumn) return ` WHERE ${conditions.length ? "(" + conditions + ")" : ""} AND ${mainMetadata.discriminatorColumn.name}=:discriminatorColumnValue`; if (!conditions.length) return ""; return " WHERE " + conditions; } /** * Replaces all entity's propertyName to name in the given statement. */ replacePropertyNames(statement) { this.aliasMap.aliases.forEach(alias => { const metadata = this.aliasMap.getEntityMetadataByAlias(alias); if (!metadata) return; metadata.embeddeds.forEach(embedded => { embedded.columns.forEach(column => { const expression = alias.name + "." + embedded.propertyName + "." + column.propertyName + "([ =]|.{0}$)"; statement = statement.replace(new RegExp(expression, "gm"), this.connection.driver.escapeAliasName(alias.name) + "." + this.connection.driver.escapeColumnName(column.name) + "$1"); }); }); metadata.columns.forEach(column => { const expression = alias.name + "." + column.propertyName + "([ =]|.{0}$)"; statement = statement.replace(new RegExp(expression, "gm"), this.connection.driver.escapeAliasName(alias.name) + "." + this.connection.driver.escapeColumnName(column.name) + "$1"); }); metadata.relationsWithJoinColumns.forEach(relation => { const expression = alias.name + "." + relation.propertyName + "([ =]|.{0}$)"; statement = statement.replace(new RegExp(expression, "gm"), this.connection.driver.escapeAliasName(alias.name) + "." + this.connection.driver.escapeColumnName(relation.name) + "$1"); }); }); return statement; } createJoinRelationIdsExpression() { return this.joinRelationIds.map(join => { const parentAlias = join.alias.parentAliasName; const foundAlias = this.aliasMap.findAliasByName(parentAlias); if (!foundAlias) throw new Error(`Alias "${parentAlias}" was not found`); const parentMetadata = this.aliasMap.getEntityMetadataByAlias(foundAlias); if (!parentMetadata) throw new Error("Cannot get entity metadata for the given alias " + foundAlias.name); const relation = parentMetadata.findRelationWithPropertyName(join.alias.parentPropertyName); const junctionMetadata = relation.junctionEntityMetadata; const junctionTable = junctionMetadata.table.name; const junctionAlias = join.alias.name; const joinTable = relation.isOwning ? relation.joinTable : relation.inverseRelation.joinTable; // not sure if this is correct const joinTableColumn = joinTable.referencedColumn.name; // not sure if this is correct let condition1 = ""; if (relation.isOwning) { condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[0].name) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn); // condition2 = joinAlias + "." + inverseJoinColumnName + "=" + junctionAlias + "." + junctionMetadata.columns[1].name; } else { condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[1].name) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn); // condition2 = joinAlias + "." + inverseJoinColumnName + "=" + junctionAlias + "." + junctionMetadata.columns[0].name; } return " " + join.type + " JOIN " + junctionTable + " " + this.connection.driver.escapeAliasName(junctionAlias) + " " + join.conditionType + " " + condition1; // " " + joinType + " JOIN " + joinTableName + " " + joinAlias + " " + join.conditionType + " " + condition2 + appendedCondition; // console.log(join); // return " " + join.type + " JOIN " + joinTableName + " " + join.alias.name + " " + (join.condition ? (join.conditionType + " " + join.condition) : ""); }); } createJoinExpression() { let joins = this.joins.map(join