added migrations execute and revert functionality

This commit is contained in:
Umed Khudoiberdiev 2016-12-12 12:49:05 +05:00
parent 31d5a76f5f
commit 0f9287ad8e
22 changed files with 565 additions and 107 deletions

View File

@ -2,6 +2,7 @@ import "reflect-metadata";
import {ConnectionOptions, createConnection} from "../../src/index";
import {Post} from "./entity/Post";
import {Author} from "./entity/Author";
import {MigrationExecutor} from "../../src/migration/MigrationExecutor";
const options: ConnectionOptions = {
driver: {
@ -14,15 +15,14 @@ const options: ConnectionOptions = {
},
autoSchemaSync: true,
logging: {
// logQueries: true,
// logSchemaCreation: true,
// logFailedQueryError: true
logQueries: true,
},
entities: [Post, Author],
};
createConnection(options).then(async connection => {
// first insert all the data
let author = new Author();
author.firstName = "Umed";
author.lastName = "Khudoiberdiev";
@ -34,6 +34,42 @@ createConnection(options).then(async connection => {
let postRepository = connection.getRepository(Post);
await postRepository.persist(post);
console.log("Post has been saved");
console.log("Database schema was created and data has been inserted into the database.");
// close connection now
await connection.close();
// now create a new connection
connection = await createConnection({
name: "mysql",
driver: {
type: "mysql",
host: "localhost",
port: 3306,
username: "test",
password: "test",
database: "test"
},
logging: {
logQueries: true
},
entities: [
Post,
Author
],
migrations: [
__dirname + "/migrations/*{.js,.ts}"
]
});
// run all migrations
const migrationExecutor = new MigrationExecutor(connection);
await migrationExecutor.executePendingMigrations();
// and undo migrations two times (because we have two migrations)
await migrationExecutor.undoLastMigration();
await migrationExecutor.undoLastMigration();
console.log("Done. We run two migrations then reverted them.");
}).catch(error => console.log("Error: ", error));

View File

@ -2,7 +2,7 @@ import {MigrationInterface} from "../../../src/migration/MigrationInterface";
import {Connection} from "../../../src/connection/Connection";
import {QueryRunner} from "../../../src/query-runner/QueryRunner";
export class FirstReleaseMigration implements MigrationInterface {
export class FirstReleaseMigration1481283582 implements MigrationInterface {
async up(queryRunner: QueryRunner, connection: Connection): Promise<any> {
await queryRunner.query("ALTER TABLE `post` CHANGE `title` `name` VARCHAR(255)");

View File

@ -0,0 +1,15 @@
import {MigrationInterface} from "../../../src/migration/MigrationInterface";
import {Connection} from "../../../src/connection/Connection";
import {QueryRunner} from "../../../src/query-runner/QueryRunner";
export class SecondReleaseMigration1481521933 implements MigrationInterface {
async up(queryRunner: QueryRunner, connection: Connection): Promise<any> {
await queryRunner.query("ALTER TABLE `post` CHANGE `name` `title` VARCHAR(500)");
}
async down(queryRunner: QueryRunner, connection: Connection): Promise<any> {
await queryRunner.query("ALTER TABLE `post` CHANGE `title` `name` VARCHAR(255)");
}
}

View File

@ -28,6 +28,7 @@ import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {Logger} from "../logger/Logger";
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
import {EntityMetadataNotFound} from "../metadata-args/error/EntityMetadataNotFound";
import {MigrationInterface} from "../migration/MigrationInterface";
/**
* Connection is a single database connection to a specific database of a database management system.
@ -108,6 +109,11 @@ export class Connection {
*/
private readonly namingStrategyClasses: Function[] = [];
/**
* Registered migration classes to be used for this connection.
*/
private readonly migrationClasses: Function[] = [];
/**
* Naming strategy to be used in this connection.
*/
@ -252,6 +258,14 @@ export class Connection {
return this;
}
/**
* Imports migrations from the given paths (directories) and registers them in the current connection.
*/
importMigrationsFromDirectories(paths: string[]): this {
this.importMigrations(importClassesFromDirectories(paths));
return this;
}
/**
* Imports entities and registers them in the current connection.
*/
@ -296,6 +310,17 @@ export class Connection {
return this;
}
/**
* Imports migrations and registers them in the current connection.
*/
importMigrations(migrations: Function[]): this {
if (this.isConnected)
throw new CannotImportAlreadyConnectedError("migrations", this.name);
migrations.forEach(cls => this.migrationClasses.push(cls));
return this;
}
/**
* Sets given naming strategy to be used.
* Naming strategy must be set to be used before connection is established.
@ -430,6 +455,19 @@ export class Connection {
return new EntityManager(this, queryRunnerProvider);
}
/**
* Gets migration instances that are registered for this connection.
*/
getMigrations(): MigrationInterface[] {
if (this.migrationClasses && this.migrationClasses.length) {
return this.migrationClasses.map(migrationClass => {
return getFromContainer<MigrationInterface>(migrationClass);
});
}
return [];
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -99,6 +99,14 @@ export class ConnectionManager {
.importNamingStrategiesFromDirectories(directories);
}
// import migrations
if (options.migrations) {
const [directories, classes] = this.splitStringsAndClasses(options.migrations);
connection
.importMigrations(classes)
.importMigrationsFromDirectories(directories);
}
// set naming strategy to be used for this connection
if (options.usedNamingStrategy)
connection.useNamingStrategy(options.usedNamingStrategy as any);

View File

@ -53,6 +53,13 @@ export interface ConnectionOptions {
*/
readonly entitySchemas?: EntitySchema[]|string[];
/**
* Migrations to be loaded for this connection.
* Accepts both migration classes and directories where from migrations need to be loaded.
* Directories support glob patterns.
*/
readonly migrations?: Function[]|string[];
/**
* Logging options.
*/

View File

@ -14,6 +14,7 @@ import {PrimaryKeySchema} from "../../schema-builder/schema/PrimaryKeySchema";
import {IndexSchema} from "../../schema-builder/schema/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {ColumnType} from "../../metadata/types/ColumnTypes";
/**
* Runs queries on a single mysql database connection.
@ -608,35 +609,36 @@ export class MysqlQueryRunner implements QueryRunner {
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) {
switch (typeOptions.type) {
case "string":
return "varchar(" + (column.length ? column.length : 255) + ")";
return "varchar(" + (typeOptions.length ? typeOptions.length : 255) + ")";
case "text":
return "text";
case "boolean":
return "tinyint(1)";
case "integer":
case "int":
return "int(" + (column.length ? column.length : 11) + ")";
return "int(" + (typeOptions.length ? typeOptions.length : 11) + ")";
case "smallint":
return "smallint(" + (column.length ? column.length : 11) + ")";
return "smallint(" + (typeOptions.length ? typeOptions.length : 11) + ")";
case "bigint":
return "bigint(" + (column.length ? column.length : 11) + ")";
return "bigint(" + (typeOptions.length ? typeOptions.length : 11) + ")";
case "float":
return "float";
case "double":
case "number":
return "double";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
if (typeOptions.precision && typeOptions.scale) {
return `decimal(${typeOptions.precision},${typeOptions.scale})`;
} else if (column.scale) {
return `decimal(${column.scale})`;
} else if (typeOptions.scale) {
return `decimal(${typeOptions.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else if (typeOptions.precision) {
return `decimal(${typeOptions.precision})`;
} else {
return "decimal";
@ -651,10 +653,10 @@ export class MysqlQueryRunner implements QueryRunner {
case "json":
return "text";
case "simple_array":
return column.length ? "varchar(" + column.length + ")" : "text";
return typeOptions.length ? "varchar(" + typeOptions.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "MySQL");
throw new DataTypeNotSupportedByDriverError(typeOptions.type, "MySQL");
}
// -------------------------------------------------------------------------

View File

@ -14,6 +14,7 @@ import {PrimaryKeySchema} from "../../schema-builder/schema/PrimaryKeySchema";
import {IndexSchema} from "../../schema-builder/schema/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {ColumnType} from "../../metadata/types/ColumnTypes";
/**
* Runs queries on a single mysql database connection.
@ -664,10 +665,10 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) {
switch (typeOptions.type) {
case "string":
return "varchar2(" + (column.length ? column.length : 255) + ")";
return "varchar2(" + (typeOptions.length ? typeOptions.length : 255) + ")";
case "text":
return "clob";
case "boolean":
@ -676,12 +677,12 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE
case "int":
// if (column.isGenerated)
// return `number(22)`;
if (column.precision && column.scale)
return `number(${column.precision},${column.scale})`;
if (column.precision)
return `number(${column.precision},0)`;
if (column.scale)
return `number(0,${column.scale})`;
if (typeOptions.precision && typeOptions.scale)
return `number(${typeOptions.precision},${typeOptions.scale})`;
if (typeOptions.precision)
return `number(${typeOptions.precision},0)`;
if (typeOptions.scale)
return `number(0,${typeOptions.scale})`;
return "number(10,0)";
case "smallint":
@ -689,26 +690,26 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE
case "bigint":
return "number(20)";
case "float":
if (column.precision && column.scale)
return `float(${column.precision},${column.scale})`;
if (column.precision)
return `float(${column.precision},0)`;
if (column.scale)
return `float(0,${column.scale})`;
if (typeOptions.precision && typeOptions.scale)
return `float(${typeOptions.precision},${typeOptions.scale})`;
if (typeOptions.precision)
return `float(${typeOptions.precision},0)`;
if (typeOptions.scale)
return `float(0,${typeOptions.scale})`;
return `float(126)`;
case "double":
case "number":
return "float(126)";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
if (typeOptions.precision && typeOptions.scale) {
return `decimal(${typeOptions.precision},${typeOptions.scale})`;
} else if (column.scale) {
return `decimal(0,${column.scale})`;
} else if (typeOptions.scale) {
return `decimal(0,${typeOptions.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else if (typeOptions.precision) {
return `decimal(${typeOptions.precision})`;
} else {
return "decimal";
@ -722,10 +723,10 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE
case "json":
return "clob";
case "simple_array":
return column.length ? "varchar2(" + column.length + ")" : "text";
return typeOptions.length ? "varchar2(" + typeOptions.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "Oracle");
throw new DataTypeNotSupportedByDriverError(typeOptions.type, "Oracle");
}
// -------------------------------------------------------------------------

View File

@ -14,6 +14,7 @@ import {ForeignKeySchema} from "../../schema-builder/schema/ForeignKeySchema";
import {PrimaryKeySchema} from "../../schema-builder/schema/PrimaryKeySchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {ColumnType} from "../../metadata/types/ColumnTypes";
/**
* Runs queries on a single postgres database connection.
@ -663,10 +664,10 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`;
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata): string {
switch (column.normalizedDataType) {
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }): string {
switch (typeOptions.type) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";
return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")";
case "text":
return "text";
case "boolean":
@ -684,14 +685,14 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`;
case "number":
return "double precision";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
if (typeOptions.precision && typeOptions.scale) {
return `decimal(${typeOptions.precision},${typeOptions.scale})`;
} else if (column.scale) {
return `decimal(${column.scale})`;
} else if (typeOptions.scale) {
return `decimal(${typeOptions.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else if (typeOptions.precision) {
return `decimal(${typeOptions.precision})`;
} else {
return "decimal";
@ -700,13 +701,13 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`;
case "date":
return "date";
case "time":
if (column.timezone) {
if (typeOptions.timezone) {
return "time with time zone";
} else {
return "time without time zone";
}
case "datetime":
if (column.timezone) {
if (typeOptions.timezone) {
return "timestamp with time zone";
} else {
return "timestamp without time zone";
@ -714,10 +715,10 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`;
case "json":
return "json";
case "simple_array":
return column.length ? "character varying(" + column.length + ")" : "text";
return typeOptions.length ? "character varying(" + typeOptions.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "Postgres");
throw new DataTypeNotSupportedByDriverError(typeOptions.type, "Postgres");
}
// -------------------------------------------------------------------------

View File

@ -92,7 +92,7 @@ export class SqliteDriver implements Driver {
// we need to enable foreign keys in sqlite to make sure all foreign key related features
// working properly. this also makes onDelete to work with sqlite.
connection.run(`PRAGMA foreign_keys = ON;`, (err: any, result: any) => {
connection.executePendingMigrations(`PRAGMA foreign_keys = ON;`, (err: any, result: any) => {
ok();
});
});

View File

@ -14,6 +14,7 @@ import {ForeignKeySchema} from "../../schema-builder/schema/ForeignKeySchema";
import {PrimaryKeySchema} from "../../schema-builder/schema/PrimaryKeySchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {ColumnType} from "../../metadata/types/ColumnTypes";
/**
* Runs queries on a single sqlite database connection.
@ -167,7 +168,7 @@ export class SqliteQueryRunner implements QueryRunner {
this.logger.logQuery(sql, parameters);
return new Promise<any[]>((ok, fail) => {
const __this = this;
this.databaseConnection.connection.run(sql, parameters, function (err: any): void {
this.databaseConnection.connection.executePendingMigrations(sql, parameters, function (err: any): void {
if (err) {
__this.logger.logFailedQuery(sql, parameters);
__this.logger.logQueryError(err);
@ -650,10 +651,10 @@ export class SqliteQueryRunner implements QueryRunner {
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) {
switch (typeOptions.type) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";
return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")";
case "text":
return "text";
case "boolean":
@ -671,14 +672,14 @@ export class SqliteQueryRunner implements QueryRunner {
case "number":
return "double precision";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
if (typeOptions.precision && typeOptions.scale) {
return `decimal(${typeOptions.precision},${typeOptions.scale})`;
} else if (column.scale) {
return `decimal(${column.scale})`;
} else if (typeOptions.scale) {
return `decimal(${typeOptions.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else if (typeOptions.precision) {
return `decimal(${typeOptions.precision})`;
} else {
return "decimal";
@ -687,13 +688,13 @@ export class SqliteQueryRunner implements QueryRunner {
case "date":
return "date";
case "time":
if (column.timezone) {
if (typeOptions.timezone) {
return "time with time zone";
} else {
return "time without time zone";
}
case "datetime":
if (column.timezone) {
if (typeOptions.timezone) {
return "timestamp with time zone";
} else {
return "timestamp without time zone";
@ -701,10 +702,10 @@ export class SqliteQueryRunner implements QueryRunner {
case "json":
return "json";
case "simple_array":
return column.length ? "character varying(" + column.length + ")" : "text";
return typeOptions.length ? "character varying(" + typeOptions.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "SQLite");
throw new DataTypeNotSupportedByDriverError(typeOptions.type, "SQLite");
}
// -------------------------------------------------------------------------

View File

@ -14,6 +14,7 @@ import {PrimaryKeySchema} from "../../schema-builder/schema/PrimaryKeySchema";
import {IndexSchema} from "../../schema-builder/schema/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {ColumnType} from "../../metadata/types/ColumnTypes";
/**
* Runs queries on a single mysql database connection.
@ -713,10 +714,10 @@ WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_C
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) {
switch (typeOptions.type) {
case "string":
return "nvarchar(" + (column.length ? column.length : 255) + ")";
return "nvarchar(" + (typeOptions.length ? typeOptions.length : 255) + ")";
case "text":
return "ntext";
case "boolean":
@ -755,10 +756,10 @@ WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_C
case "json":
return "text";
case "simple_array":
return column.length ? "nvarchar(" + column.length + ")" : "text";
return typeOptions.length ? "nvarchar(" + typeOptions.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "MySQL");
throw new DataTypeNotSupportedByDriverError(typeOptions.type, "SQLServer");
}
// -------------------------------------------------------------------------

View File

@ -12,6 +12,7 @@ import {ForeignKeySchema} from "../../schema-builder/schema/ForeignKeySchema";
import {IndexSchema} from "../../schema-builder/schema/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {WebsqlDriver} from "./WebsqlDriver";
import {ColumnType} from "../../metadata/types/ColumnTypes";
/**
* Runs queries on a single websql database connection.
@ -659,10 +660,10 @@ export class WebsqlQueryRunner implements QueryRunner {
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) {
switch (typeOptions.type) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";
return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")";
case "text":
return "text";
case "boolean":
@ -680,14 +681,14 @@ export class WebsqlQueryRunner implements QueryRunner {
case "number":
return "double precision";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
if (typeOptions.precision && typeOptions.scale) {
return `decimal(${typeOptions.precision},${typeOptions.scale})`;
} else if (column.scale) {
return `decimal(${column.scale})`;
} else if (typeOptions.scale) {
return `decimal(${typeOptions.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else if (typeOptions.precision) {
return `decimal(${typeOptions.precision})`;
} else {
return "decimal";
@ -696,13 +697,13 @@ export class WebsqlQueryRunner implements QueryRunner {
case "date":
return "date";
case "time":
if (column.timezone) {
if (typeOptions.timezone) {
return "time with time zone";
} else {
return "time without time zone";
}
case "datetime":
if (column.timezone) {
if (typeOptions.timezone) {
return "timestamp with time zone";
} else {
return "timestamp without time zone";
@ -710,10 +711,10 @@ export class WebsqlQueryRunner implements QueryRunner {
case "json":
return "json";
case "simple_array":
return column.length ? "character varying(" + column.length + ")" : "text";
return typeOptions.length ? "character varying(" + typeOptions.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "SQLite");
throw new DataTypeNotSupportedByDriverError(typeOptions.type, "WebSQL");
}
// -------------------------------------------------------------------------

View File

@ -306,17 +306,4 @@ export class ColumnMetadata {
}
}
get normalizedDataType() {
if (typeof this.type === "string") {
return this.type.toLowerCase();
} else if (typeof this.type === "object" &&
(this.type as any).name &&
typeof (this.type as any).name === "string") {
return (this.type as any).toLowerCase(); // todo: shouldnt be a .name here?
}
throw new Error(`Column data type cannot be normalized. Make sure you have supplied a correct column type.`);
}
}

View File

@ -0,0 +1,37 @@
import {MigrationInterface} from "./MigrationInterface";
/**
* Represents entity of the migration in the database.
*/
export class Migration {
// -------------------------------------------------------------------------
// Public Properties
// -------------------------------------------------------------------------
/**
* Timestamp of the migration.
*/
timestamp: number;
/**
* Name of the migration (class name).
*/
name: string;
/**
* Migration instance that needs to be run.
*/
instance?: MigrationInterface;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(timestamp: number, name: string, instance?: MigrationInterface) {
this.timestamp = timestamp;
this.name = name;
this.instance = instance;
}
}

View File

@ -0,0 +1,262 @@
import {TableSchema} from "../schema-builder/schema/TableSchema";
import {ColumnSchema} from "../schema-builder/schema/ColumnSchema";
import {ColumnTypes} from "../metadata/types/ColumnTypes";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {Connection} from "../connection/Connection";
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
import {Migration} from "./Migration";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {PromiseUtils} from "../util/PromiseUtils";
/**
* Executes migrations: runs pending and reverts previously executed migrations.
*/
export class MigrationExecutor {
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
protected queryRunnerProvider: QueryRunnerProvider;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected connection: Connection, queryRunnerProvider?: QueryRunnerProvider) {
this.queryRunnerProvider = queryRunnerProvider || new QueryRunnerProvider(connection.driver, true);
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Executes all pending migrations. Pending migrations are migrations that are not yet executed,
* thus not saved in the database.
*/
async executePendingMigrations(): Promise<void> {
const queryRunner = await this.queryRunnerProvider.provide();
// create migrations table if its not created yet
await this.createMigrationsTableIfNotExist();
// get all migrations that are executed and saved in the database
const executedMigrations = await this.loadExecutedMigrations();
// get the time when last migration was executed
let lastTimeExecutedMigration = this.getLatestMigration(executedMigrations);
// get all user's migrations in the source code
const allMigrations = this.getMigrations();
// find all migrations that needs to be executed
const pendingMigrations = allMigrations.filter(migration => {
// check if we already have executed migration
const executedMigration = executedMigrations.find(executedMigration => executedMigration.name === migration.name);
if (executedMigration)
return false;
// migration is new and not executed. now check if its timestamp is correct
if (lastTimeExecutedMigration && migration.timestamp < lastTimeExecutedMigration.timestamp)
throw new Error(`New migration found: ${migration.name}, however this migration's timestamp is not valid. Migration's timestamp should not be older then migrations already executed in the database.`);
// every check is passed means that migration was not run yet and we need to run it
return true;
});
// if no migrations are pending then nothing to do here
if (!pendingMigrations.length) {
this.connection.logger.log("info", `No migrations are pending`);
return;
}
// log information about migration execution
this.connection.logger.log("info", `${executedMigrations.length} migrations are already loaded in the database.`);
this.connection.logger.log("info", `${allMigrations.length} migrations were found in the source code.`);
if (lastTimeExecutedMigration)
this.connection.logger.log("info", `${lastTimeExecutedMigration.name} is the last executed migration. It was executed on ${new Date(lastTimeExecutedMigration.timestamp * 1000).toString()}.`);
this.connection.logger.log("info", `${pendingMigrations.length} migrations are new migrations that needs to be executed.`);
// start transaction if its not started yet
let transactionStartedByUs = false;
if (!queryRunner.isTransactionActive()) {
await queryRunner.beginTransaction();
transactionStartedByUs = true;
}
// run all pending migrations in a sequence
try {
await PromiseUtils.runInSequence(pendingMigrations, migration => {
return migration.instance!.up(queryRunner, this.connection)
.then(() => { // now when migration is executed we need to insert record about it into the database
return this.insertExecutedMigration(migration);
})
.then(() => { // informative log about migration success
this.connection.logger.log("info", `Migration ${migration.name} has been executed successfully.`);
});
});
// commit transaction if we started it
if (transactionStartedByUs)
await queryRunner.commitTransaction();
} catch (err) { // rollback transaction if we started it
if (transactionStartedByUs)
await queryRunner.rollbackTransaction();
throw err;
}
}
/**
* Reverts last migration that were run.
*/
async undoLastMigration(): Promise<void> {
const queryRunner = await this.queryRunnerProvider.provide();
// create migrations table if its not created yet
await this.createMigrationsTableIfNotExist();
// get all migrations that are executed and saved in the database
const executedMigrations = await this.loadExecutedMigrations();
// get the time when last migration was executed
let lastTimeExecutedMigration = this.getLatestMigration(executedMigrations);
// if no migrations found in the database then nothing to revert
if (!lastTimeExecutedMigration) {
this.connection.logger.log("info", `No migrations was found in the database. Nothing to revert!`);
return;
}
// get all user's migrations in the source code
const allMigrations = this.getMigrations();
// find the instance of the migration we need to remove
const migrationToRevert = allMigrations.find(migration => migration.name === lastTimeExecutedMigration!.name);
// if no migrations found in the database then nothing to revert
if (!migrationToRevert)
throw new Error(`No migration ${lastTimeExecutedMigration.name} was found in the source code. Make sure you have this migration in your codebase and its included in the connection options.`);
// log information about migration execution
this.connection.logger.log("info", `${executedMigrations.length} migrations are already loaded in the database.`);
this.connection.logger.log("info", `${lastTimeExecutedMigration.name} is the last executed migration. It was executed on ${new Date(lastTimeExecutedMigration.timestamp * 1000).toString()}.`);
this.connection.logger.log("info", `Now reverting it...`);
// start transaction if its not started yet
let transactionStartedByUs = false;
if (!queryRunner.isTransactionActive()) {
await queryRunner.beginTransaction();
transactionStartedByUs = true;
}
try {
await migrationToRevert.instance!.down(queryRunner, this.connection);
await this.deleteExecutedMigration(migrationToRevert);
this.connection.logger.log("info", `Migration ${migrationToRevert.name} has been reverted successfully.`);
// commit transaction if we started it
if (transactionStartedByUs)
await queryRunner.commitTransaction();
} catch (err) { // rollback transaction if we started it
if (transactionStartedByUs)
await queryRunner.rollbackTransaction();
throw err;
}
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Creates table "migrations" that will store information about executed migrations.
*/
protected async createMigrationsTableIfNotExist(): Promise<void> {
const queryRunner = await this.queryRunnerProvider.provide();
const tableExist = await queryRunner.hasTable("migrations"); // todo: table name should be configurable
if (!tableExist) {
await queryRunner.createTable(new TableSchema("migrations", [
new ColumnSchema({
name: "timestamp",
type: queryRunner.normalizeType({
type: ColumnTypes.NUMBER
}),
isPrimary: true,
isNullable: false
}),
new ColumnSchema({
name: "name",
type: queryRunner.normalizeType({
type: ColumnTypes.STRING
}),
isNullable: false
}),
]));
}
}
/**
* Loads all migrations that were executed and saved into the database.
*/
protected async loadExecutedMigrations(): Promise<Migration[]> {
const migrationsRaw = await new QueryBuilder(this.connection, this.queryRunnerProvider)
.select()
.fromTable("migrations", "migrations")
.getScalarMany<ObjectLiteral>();
return migrationsRaw.map(migrationRaw => {
return new Migration(parseInt(migrationRaw["timestamp"]), migrationRaw["name"]);
});
}
/**
* Gets all migrations that setup for this connection.
*/
protected getMigrations(): Migration[] {
return this.connection.getMigrations().map(migration => {
const migrationClassName = (migration.constructor as any).name;
const migrationTimestamp = parseInt(migrationClassName.substr(-10));
if (!migrationTimestamp)
throw new Error(`Migration class name should contain a class name at the end of the file. ${migrationClassName} migration name is wrong.`);
return new Migration(migrationTimestamp, migrationClassName, migration);
});
}
/**
* Finds the latest migration (sorts by timestamp) in the given array of migrations.
*/
protected getLatestMigration(migrations: Migration[]): Migration|undefined {
const sortedMigrations = migrations.map(migration => migration).sort((a, b) => (a.timestamp - b.timestamp) * -1);
return sortedMigrations.length > 0 ? sortedMigrations[0] : undefined;
}
/**
* Inserts new executed migration's data into migrations table.
*/
protected async insertExecutedMigration(migration: Migration): Promise<void> {
const queryRunner = await this.queryRunnerProvider.provide();
await queryRunner.insert("migrations", {
timestamp: migration.timestamp,
name: migration.name,
});
}
/**
* Delete previously executed migration's data from the migrations table.
*/
protected async deleteExecutedMigration(migration: Migration): Promise<void> {
const queryRunner = await this.queryRunnerProvider.provide();
await queryRunner.delete("migrations", {
timestamp: migration.timestamp,
name: migration.name,
});
}
}

View File

@ -1,6 +1,9 @@
import {Connection} from "../connection/Connection";
import {QueryRunner} from "../query-runner/QueryRunner";
/**
* Migrations should implement this interface and all its methods.
*/
export interface MigrationInterface {
/**

View File

@ -1095,14 +1095,14 @@ export class QueryBuilder<Entity> {
/**
* Gets all scalar results returned by execution of generated query builder sql.
*/
getScalarMany<T>(): Promise<T[]> {
getScalarMany<T>(): Promise<T[]> { // todo: rename to getRawMany
return this.execute();
}
/**
* Gets first scalar result returned by execution of generated query builder sql.
*/
getScalarOne<T>(): Promise<T> {
getScalarOne<T>(): Promise<T> { // todo: rename to getRawOne
return this.getScalarMany().then(results => results[0]);
}

View File

@ -3,6 +3,7 @@ import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {TableSchema} from "../schema-builder/schema/TableSchema";
import {ForeignKeySchema} from "../schema-builder/schema/ForeignKeySchema";
import {IndexSchema} from "../schema-builder/schema/IndexSchema";
import {ColumnType} from "../metadata/types/ColumnTypes";
/**
* Runs queries on a single database connection.
@ -76,7 +77,7 @@ export interface QueryRunner {
/**
* Converts a column type of the metadata to the database column's type.
*/
normalizeType(column: ColumnMetadata): any;
normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }): any;
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.

View File

@ -49,6 +49,32 @@ export class ColumnSchema {
*/
comment: string|undefined;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(options?: {
name?: string,
type?: string,
default?: string,
isNullable?: boolean,
isGenerated?: boolean,
isPrimary?: boolean,
isUnique?: boolean,
comment?: string
}) {
if (options) {
this.name = options.name || "";
this.type = options.type || "";
this.default = options.default || "";
this.isNullable = options.isNullable || false;
this.isGenerated = options.isGenerated || false;
this.isPrimary = options.isPrimary || false;
this.isUnique = options.isUnique || false;
this.comment = options.comment;
}
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------

View File

@ -4,6 +4,7 @@ import {ForeignKeySchema} from "./ForeignKeySchema";
import {PrimaryKeySchema} from "./PrimaryKeySchema";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {ObjectLiteral} from "../../common/ObjectLiteral";
/**
* Table schema in the database represented in this class.
@ -50,10 +51,17 @@ export class TableSchema {
// Constructor
// -------------------------------------------------------------------------
constructor(name: string, columns?: ColumnSchema[], justCreated?: boolean) {
constructor(name: string, columns?: ColumnSchema[]|ObjectLiteral[], justCreated?: boolean) {
this.name = name;
if (columns)
this.columns = columns;
if (columns) {
this.columns = columns.map(column => {
if (column instanceof ColumnSchema) {
return column;
} else {
return new ColumnSchema(column);
}
});
}
if (justCreated !== undefined)
this.justCreated = justCreated;

23
src/util/PromiseUtils.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* Utils to help to work with Promise objects.
*/
export class PromiseUtils {
/**
* Runs given callback that returns promise for each item in the given collection in order.
* Operations executed after each other, right after previous promise being resolved.
*/
static runInSequence<T, U>(collection: T[], callback: (item: T) => Promise<U>): Promise<U[]> {
const results: U[] = [];
return collection.reduce((promise, item) => {
return promise.then(() => {
return callback(item);
}).then(result => {
results.push(result);
});
}, Promise.resolve()).then(() => {
return results;
});
}
}