UNPKG

typeorm

Version:

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

272 lines (270 loc) • 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NestedSetSubjectExecutor = void 0; const OrmUtils_1 = require("../../util/OrmUtils"); const NestedSetMultipleRootError_1 = require("../../error/NestedSetMultipleRootError"); class NestedSetIds { } /** * Executes subject operations for nested set tree entities. */ class NestedSetSubjectExecutor { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(queryRunner) { this.queryRunner = queryRunner; } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Executes operations when subject is being inserted. */ async insert(subject) { const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const tableName = this.getTableName(subject.metadata.tablePath); const leftColumnName = escape(subject.metadata.nestedSetLeftColumn.databaseName); const rightColumnName = escape(subject.metadata.nestedSetRightColumn.databaseName); 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; const parentId = subject.metadata.getEntityIdMap(parent); let parentNsRight = undefined; if (parentId) { parentNsRight = await this.queryRunner.manager .createQueryBuilder() .select(subject.metadata.targetName + "." + subject.metadata.nestedSetRightColumn.propertyPath, "right") .from(subject.metadata.target, subject.metadata.targetName) .whereInIds(parentId) .getRawOne() .then((result) => { const value = result ? result["right"] : undefined; // CockroachDB returns numeric types as string return typeof value === "string" ? parseInt(value) : value; }); } if (parentNsRight !== undefined) { await this.queryRunner.query(`UPDATE ${tableName} SET ` + `${leftColumnName} = CASE WHEN ${leftColumnName} > ${parentNsRight} THEN ${leftColumnName} + 2 ELSE ${leftColumnName} END,` + `${rightColumnName} = ${rightColumnName} + 2 ` + `WHERE ${rightColumnName} >= ${parentNsRight}`); OrmUtils_1.OrmUtils.mergeDeep(subject.insertedValueSet, subject.metadata.nestedSetLeftColumn.createValueMap(parentNsRight), subject.metadata.nestedSetRightColumn.createValueMap(parentNsRight + 1)); } else { const isUniqueRoot = await this.isUniqueRootEntity(subject, parent); // Validate if a root entity already exits and throw an exception if (!isUniqueRoot) throw new NestedSetMultipleRootError_1.NestedSetMultipleRootError(); OrmUtils_1.OrmUtils.mergeDeep(subject.insertedValueSet, subject.metadata.nestedSetLeftColumn.createValueMap(1), subject.metadata.nestedSetRightColumn.createValueMap(2)); } } /** * 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; } if (parent) { const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const tableName = this.getTableName(subject.metadata.tablePath); const leftColumnName = escape(subject.metadata.nestedSetLeftColumn.databaseName); const rightColumnName = escape(subject.metadata.nestedSetRightColumn.databaseName); const entityId = subject.metadata.getEntityIdMap(entity); let entityNs = undefined; if (entityId) { entityNs = (await this.getNestedSetIds(subject.metadata, entityId))[0]; } let parentNs = undefined; if (parentId) { parentNs = (await this.getNestedSetIds(subject.metadata, parentId))[0]; } if (entityNs !== undefined && parentNs !== undefined) { const isMovingUp = parentNs.left > entityNs.left; const treeSize = entityNs.right - entityNs.left + 1; let entitySize; if (isMovingUp) { entitySize = parentNs.left - entityNs.right; } else { entitySize = parentNs.right - entityNs.left; } // Moved entity logic const updateLeftSide = `WHEN ${leftColumnName} >= ${entityNs.left} AND ` + `${leftColumnName} < ${entityNs.right} ` + `THEN ${leftColumnName} + ${entitySize} `; const updateRightSide = `WHEN ${rightColumnName} > ${entityNs.left} AND ` + `${rightColumnName} <= ${entityNs.right} ` + `THEN ${rightColumnName} + ${entitySize} `; // Update the surrounding entities if (isMovingUp) { await this.queryRunner.query(`UPDATE ${tableName} ` + `SET ${leftColumnName} = CASE ` + `WHEN ${leftColumnName} > ${entityNs.right} AND ` + `${leftColumnName} <= ${parentNs.left} ` + `THEN ${leftColumnName} - ${treeSize} ` + updateLeftSide + `ELSE ${leftColumnName} ` + `END, ` + `${rightColumnName} = CASE ` + `WHEN ${rightColumnName} > ${entityNs.right} AND ` + `${rightColumnName} < ${parentNs.left} ` + `THEN ${rightColumnName} - ${treeSize} ` + updateRightSide + `ELSE ${rightColumnName} ` + `END`); } else { await this.queryRunner.query(`UPDATE ${tableName} ` + `SET ${leftColumnName} = CASE ` + `WHEN ${leftColumnName} < ${entityNs.left} AND ` + `${leftColumnName} > ${parentNs.right} ` + `THEN ${leftColumnName} + ${treeSize} ` + updateLeftSide + `ELSE ${leftColumnName} ` + `END, ` + `${rightColumnName} = CASE ` + `WHEN ${rightColumnName} < ${entityNs.left} AND ` + `${rightColumnName} >= ${parentNs.right} ` + `THEN ${rightColumnName} + ${treeSize} ` + updateRightSide + `ELSE ${rightColumnName} ` + `END`); } } } else { const isUniqueRoot = await this.isUniqueRootEntity(subject, parent); // Validate if a root entity already exits and throw an exception if (!isUniqueRoot) throw new NestedSetMultipleRootError_1.NestedSetMultipleRootError(); } } /** * Executes operations when subject is being removed. */ async remove(subjects) { if (!Array.isArray(subjects)) subjects = [subjects]; const metadata = subjects[0].metadata; const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const tableName = this.getTableName(metadata.tablePath); const leftColumnName = escape(metadata.nestedSetLeftColumn.databaseName); const rightColumnName = escape(metadata.nestedSetRightColumn.databaseName); const entitiesIds = []; for (const subject of subjects) { const entityId = metadata.getEntityIdMap(subject.entity); if (entityId) { entitiesIds.push(entityId); } } const entitiesNs = await this.getNestedSetIds(metadata, entitiesIds); for (const entity of entitiesNs) { const treeSize = entity.right - entity.left + 1; await this.queryRunner.query(`UPDATE ${tableName} ` + `SET ${leftColumnName} = CASE ` + `WHEN ${leftColumnName} > ${entity.left} THEN ${leftColumnName} - ${treeSize} ` + `ELSE ${leftColumnName} ` + `END, ` + `${rightColumnName} = CASE ` + `WHEN ${rightColumnName} > ${entity.right} THEN ${rightColumnName} - ${treeSize} ` + `ELSE ${rightColumnName} ` + `END`); } } /** * Get the nested set ids for a given entity */ getNestedSetIds(metadata, ids) { const select = { left: `${metadata.targetName}.${metadata.nestedSetLeftColumn.propertyPath}`, right: `${metadata.targetName}.${metadata.nestedSetRightColumn.propertyPath}`, }; const queryBuilder = this.queryRunner.manager.createQueryBuilder(); Object.entries(select).forEach(([key, value]) => { queryBuilder.addSelect(value, key); }); return queryBuilder .from(metadata.target, metadata.targetName) .whereInIds(ids) .orderBy(select.right, "DESC") .getRawMany() .then((results) => { const data = []; for (const result of results) { const entry = {}; for (const key of Object.keys(select)) { const value = result ? result[key] : undefined; // CockroachDB returns numeric types as string entry[key] = typeof value === "string" ? parseInt(value) : value; } data.push(entry); } return data; }); } async isUniqueRootEntity(subject, parent) { const escape = (alias) => this.queryRunner.connection.driver.escape(alias); const tableName = this.getTableName(subject.metadata.tablePath); const parameters = []; const whereCondition = subject.metadata .treeParentRelation.joinColumns.map((column) => { const columnName = escape(column.databaseName); const parameter = column.getEntityValue(parent); if (parameter == null) { return `${columnName} IS NULL`; } parameters.push(parameter); const parameterName = this.queryRunner.connection.driver.createParameter("entity_" + column.databaseName, parameters.length - 1); return `${columnName} = ${parameterName}`; }) .join(" AND "); const countAlias = "count"; const result = await this.queryRunner.query(`SELECT COUNT(1) AS ${escape(countAlias)} FROM ${tableName} WHERE ${whereCondition}`, parameters, true); return parseInt(result.records[0][countAlias]) === 0; } /** * 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.NestedSetSubjectExecutor = NestedSetSubjectExecutor; //# sourceMappingURL=NestedSetSubjectExecutor.js.map