UNPKG

ionic-orm-2

Version:

Data-mapper ORM for Ionic WebSQL and SQLite

371 lines 21.1 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 { TableSchema } from "./schema/TableSchema"; import { ColumnSchema } from "./schema/ColumnSchema"; import { ForeignKeySchema } from "./schema/ForeignKeySchema"; import { IndexSchema } from "./schema/IndexSchema"; import { PrimaryKeySchema } from "./schema/PrimaryKeySchema"; /** * Creates complete tables schemas in the database based on the entity metadatas. * * Steps how schema is being built: * 1. load list of all tables with complete column and keys information from the db * 2. drop all (old) foreign keys that exist in the table, but does not exist in the metadata * 3. create new tables that does not exist in the db, but exist in the metadata * 4. drop all columns exist (left old) in the db table, but does not exist in the metadata * 5. add columns from metadata which does not exist in the table * 6. update all exist columns which metadata has changed * 7. update primary keys - update old and create new primary key from changed columns * 8. create foreign keys which does not exist in the table yet * 9. create indices which are missing in db yet, and drops indices which exist in the db, but does not exist in the metadata anymore */ export class SchemaBuilder { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- /** * @param driver Driver needs to create a query runner * @param logger Used to log schema creation events * @param entityMetadatas All entities to create schema for * @param namingStrategy Naming strategy used to generate special names */ constructor(driver, logger, entityMetadatas, namingStrategy) { this.driver = driver; this.logger = logger; this.entityMetadatas = entityMetadatas; this.namingStrategy = namingStrategy; } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Creates complete schemas for the given entity metadatas. */ build() { return __awaiter(this, void 0, void 0, function* () { this.queryRunner = yield this.driver.createQueryRunner(); this.tableSchemas = yield this.loadTableSchemas(); yield this.queryRunner.beginTransaction(); try { yield this.dropOldForeignKeys(); // await this.dropOldPrimaryKeys(); // todo: need to drop primary column because column updates are not possible yield this.createNewTables(); yield this.dropRemovedColumns(); yield this.addNewColumns(); yield this.updateExistColumns(); yield this.updatePrimaryKeys(); yield this.createForeignKeys(); yield this.createIndices(); yield this.queryRunner.commitTransaction(); } catch (error) { yield this.queryRunner.rollbackTransaction(); throw error; } finally { yield this.queryRunner.release(); } }); } // ------------------------------------------------------------------------- // Private Methods // ------------------------------------------------------------------------- get entityToSyncMetadatas() { return this.entityMetadatas.filter(metadata => !metadata.table.skipSchemaSync); } /** * Loads all table schemas from the database. */ loadTableSchemas() { const tableNames = this.entityToSyncMetadatas.map(metadata => metadata.table.name); return this.queryRunner.loadSchemaTables(tableNames, this.namingStrategy); } /** * Drops all (old) foreign keys that exist in the table schemas, but do not exist in the entity metadata. */ dropOldForeignKeys() { return __awaiter(this, void 0, void 0, function* () { yield Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (!tableSchema) return; // find foreign keys that exist in the schemas but does not exist in the entity metadata const foreignKeySchemasToDrop = tableSchema.foreignKeys.filter(foreignKeySchema => { return !metadata.foreignKeys.find(metadataForeignKey => metadataForeignKey.name === foreignKeySchema.name); }); if (foreignKeySchemasToDrop.length === 0) return; this.logger.logSchemaBuild(`dropping old foreign keys of ${tableSchema.name}: ${foreignKeySchemasToDrop.map(dbForeignKey => dbForeignKey.name).join(", ")}`); // remove foreign keys from the table schema tableSchema.removeForeignKeys(foreignKeySchemasToDrop); // drop foreign keys from the database yield this.queryRunner.dropForeignKeys(tableSchema, foreignKeySchemasToDrop); }))); }); } /** * Creates tables that do not exist in the database yet. * New tables are created without foreign and primary keys. * Primary key only can be created in conclusion with auto generated column. */ createNewTables() { return __awaiter(this, void 0, void 0, function* () { yield Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { // check if table does not exist yet const existTableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (existTableSchema) return; this.logger.logSchemaBuild(`creating a new table: ${metadata.table.name}`); // create a new table schema and sync it in the database const tableSchema = new TableSchema(metadata.table.name, this.metadataColumnsToColumnSchemas(metadata.columns), true); this.tableSchemas.push(tableSchema); yield this.queryRunner.createTable(tableSchema); }))); }); } /** * Drops all columns that exist in the table, but does not exist in the metadata (left old). * We drop their keys too, since it should be safe. */ dropRemovedColumns() { return Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (!tableSchema) return; // find columns that exist in the database but does not exist in the metadata const droppedColumnSchemas = tableSchema.columns.filter(columnSchema => { return !metadata.columns.find(columnMetadata => columnMetadata.name === columnSchema.name); }); if (droppedColumnSchemas.length === 0) return; // drop all foreign keys that has column to be removed in its columns yield Promise.all(droppedColumnSchemas.map(droppedColumnSchema => { return this.dropColumnReferencedForeignKeys(metadata.table.name, droppedColumnSchema.name); })); // drop all indices that point to this column yield Promise.all(droppedColumnSchemas.map(droppedColumnSchema => { return this.dropColumnReferencedIndices(metadata.table.name, droppedColumnSchema.name); })); this.logger.logSchemaBuild(`columns dropped in ${tableSchema.name}: ` + droppedColumnSchemas.map(column => column.name).join(", ")); // remove columns from the table schema and primary keys of it if its used in the primary keys tableSchema.removeColumns(droppedColumnSchemas); tableSchema.removePrimaryKeysOfColumns(droppedColumnSchemas); // drop columns from the database yield this.queryRunner.dropColumns(tableSchema, droppedColumnSchemas); }))); } /** * Adds columns from metadata which does not exist in the table. * Columns are created without keys. */ addNewColumns() { return Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (!tableSchema) return; // find which columns are new const newColumnMetadatas = metadata.columns.filter(columnMetadata => { return !tableSchema.columns.find(columnSchema => columnSchema.name === columnMetadata.name); }); if (newColumnMetadatas.length === 0) return; this.logger.logSchemaBuild(`new columns added: ` + newColumnMetadatas.map(column => column.name).join(", ")); // create columns in the database const newColumnSchemas = this.metadataColumnsToColumnSchemas(newColumnMetadatas); tableSchema.addColumns(newColumnSchemas); yield this.queryRunner.createColumns(tableSchema, newColumnSchemas); }))); } /** * Update all exist columns which metadata has changed. * Still don't create keys. Also we don't touch foreign keys of the changed columns. */ updateExistColumns() { return Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (!tableSchema) return; const updatedColumnSchemas = tableSchema.findChangedColumns(this.queryRunner, metadata.columns); if (updatedColumnSchemas.length === 0) return; this.logger.logSchemaBuild(`columns changed in ${tableSchema.name}. updating: ` + updatedColumnSchemas.map(column => column.name).join(", ")); // drop all foreign keys that point to this column const dropRelatedForeignKeysPromises = updatedColumnSchemas .filter(changedColumnSchema => !!metadata.columns.find(columnMetadata => columnMetadata.name === changedColumnSchema.name)) .map(changedColumnSchema => this.dropColumnReferencedForeignKeys(metadata.table.name, changedColumnSchema.name)); // wait until all related foreign keys are dropped yield Promise.all(dropRelatedForeignKeysPromises); // drop all indices that point to this column const dropRelatedIndicesPromises = updatedColumnSchemas .filter(changedColumnSchema => !!metadata.columns.find(columnMetadata => columnMetadata.name === changedColumnSchema.name)) .map(changedColumnSchema => this.dropColumnReferencedIndices(metadata.table.name, changedColumnSchema.name)); // wait until all related indices are dropped yield Promise.all(dropRelatedIndicesPromises); // generate a map of new/old columns const newAndOldColumnSchemas = updatedColumnSchemas.map(changedColumnSchema => { const columnMetadata = metadata.columns.find(column => column.name === changedColumnSchema.name); const newColumnSchema = ColumnSchema.create(columnMetadata, this.queryRunner.normalizeType(columnMetadata)); tableSchema.replaceColumn(changedColumnSchema, newColumnSchema); return { newColumn: newColumnSchema, oldColumn: changedColumnSchema }; }); return this.queryRunner.changeColumns(tableSchema, newAndOldColumnSchemas); }))); } /** * Creates primary keys which does not exist in the table yet. */ updatePrimaryKeys() { return Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name && !table.justCreated); if (!tableSchema) return; const metadataPrimaryColumns = metadata.columns.filter(column => column.isPrimary && !column.isGenerated); const addedKeys = metadataPrimaryColumns .filter(primaryKey => { return !tableSchema.primaryKeysWithoutGenerated.find(dbPrimaryKey => dbPrimaryKey.columnName === primaryKey.name); }) .map(primaryKey => new PrimaryKeySchema("", primaryKey.name)); const droppedKeys = tableSchema.primaryKeysWithoutGenerated.filter(primaryKeySchema => { return !metadataPrimaryColumns.find(primaryKeyMetadata => primaryKeyMetadata.name === primaryKeySchema.columnName); }); if (addedKeys.length === 0 && droppedKeys.length === 0) return; this.logger.logSchemaBuild(`primary keys of ${tableSchema.name} has changed: dropped - ${droppedKeys.map(key => key.columnName).join(", ") || "nothing"}; added - ${addedKeys.map(key => key.columnName).join(", ") || "nothing"}`); tableSchema.addPrimaryKeys(addedKeys); tableSchema.removePrimaryKeys(droppedKeys); yield this.queryRunner.updatePrimaryKeys(tableSchema); }))); } /** * Creates foreign keys which does not exist in the table yet. */ createForeignKeys() { return Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (!tableSchema) return; const newKeys = metadata.foreignKeys.filter(foreignKey => { return !tableSchema.foreignKeys.find(dbForeignKey => dbForeignKey.name === foreignKey.name); }); if (newKeys.length === 0) return; const dbForeignKeys = newKeys.map(foreignKeyMetadata => ForeignKeySchema.create(foreignKeyMetadata)); this.logger.logSchemaBuild(`creating a foreign keys: ${newKeys.map(key => key.name).join(", ")}`); yield this.queryRunner.createForeignKeys(tableSchema, dbForeignKeys); tableSchema.addForeignKeys(dbForeignKeys); }))); } /** * Creates indices which are missing in db yet, and drops indices which exist in the db, * but does not exist in the metadata anymore. */ createIndices() { // return Promise.all(this.entityMetadatas.map(metadata => this.createIndices(metadata.table, metadata.indices))); return Promise.all(this.entityToSyncMetadatas.map((metadata) => __awaiter(this, void 0, void 0, function* () { const tableSchema = this.tableSchemas.find(table => table.name === metadata.table.name); if (!tableSchema) return; // drop all indices that exist in the table, but does not exist in the given composite indices const dropQueries = tableSchema.indices .filter(indexSchema => !metadata.indices.find(indexMetadata => indexMetadata.name === indexSchema.name)) .map((indexSchema) => __awaiter(this, void 0, void 0, function* () { this.logger.logSchemaBuild(`dropping an index: ${indexSchema.name}`); tableSchema.removeIndex(indexSchema); yield this.queryRunner.dropIndex(metadata.table.name, indexSchema.name); })); // then create table indices for all composite indices we have const addQueries = metadata.indices .filter(indexMetadata => !tableSchema.indices.find(indexSchema => indexSchema.name === indexMetadata.name)) .map((indexMetadata) => __awaiter(this, void 0, void 0, function* () { const indexSchema = IndexSchema.create(indexMetadata); tableSchema.indices.push(indexSchema); this.logger.logSchemaBuild(`adding new index: ${indexSchema.name}`); yield this.queryRunner.createIndex(indexSchema); })); yield Promise.all(dropQueries.concat(addQueries)); }))); } /** * Drops all indices where given column of the given table is being used. */ dropColumnReferencedIndices(tableName, columnName) { return __awaiter(this, void 0, void 0, function* () { const allIndexMetadatas = this.entityMetadatas.reduce((all, metadata) => all.concat(metadata.indices), []); const tableSchema = this.tableSchemas.find(table => table.name === tableName); if (!tableSchema) return; // console.log(allIndexMetadatas); // find depend indices to drop them const dependIndices = allIndexMetadatas.filter(indexMetadata => { return indexMetadata.tableName === tableName && indexMetadata.columns.indexOf(columnName) !== -1; }); if (!dependIndices.length) return; const dependIndicesInTable = tableSchema.indices.filter(indexSchema => { return !!dependIndices.find(indexMetadata => indexSchema.name === indexMetadata.name); }); if (dependIndicesInTable.length === 0) return; this.logger.logSchemaBuild(`dropping related indices of ${tableName}#${columnName}: ${dependIndicesInTable.map(index => index.name).join(", ")}`); const dropPromises = dependIndicesInTable.map(index => { tableSchema.removeIndex(index); return this.queryRunner.dropIndex(tableSchema.name, index.name); }); yield Promise.all(dropPromises); }); } /** * Drops all foreign keys where given column of the given table is being used. */ dropColumnReferencedForeignKeys(tableName, columnName) { return __awaiter(this, void 0, void 0, function* () { const allForeignKeyMetadatas = this.entityMetadatas.reduce((all, metadata) => all.concat(metadata.foreignKeys), []); const tableSchema = this.tableSchemas.find(table => table.name === tableName); if (!tableSchema) return; // find depend foreign keys to drop them const dependForeignKeys = allForeignKeyMetadatas.filter(foreignKey => { if (foreignKey.tableName === tableName) { return !!foreignKey.columns.find(fkColumn => { return fkColumn.name === columnName; }); } else if (foreignKey.referencedTableName === tableName) { return !!foreignKey.referencedColumns.find(fkColumn => { return fkColumn.name === columnName; }); } return false; }); if (!dependForeignKeys.length) return; const dependForeignKeyInTable = dependForeignKeys.filter(fk => { return !!tableSchema.foreignKeys.find(dbForeignKey => dbForeignKey.name === fk.name); }); if (dependForeignKeyInTable.length === 0) return; this.logger.logSchemaBuild(`dropping related foreign keys of ${tableName}#${columnName}: ${dependForeignKeyInTable.map(foreignKey => foreignKey.name).join(", ")}`); const foreignKeySchemas = dependForeignKeyInTable.map(foreignKeyMetadata => ForeignKeySchema.create(foreignKeyMetadata)); // todo: why created again, when exist in the tableSchema.foreignKeys ?! tableSchema.removeForeignKeys(foreignKeySchemas); yield this.queryRunner.dropForeignKeys(tableSchema, foreignKeySchemas); }); } /** * Creates new column schemas from the given column metadatas. */ metadataColumnsToColumnSchemas(columns) { return columns.map(columnMetadata => { return ColumnSchema.create(columnMetadata, this.queryRunner.normalizeType(columnMetadata)); }); } } //# sourceMappingURL=SchemaBuilder.js.map