diff --git a/docs/migrations.md b/docs/migrations.md index 7c903aa6dc..3e4799d205 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -195,7 +195,7 @@ export class PostRefactoringTIMESTAMP implements MigrationInterface { ``` See, you don't need to write the queries on your own. -The rule of thumb for generating migrations is that you generate them after "each" change you made to your models. +The rule of thumb for generating migrations is that you generate them after "each" change you made to your models. To apply multi-line formatting to your generated migration queries, use the `p` (alias for `--pretty`) flag. ## Connection option If you need to run/revert your migrations for another connection rather than the default, use the `-c` (alias for `--connection`) and pass the config name as an argument diff --git a/package-lock.json b/package-lock.json index e81e794ade..edd53ab49d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -251,6 +251,12 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@sqltools/formatter": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.2.tgz", + "integrity": "sha512-/5O7Fq6Vnv8L6ucmPjaWbVG1XkP4FO+w5glqfkIsq3Xw4oyNAdJddbnYodNDAfjVUvo/rrSCTom4kAND7T1o5Q==", + "dev": true + }, "@types/app-root-path": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/app-root-path/-/app-root-path-1.2.4.tgz", diff --git a/package.json b/package.json index 7432d5c900..6e22c75a48 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "typescript": "^3.3.3333" }, "dependencies": { + "@sqltools/formatter": "1.2.2", "app-root-path": "^3.0.0", "buffer": "^5.5.0", "chalk": "^4.1.0", diff --git a/src/commands/MigrationGenerateCommand.ts b/src/commands/MigrationGenerateCommand.ts index f036fd67fc..5fb779b7cc 100644 --- a/src/commands/MigrationGenerateCommand.ts +++ b/src/commands/MigrationGenerateCommand.ts @@ -7,6 +7,7 @@ import {camelCase} from "../util/StringUtils"; import * as yargs from "yargs"; import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver"; import chalk from "chalk"; +import { format } from "@sqltools/formatter/lib/sqlFormatter"; /** * Generates a new migration file with sql needs to be executed to update schema. @@ -33,6 +34,12 @@ export class MigrationGenerateCommand implements yargs.CommandModule { alias: "dir", describe: "Directory where migration should be created." }) + .option("p", { + alias: "pretty", + type: "boolean", + default: false, + describe: "Pretty-print generated SQL", + }) .option("f", { alias: "config", default: "ormconfig", @@ -76,6 +83,16 @@ export class MigrationGenerateCommand implements yargs.CommandModule { }); connection = await createConnection(connectionOptions); const sqlInMemory = await connection.driver.createSchemaBuilder().log(); + + if (args.pretty) { + sqlInMemory.upQueries.forEach(upQuery => { + upQuery.query = MigrationGenerateCommand.prettifyQuery(upQuery.query); + }); + sqlInMemory.downQueries.forEach(downQuery => { + downQuery.query = MigrationGenerateCommand.prettifyQuery(downQuery.query); + }); + } + const upSqls: string[] = [], downSqls: string[] = []; // mysql is exceptional here because it uses ` character in to escape names in queries, that's why for mysql @@ -160,4 +177,11 @@ ${downSqls.join(` `; } + /** + * + */ + protected static prettifyQuery(query: string) { + const formattedQuery = format(query, { indent: " " }); + return "\n" + formattedQuery.replace(/^/gm, " ") + "\n "; + } } diff --git a/test/github-issues/4415/entity/Post.ts b/test/github-issues/4415/entity/Post.ts new file mode 100644 index 0000000000..94e6f92ea5 --- /dev/null +++ b/test/github-issues/4415/entity/Post.ts @@ -0,0 +1,14 @@ +import { PrimaryGeneratedColumn, Entity, Column, CreateDateColumn } from "../../../../src"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id?: number; + + @Column() + title: string; + + @CreateDateColumn() + readonly createdAt?: Date; +} diff --git a/test/github-issues/4415/entity/Username.ts b/test/github-issues/4415/entity/Username.ts new file mode 100644 index 0000000000..b1f7b55552 --- /dev/null +++ b/test/github-issues/4415/entity/Username.ts @@ -0,0 +1,15 @@ +import { Column } from "../../../../src/decorator/columns/Column"; +import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"; +import { Entity } from "../../../../src/decorator/entity/Entity"; + +@Entity() +export class Username { + @PrimaryColumn() + username: string; + + @Column() + email: string; + + @Column() + something: string; +} diff --git a/test/github-issues/4415/issue-4415.ts b/test/github-issues/4415/issue-4415.ts new file mode 100644 index 0000000000..5f0a08af57 --- /dev/null +++ b/test/github-issues/4415/issue-4415.ts @@ -0,0 +1,108 @@ +import sinon from "sinon"; +import { ConnectionOptions, ConnectionOptionsReader, DatabaseType } from "../../../src"; +import { setupTestingConnections, createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils"; +import { Username } from "./entity/Username"; +import { CommandUtils } from "../../../src/commands/CommandUtils"; +import { MigrationGenerateCommand } from "../../../src/commands/MigrationGenerateCommand"; +import { Post } from "./entity/Post"; +import { resultsTemplates } from "./results-templates"; + +describe("github issues > #4415 allow beautify generated migrations", () => { + let connectionOptions: ConnectionOptions[]; + let createFileStub: sinon.SinonStub; + let getConnectionOptionsStub: sinon.SinonStub; + let migrationGenerateCommand: MigrationGenerateCommand; + let connectionOptionsReader: ConnectionOptionsReader; + let baseConnectionOptions: ConnectionOptions; + + const enabledDrivers = [ + "postgres", + "mssql", + "mysql", + "mariadb", + "sqlite", + "better-sqlite3", + "oracle", + "cockroachdb" + ] as DatabaseType[]; + + // simulate args: `npm run typeorm migration:run -- -n test-migration -d test-directory` + const testHandlerArgs = (options: Record) => ({ + "$0": "test", + "_": ["test"], + "name": "test-migration", + "dir": "test-directory", + ...options + }); + + before(async () => { + // clean out db from any prior tests in case previous state impacts the generated migrations + const connections = await createTestingConnections({ + entities: [], + enabledDrivers + }); + await reloadTestingDatabases(connections); + await closeTestingConnections(connections); + + connectionOptions = await setupTestingConnections({ + entities: [Username, Post], + enabledDrivers + }); + connectionOptionsReader = new ConnectionOptionsReader(); + migrationGenerateCommand = new MigrationGenerateCommand(); + createFileStub = sinon.stub(CommandUtils, "createFile"); + }); + after(() => createFileStub.restore()); + + it("writes regular migration file when no option is passed", async () => { + for (const connectionOption of connectionOptions) { + createFileStub.resetHistory(); + baseConnectionOptions = await connectionOptionsReader.get(connectionOption.name as string); + getConnectionOptionsStub = sinon.stub(ConnectionOptionsReader.prototype, "get").resolves({ + ...baseConnectionOptions, + entities: [Username, Post] + }); + + await migrationGenerateCommand.handler(testHandlerArgs({ + "connection": connectionOption.name + })); + + // compare against control test strings in results-templates.ts + for (const control of resultsTemplates[connectionOption.type as string].control) { + sinon.assert.calledWith( + createFileStub, + sinon.match(/test-directory.*test-migration.ts/), + sinon.match(control) + ); + } + + getConnectionOptionsStub.restore(); + } + }); + + it("writes pretty printed file when pretty option is passed", async () => { + for (const connectionOption of connectionOptions) { + createFileStub.resetHistory(); + baseConnectionOptions = await connectionOptionsReader.get(connectionOption.name as string); + getConnectionOptionsStub = sinon.stub(ConnectionOptionsReader.prototype, "get").resolves({ + ...baseConnectionOptions, + entities: [Username, Post] + }); + + await migrationGenerateCommand.handler(testHandlerArgs({ + "connection": connectionOption.name, + "pretty": true + })); + + // compare against "pretty" test strings in results-templates.ts + for (const pretty of resultsTemplates[connectionOption.type as string].pretty) { + sinon.assert.calledWith( + createFileStub, + sinon.match(/test-directory.*test-migration.ts/), + sinon.match(pretty) + ); + } + getConnectionOptionsStub.restore(); + } + }); +}); diff --git a/test/github-issues/4415/results-templates.ts b/test/github-issues/4415/results-templates.ts new file mode 100644 index 0000000000..ce7c7d63ee --- /dev/null +++ b/test/github-issues/4415/results-templates.ts @@ -0,0 +1,157 @@ +export const resultsTemplates: Record = { + + postgres: { + control: [ + `CREATE TABLE "post" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`, + `CREATE TABLE "username" ("username" character varying NOT NULL, "email" character varying NOT NULL, "something" character varying NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))` + ], + pretty: [ + ` + CREATE TABLE "post" ( + "id" SERIAL NOT NULL, + "title" character varying NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id") + ) + `, + ` + CREATE TABLE "username" ( + "username" character varying NOT NULL, + "email" character varying NOT NULL, + "something" character varying NOT NULL, + CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username") + ) + ` + ] + }, + + mssql: { + control: [ + `CREATE TABLE "post" ("id" int NOT NULL IDENTITY(1,1), "title" nvarchar(255) NOT NULL, "createdAt" datetime2 NOT NULL CONSTRAINT "DF_fb91bea2d37140a877b775e6b2a" DEFAULT getdate(), CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`, + `CREATE TABLE "username" ("username" nvarchar(255) NOT NULL, "email" nvarchar(255) NOT NULL, "something" nvarchar(255) NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))` + ], + pretty: [ + ` + CREATE TABLE "post" ( + "id" int NOT NULL IDENTITY(1, 1), + "title" nvarchar(255) NOT NULL, + "createdAt" datetime2 NOT NULL CONSTRAINT "DF_fb91bea2d37140a877b775e6b2a" DEFAULT getdate(), + CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id") + ) + `, + ` + CREATE TABLE "username" ( + "username" nvarchar(255) NOT NULL, + "email" nvarchar(255) NOT NULL, + "something" nvarchar(255) NOT NULL, + CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username") + ) + ` + ] + }, + + sqlite: { + control: [ + `CREATE TABLE "post" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')))`, + `CREATE TABLE "username" ("username" varchar PRIMARY KEY NOT NULL, "email" varchar NOT NULL, "something" varchar NOT NULL)`, + ], + pretty: [ + ` + CREATE TABLE "post" ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "title" varchar NOT NULL, + "createdAt" datetime NOT NULL DEFAULT (datetime('now')) + ) + `, + ` + CREATE TABLE "username" ( + "username" varchar PRIMARY KEY NOT NULL, + "email" varchar NOT NULL, + "something" varchar NOT NULL + ) + ` + ] + }, + + mysql: { + control: [ + `CREATE TABLE \`post\` (\`id\` int NOT NULL AUTO_INCREMENT, \`title\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + `CREATE TABLE \`username\` (\`username\` varchar(255) NOT NULL, \`email\` varchar(255) NOT NULL, \`something\` varchar(255) NOT NULL, PRIMARY KEY (\`username\`)) ENGINE=InnoDB` + ], + pretty: [ + ` + CREATE TABLE \`post\` ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`title\` varchar(255) NOT NULL, + \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `, + ` + CREATE TABLE \`username\` ( + \`username\` varchar(255) NOT NULL, + \`email\` varchar(255) NOT NULL, + \`something\` varchar(255) NOT NULL, + PRIMARY KEY (\`username\`) + ) ENGINE = InnoDB + ` + ] + }, + + oracle: { + control: [ + `CREATE TABLE "post" ("id" number GENERATED BY DEFAULT AS IDENTITY, "title" varchar2(255) NOT NULL, "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`, + `CREATE TABLE "username" ("username" varchar2(255) NOT NULL, "email" varchar2(255) NOT NULL, "something" varchar2(255) NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))` + ], + pretty: [ + ` + CREATE TABLE "post" ( + "id" number GENERATED BY DEFAULT AS IDENTITY, + "title" varchar2(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id") + ) + `, + ` + CREATE TABLE "username" ( + "username" varchar2(255) NOT NULL, + "email" varchar2(255) NOT NULL, + "something" varchar2(255) NOT NULL, + CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username") + ) + ` + ] + }, + + cockroachdb: { + control: [ + `CREATE SEQUENCE "post_id_seq"`, + `CREATE TABLE "post" ("id" INT DEFAULT nextval('"post_id_seq"') NOT NULL, "title" varchar NOT NULL, "createdAt" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`, + `CREATE TABLE "username" ("username" varchar NOT NULL, "email" varchar NOT NULL, "something" varchar NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))` + ], + pretty: [ + ` + CREATE SEQUENCE "post_id_seq" + `, + ` + CREATE TABLE "post" ( + "id" INT DEFAULT nextval('"post_id_seq"') NOT NULL, + "title" varchar NOT NULL, + "createdAt" timestamptz NOT NULL DEFAULT now(), + CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id") + ) + `, + ` + CREATE TABLE "username" ( + "username" varchar NOT NULL, + "email" varchar NOT NULL, + "something" varchar NOT NULL, + CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username") + ) + ` + ] + }, + + get mariadb() { return this.mysql; }, + get "better-sqlite3"() { return this.sqlite; }, +};