mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added migrations execute and revert functionality
This commit is contained in:
parent
31d5a76f5f
commit
0f9287ad8e
@ -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));
|
||||
@ -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)");
|
||||
|
||||
@ -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)");
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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.`);
|
||||
}
|
||||
|
||||
}
|
||||
37
src/migration/Migration.ts
Normal file
37
src/migration/Migration.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
262
src/migration/MigrationExecutor.ts
Normal file
262
src/migration/MigrationExecutor.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@ -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]);
|
||||
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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
23
src/util/PromiseUtils.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user