UNPKG

typeorm

Version:

Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.

242 lines (240 loc) • 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClosureSubjectExecutor = void 0; const CannotAttachTreeChildrenEntityError_1 = require("../../error/CannotAttachTreeChildrenEntityError"); const OrmUtils_1 = require("../../util/OrmUtils"); /** * Executes subject operations for closure entities. */ class ClosureSubjectExecutor { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(queryRunner) { this.queryRunner = queryRunner; } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Executes operations when subject is being inserted. */ async insert(subject) { // create values to be inserted into the closure junction const closureJunctionInsertMap = {}; subject.metadata.closureJunctionTable.ancestorColumns.forEach((column) => { closureJunctionInsertMap[column.databaseName] = subject.identifier; }); subject.metadata.closureJunctionTable.descendantColumns.forEach((column) => { closureJunctionInsertMap[column.databaseName] = subject.identifier; }); // insert values into the closure junction table await this.queryRunner.manager .createQueryBuilder() .insert() .into(subject.metadata.closureJunctionTable.tablePath) .values(closureJunctionInsertMap) .updateEntity(false) .callListeners(false) .execute(); let parent = subject.metadata.treeParentRelation.getEntityValue(subject.entity); // if entity was attached via parent if (!parent && subject.parentSubject && subject.parentSubject.entity) // if entity was attached via children parent = subject.parentSubject.insertedValueSet ? subject.parentSubject.insertedValueSet : subject.parentSubject.entity; if (parent) { const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const tableName = this.getTableName(subject.metadata.closureJunctionTable.tablePath); const queryParams = []; const ancestorColumnNames = subject.metadata.closureJunctionTable.ancestorColumns.map((column) => { return escape(column.databaseName); }); const descendantColumnNames = subject.metadata.closureJunctionTable.descendantColumns.map((column) => { return escape(column.databaseName); }); const childEntityIds1 = subject.metadata.primaryColumns.map((column) => { queryParams.push(column.getEntityValue(subject.insertedValueSet ? subject.insertedValueSet : subject.entity)); return this.queryRunner.connection.driver.createParameter("child_entity_" + column.databaseName, queryParams.length - 1); }); const whereCondition = subject.metadata.closureJunctionTable.descendantColumns.map((column) => { const columnName = escape(column.databaseName); const parentId = column.referencedColumn.getEntityValue(parent); if (!parentId) throw new CannotAttachTreeChildrenEntityError_1.CannotAttachTreeChildrenEntityError(subject.metadata.name); queryParams.push(parentId); const parameterName = this.queryRunner.connection.driver.createParameter("parent_entity_" + column.referencedColumn.databaseName, queryParams.length - 1); return `${columnName} = ${parameterName}`; }); await this.queryRunner.query(`INSERT INTO ${tableName} (${[ ...ancestorColumnNames, ...descendantColumnNames, ].join(", ")}) ` + `SELECT ${ancestorColumnNames.join(", ")}, ${childEntityIds1.join(", ")} FROM ${tableName} WHERE ${whereCondition.join(" AND ")}`, queryParams); } } /** * Executes operations when subject is being updated. */ async update(subject) { let parent = subject.metadata.treeParentRelation.getEntityValue(subject.entity); // if entity was attached via parent if (!parent && subject.parentSubject && subject.parentSubject.entity) // if entity was attached via children parent = subject.parentSubject.entity; let entity = subject.databaseEntity; // if entity was attached via parent if (!entity && parent) // if entity was attached via children entity = subject.metadata .treeChildrenRelation.getEntityValue(parent) .find((child) => { return Object.entries(subject.identifier).every(([key, value]) => child[key] === value); }); // Exit if the parent or the entity where never set if (entity === undefined || parent === undefined) { return; } const oldParent = subject.metadata.treeParentRelation.getEntityValue(entity); const oldParentId = subject.metadata.getEntityIdMap(oldParent); const parentId = subject.metadata.getEntityIdMap(parent); // Exit if the new and old parents are the same if (OrmUtils_1.OrmUtils.compareIds(oldParentId, parentId)) { return; } const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const closureTable = subject.metadata.closureJunctionTable; const ancestorColumnNames = closureTable.ancestorColumns.map((column) => { return escape(column.databaseName); }); const descendantColumnNames = closureTable.descendantColumns.map((column) => { return escape(column.databaseName); }); // Delete logic const createSubQuery = (qb, alias) => { const subAlias = `sub${alias}`; const subSelect = qb .createQueryBuilder() .select(descendantColumnNames.join(", ")) .from(closureTable.tablePath, subAlias); // Create where conditions e.g. (WHERE "subdescendant"."id_ancestor" = :value_id) for (const column of closureTable.ancestorColumns) { subSelect.andWhere(`${escape(subAlias)}.${escape(column.databaseName)} = :value_${column.referencedColumn.databaseName}`); } return qb .createQueryBuilder() .select(descendantColumnNames.join(", ")) .from(`(${subSelect.getQuery()})`, alias) .setParameters(subSelect.getParameters()) .getQuery(); }; const parameters = {}; for (const column of subject.metadata.primaryColumns) { parameters[`value_${column.databaseName}`] = entity[column.databaseName]; } await this.queryRunner.manager .createQueryBuilder() .delete() .from(closureTable.tablePath) .where((qb) => `(${descendantColumnNames.join(", ")}) IN (${createSubQuery(qb, "descendant")})`) .andWhere((qb) => `(${ancestorColumnNames.join(", ")}) NOT IN (${createSubQuery(qb, "ancestor")})`) .setParameters(parameters) .execute(); /** * Only insert new parent if it exits * * This only happens if the entity doesn't become a root entity */ if (parent) { // Insert logic const queryParams = []; const tableName = this.getTableName(closureTable.tablePath); const superAlias = escape("supertree"); const subAlias = escape("subtree"); const select = [ ...ancestorColumnNames.map((columnName) => `${superAlias}.${columnName}`), ...descendantColumnNames.map((columnName) => `${subAlias}.${columnName}`), ]; const entityWhereCondition = subject.metadata.closureJunctionTable.ancestorColumns.map((column) => { const columnName = escape(column.databaseName); const entityId = column.referencedColumn.getEntityValue(entity); queryParams.push(entityId); const parameterName = this.queryRunner.connection.driver.createParameter("entity_" + column.referencedColumn.databaseName, queryParams.length - 1); return `${subAlias}.${columnName} = ${parameterName}`; }); const parentWhereCondition = subject.metadata.closureJunctionTable.descendantColumns.map((column) => { const columnName = escape(column.databaseName); const parentId = column.referencedColumn.getEntityValue(parent); if (!parentId) throw new CannotAttachTreeChildrenEntityError_1.CannotAttachTreeChildrenEntityError(subject.metadata.name); queryParams.push(parentId); const parameterName = this.queryRunner.connection.driver.createParameter("parent_entity_" + column.referencedColumn.databaseName, queryParams.length - 1); return `${superAlias}.${columnName} = ${parameterName}`; }); await this.queryRunner.query(`INSERT INTO ${tableName} (${[ ...ancestorColumnNames, ...descendantColumnNames, ].join(", ")}) ` + `SELECT ${select.join(", ")} ` + `FROM ${tableName} AS ${superAlias}, ${tableName} AS ${subAlias} ` + `WHERE ${[ ...entityWhereCondition, ...parentWhereCondition, ].join(" AND ")}`, queryParams); } } /** * Executes operations when subject is being removed. */ async remove(subjects) { // Only mssql need to execute deletes for the juntion table as it doesn't support multi cascade paths. if (!(this.queryRunner.connection.driver.options.type === "mssql")) { return; } if (!Array.isArray(subjects)) subjects = [subjects]; const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const identifiers = subjects.map((subject) => subject.identifier); const closureTable = subjects[0].metadata.closureJunctionTable; const generateWheres = (columns) => { return columns .map((column) => { const data = identifiers.map((identifier) => identifier[column.referencedColumn.databaseName]); return `${escape(column.databaseName)} IN (${data.join(", ")})`; }) .join(" AND "); }; const ancestorWhere = generateWheres(closureTable.ancestorColumns); const descendantWhere = generateWheres(closureTable.descendantColumns); await this.queryRunner.manager .createQueryBuilder() .delete() .from(closureTable.tablePath) .where(ancestorWhere) .orWhere(descendantWhere) .execute(); } /** * Gets escaped table name with schema name if SqlServer or Postgres 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 return i === "" ? i : this.queryRunner.connection.driver.escape(i); }) .join("."); } } exports.ClosureSubjectExecutor = ClosureSubjectExecutor; //# sourceMappingURL=ClosureSubjectExecutor.js.map