added schema sync log and migrations basic generation commands

This commit is contained in:
Umed Khudoiberdiev 2017-06-19 13:40:03 +05:00
parent b82b2f31a8
commit 6dacbc4e5e
17 changed files with 519 additions and 29 deletions

View File

@ -1,7 +1,7 @@
{
"name": "typeorm",
"private": true,
"version": "0.1.0-alpha.12",
"version": "0.1.0-alpha.13",
"description": "Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL, MongoDB databases.",
"license": "MIT",
"readmeFilename": "README.md",

View File

@ -8,21 +8,19 @@ import {MigrationCreateCommand} from "./commands/MigrationCreateCommand";
import {MigrationRunCommand} from "./commands/MigrationRunCommand";
import {MigrationRevertCommand} from "./commands/MigrationRevertCommand";
import {SubscriberCreateCommand} from "./commands/SubscriberCreateCommand";
require("yargonaut")
.style("blue")
.style("yellow", "required")
.helpStyle("green")
.errorsStyle("red");
import {SchemaSyncLogCommand} from "./commands/SchemaSyncLogCommand";
import {MigrationGenerateCommand} from "./commands/MigrationGenerateCommand";
require("yargs")
.usage("Usage: $0 <command> [options]")
.command(new SchemaSyncCommand())
.command(new SchemaSyncLogCommand())
.command(new SchemaDropCommand())
.command(new QueryCommand())
.command(new EntityCreateCommand())
.command(new SubscriberCreateCommand())
.command(new MigrationCreateCommand())
.command(new MigrationGenerateCommand())
.command(new MigrationRunCommand())
.command(new MigrationRevertCommand())
.demand(1)
@ -31,3 +29,9 @@ require("yargs")
.help("h")
.alias("h", "help")
.argv;
require("yargonaut")
.style("blue")
.style("yellow", "required")
.helpStyle("green")
.errorsStyle("red");

View File

@ -0,0 +1,98 @@
import {ConnectionOptionsReader} from "../connection/ConnectionOptionsReader";
import {CommandUtils} from "./CommandUtils";
import {Connection} from "../connection/Connection";
import {createConnection} from "../index";
const mkdirp = require("mkdirp");
/**
* Generates a new migration file with sql needs to be executed to update schema.
*/
export class MigrationGenerateCommand {
command = "migrations:generate";
describe = "Generates a new migration file with sql needs to be executed to update schema.";
builder(yargs: any) {
return yargs
.option("c", {
alias: "connection",
default: "default",
describe: "Name of the connection on which run a query."
})
.option("n", {
alias: "name",
describe: "Name of the migration class.",
demand: true
})
.option("d", {
alias: "dir",
describe: "Directory where migration should be created."
})
.option("cf", {
alias: "config",
default: "ormconfig",
describe: "Name of the file with connection configuration."
});
}
async handler(argv: any) {
const timestamp = new Date().getTime();
const filename = timestamp + "-" + argv.name + ".ts";
let directory = argv.dir;
// if directory is not set then try to open tsconfig and find default path there
if (!directory) {
try {
const connectionOptionsReader = new ConnectionOptionsReader({ root: process.cwd(), configName: argv.config });
const connectionOptions = await connectionOptionsReader.get(argv.connection);
directory = connectionOptions.cli ? connectionOptions.cli.migrationsDir : undefined;
} catch (err) { }
}
let connection: Connection|undefined = undefined;
try {
process.env.LOGGER_CLI_SCHEMA_SYNC = true;
process.env.SKIP_SCHEMA_CREATION = true;
const connectionOptionsReader = new ConnectionOptionsReader({ root: process.cwd(), configName: argv.config });
const connectionOptions = await connectionOptionsReader.get(argv.connection);
connection = await createConnection(connectionOptions);
const sqls = await connection.logSyncSchema();
const fileContent = MigrationGenerateCommand.getTemplate(argv.name, timestamp, sqls);
await CommandUtils.createFile(process.cwd() + "/" + (directory ? (directory + "/") : "") + filename, fileContent);
} catch (err) {
if (connection)
(connection as Connection).logger.log("error", err);
throw err;
} finally {
if (connection)
await connection.close();
}
}
// -------------------------------------------------------------------------
// Protected Static Methods
// -------------------------------------------------------------------------
/**
* Gets contents of the migration file.
*/
protected static getTemplate(name: string, timestamp: number, sqlQueries: string[]): string {
return `import {Connection, EntityManager, MigrationInterface, QueryRunner} from "typeorm";
export class ${name}${timestamp} implements MigrationInterface {
public async up(queryRunner: QueryRunner, connection: Connection, entityManager?: EntityManager): Promise<any> {
${sqlQueries.map(query => "queryRunner.query(" + query + ")\r\n")}
}
public async down(queryRunner: QueryRunner, connection: Connection, entityManager?: EntityManager): Promise<any> {
}
}
`;
}
}

View File

@ -0,0 +1,50 @@
import {createConnections, createConnection} from "../index";
import {Connection} from "../connection/Connection";
import {ConnectionOptionsReader} from "../connection/ConnectionOptionsReader";
/**
* Shows sql to be executed by schema:sync command.
*/
export class SchemaSyncLogCommand {
command = "schema:sync";
describe = "Shows sql to be executed by schema:sync command. It shows schema:sync log only for your default connection. " +
"To run update queries on a concrete connection use -c option.";
builder(yargs: any) {
return yargs
.option("c", {
alias: "connection",
default: "default",
describe: "Name of the connection of which schema sync log should be shown."
})
.option("cf", {
alias: "config",
default: "ormconfig",
describe: "Name of the file with connection configuration."
});
}
async handler(argv: any) {
let connection: Connection|undefined = undefined;
try {
process.env.LOGGER_CLI_SCHEMA_SYNC = true;
process.env.SKIP_SCHEMA_CREATION = true;
const connectionOptionsReader = new ConnectionOptionsReader({ root: process.cwd(), configName: argv.config });
const connectionOptions = await connectionOptionsReader.get(argv.connection);
connection = await createConnection(connectionOptions);
const sqls = await connection.logSyncSchema();
sqls.forEach(sql => console.log(sql));
} catch (err) {
if (connection)
(connection as Connection).logger.log("error", err);
throw err;
} finally {
if (connection)
await connection.close();
}
}
}

View File

@ -200,6 +200,17 @@ export class Connection {
await schemaBuilder.build();
}
/**
* Returns sql queries generated by schema builder.
*/
async logSyncSchema(): Promise<string[]> {
if (!this.isConnected)
throw new CannotExecuteNotConnectedError(this.name);
const schemaBuilder = this.driver.createSchemaBuilder();
return schemaBuilder.log();
}
/**
* Drops the database and all its data.
* Be careful with this method on production since this method will erase all your database tables and their data.

View File

@ -560,6 +560,32 @@ export class MongoQueryRunner implements QueryRunner {
.dropCollection(collectionName);
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
throw new Error(`This operation is not supported by MongoDB driver.`);
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
throw new Error(`This operation is not supported by MongoDB driver.`);
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
throw new Error(`This operation is not supported by MongoDB driver.`);
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -45,6 +45,16 @@ export class MysqlQueryRunner implements QueryRunner {
*/
protected databaseConnectionPromise: Promise<any>;
/**
* Indicates if special query runner mode in which sql queries won't be executed is enabled.
*/
protected sqlMemoryMode: boolean = false;
/**
* Sql-s stored if "sql in memory" mode is enabled.
*/
protected sqlsInMemory: string[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -130,6 +140,12 @@ export class MysqlQueryRunner implements QueryRunner {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if sql-in-memory mode is enabled then simply store sql in memory and return
if (this.sqlMemoryMode === true) {
this.sqlsInMemory.push(query);
return Promise.resolve() as Promise<any>;
}
return new Promise(async (ok, fail) => {
this.driver.connection.logger.logQuery(query, parameters);
const databaseConnection = await this.connect();
@ -549,6 +565,33 @@ export class MysqlQueryRunner implements QueryRunner {
}
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
this.sqlMemoryMode = true;
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
this.sqlsInMemory = [];
this.sqlMemoryMode = false;
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
return this.sqlsInMemory;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -47,6 +47,16 @@ export class OracleQueryRunner implements QueryRunner {
*/
protected databaseConnectionPromise: Promise<any>;
/**
* Indicates if special query runner mode in which sql queries won't be executed is enabled.
*/
protected sqlMemoryMode: boolean = false;
/**
* Sql-s stored if "sql in memory" mode is enabled.
*/
protected sqlsInMemory: string[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -615,6 +625,33 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
this.sqlMemoryMode = true;
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
this.sqlsInMemory = [];
this.sqlMemoryMode = false;
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
return this.sqlsInMemory;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -50,6 +50,16 @@ export class PostgresQueryRunner implements QueryRunner {
*/
protected releaseCallback: Function;
/**
* Indicates if special query runner mode in which sql queries won't be executed is enabled.
*/
protected sqlMemoryMode: boolean = false;
/**
* Sql-s stored if "sql in memory" mode is enabled.
*/
protected sqlsInMemory: string[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -641,6 +651,33 @@ where constraint_type = 'PRIMARY KEY' AND c.table_schema = '${this.schemaName}'
}
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
this.sqlMemoryMode = true;
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
this.sqlsInMemory = [];
this.sqlMemoryMode = false;
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
return this.sqlsInMemory;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -35,6 +35,20 @@ export class SqliteQueryRunner implements QueryRunner {
*/
isTransactionActive = false;
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
/**
* Indicates if special query runner mode in which sql queries won't be executed is enabled.
*/
protected sqlMemoryMode: boolean = false;
/**
* Sql-s stored if "sql in memory" mode is enabled.
*/
protected sqlsInMemory: string[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -557,6 +571,33 @@ export class SqliteQueryRunner implements QueryRunner {
}
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
this.sqlMemoryMode = true;
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
this.sqlsInMemory = [];
this.sqlMemoryMode = false;
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
return this.sqlsInMemory;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -49,6 +49,16 @@ export class SqlServerQueryRunner implements QueryRunner {
*/
protected queryResponsibilityChain: Promise<any>[] = [];
/**
* Indicates if special query runner mode in which sql queries won't be executed is enabled.
*/
protected sqlMemoryMode: boolean = false;
/**
* Sql-s stored if "sql in memory" mode is enabled.
*/
protected sqlsInMemory: string[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -666,6 +676,33 @@ WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_C
}
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
this.sqlMemoryMode = true;
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
this.sqlsInMemory = [];
this.sqlMemoryMode = false;
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
return this.sqlsInMemory;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -49,6 +49,16 @@ export class WebsqlQueryRunner implements QueryRunner {
*/
protected databaseConnectionPromise: Promise<any>;
/**
* Indicates if special query runner mode in which sql queries won't be executed is enabled.
*/
protected sqlMemoryMode: boolean = false;
/**
* Sql-s stored if "sql in memory" mode is enabled.
*/
protected sqlsInMemory: string[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -608,6 +618,33 @@ export class WebsqlQueryRunner implements QueryRunner {
}
}
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void {
this.sqlMemoryMode = true;
}
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void {
this.sqlsInMemory = [];
this.sqlMemoryMode = false;
}
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[] {
return this.sqlsInMemory;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -3,8 +3,6 @@ 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 "../driver/types/ColumnTypes";
import {ColumnOptions} from "../decorator/options/ColumnOptions";
/**
* Runs queries on a single database connection.
@ -233,4 +231,24 @@ export interface QueryRunner {
*/
truncate(tableName: string): Promise<void>;
/**
* Enables special query runner mode in which sql queries won't be executed,
* instead they will be memorized into a special variable inside query runner.
* You can get memorized sql using getMemorySql() method.
*/
enableSqlMemory(): void;
/**
* Disables special query runner mode in which sql queries won't be executed
* started by calling enableSqlMemory() method.
*
* Previously memorized sql will be flushed.
*/
disableSqlMemory(): void;
/**
* Gets sql stored in the memory. Parameters in the sql are already replaced.
*/
getMemorySql(): string[];
}

View File

@ -45,4 +45,11 @@ export class MongoSchemaBuilder implements SchemaBuilder {
await Promise.all(promises);
}
/**
* Returns query to be executed by schema builder.
*/
log(): Promise<string[]> {
return Promise.resolve([]);
}
}

View File

@ -62,15 +62,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
await this.queryRunner.startTransaction();
try {
await this.dropOldForeignKeys();
// await this.dropOldPrimaryKeys(); // todo: need to drop primary column because column updates are not possible
await this.createNewTables();
await this.dropRemovedColumns();
await this.addNewColumns();
await this.updateExistColumns();
await this.updatePrimaryKeys();
await this.createIndices(); // we need to create indices before foreign keys because foreign keys rely on unique indices
await this.createForeignKeys();
await this.executeSchemaSyncOperationsInProperOrder();
await this.queryRunner.commitTransaction();
} catch (error) {
@ -85,14 +77,30 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
}
}
/**
* Returns sql queries to be executed by schema builder.
*/
async log(): Promise<string[]> {
this.queryRunner = await this.connection.createQueryRunner();
try {
this.tableSchemas = await this.loadTableSchemas();
this.queryRunner.enableSqlMemory();
await this.executeSchemaSyncOperationsInProperOrder();
return this.queryRunner.getMemorySql();
} finally {
// its important to disable this mode despite the fact we are release query builder
// because there exist drivers which reuse same query runner. Also its important to disable
// sql memory after call of getMemorySql() method because last one flushes sql memory.
this.queryRunner.disableSqlMemory();
await this.queryRunner.release();
}
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
protected get entityToSyncMetadatas(): EntityMetadata[] {
return this.connection.entityMetadatas.filter(metadata => !metadata.skipSync && metadata.tableType !== "single-table-child");
}
/**
* Loads all table schemas from the database.
*/
@ -101,6 +109,29 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
return this.queryRunner.loadTableSchemas(tableNames);
}
/**
* Returns only entities that should be synced in the database.
*/
protected get entityToSyncMetadatas(): EntityMetadata[] {
return this.connection.entityMetadatas.filter(metadata => !metadata.skipSync && metadata.tableType !== "single-table-child");
}
/**
* Executes schema sync operations in a proper order.
* Order of operations matter here.
*/
protected async executeSchemaSyncOperationsInProperOrder(): Promise<void> {
await this.dropOldForeignKeys();
// await this.dropOldPrimaryKeys(); // todo: need to drop primary column because column updates are not possible
await this.createNewTables();
await this.dropRemovedColumns();
await this.addNewColumns();
await this.updateExistColumns();
await this.updatePrimaryKeys();
await this.createIndices(); // we need to create indices before foreign keys because foreign keys rely on unique indices
await this.createForeignKeys();
}
/**
* Drops all (old) foreign keys that exist in the table schemas, but do not exist in the entity metadata.
*/

View File

@ -8,4 +8,9 @@ export interface SchemaBuilder {
*/
build(): Promise<void>;
/**
* Returns queries to be executed by schema builder.
*/
log(): Promise<string[]>;
}

View File

@ -9,20 +9,13 @@ import {View} from "./entity/View";
import {Category} from "./entity/Category";
import {closeTestingConnections, createTestingConnections, setupSingleTestingConnection} from "../../utils/test-utils";
import {Connection} from "../../../src/connection/Connection";
import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver";
import {Repository} from "../../../src/repository/Repository";
import {TreeRepository} from "../../../src/repository/TreeRepository";
import {getConnectionManager} from "../../../src/index";
import {NoConnectionForRepositoryError} from "../../../src/connection/error/NoConnectionForRepositoryError";
import {FirstCustomNamingStrategy} from "./naming-strategy/FirstCustomNamingStrategy";
import {SecondCustomNamingStrategy} from "./naming-strategy/SecondCustomNamingStrategy";
import {EntityManager} from "../../../src/entity-manager/EntityManager";
import {CannotGetEntityManagerNotConnectedError} from "../../../src/connection/error/CannotGetEntityManagerNotConnectedError";
import {Blog} from "./modules/blog/entity/Blog";
import {Question} from "./modules/question/entity/Question";
import {Video} from "./modules/video/entity/Video";
import {ConnectionOptions} from "../../../src/connection/ConnectionOptions";
import {DefaultNamingStrategy} from "../../../src/naming-strategy/DefaultNamingStrategy";
import {PostgresConnectionOptions} from "../../../src/driver/postgres/PostgresConnectionOptions";
describe("Connection", () => {
@ -194,6 +187,21 @@ describe("Connection", () => {
});
describe.only("log a schema when connection.logSyncSchema is called", function() {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Post]
}));
after(() => closeTestingConnections(connections));
it("should return sql log properly", () => Promise.all(connections.map(async connection => {
const sql = await connection.logSyncSchema();
console.log(sql);
})));
});
describe("after connection is closed successfully", function() {
// open a close connections