Skip to content

Commit

Permalink
feat: output Javascript Migrations instead of TypeScript (#7294)
Browse files Browse the repository at this point in the history
* docs / test: Added tests and documentation for Feature 7253 - Migrations Javascript output

* Change in the test

* test: Re-arranged the tests to move them to the core tests directory

* tests: Adjusted Tests a bit

* tests - renamed tests to follow the other functional tests naming

* tests - renamed tests to follow the other functional tests naming

* tests - Fixed issues with the test connections setup

* tests - Removed unnecesary restore
  • Loading branch information
foxxor committed Feb 24, 2021
1 parent 70db845 commit b97cc4f
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 5 deletions.
21 changes: 19 additions & 2 deletions docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Once you have a migration to run on production, you can run them using a CLI com
typeorm migration:run
```

**`typeorm migration:create` and `typeorm migration:generate` will create `.ts` files. The `migration:run` and `migration:revert` commands only work on `.js` files. Thus the typescript files need to be compiled before running the commands.** Alternatively you can use `ts-node` in conjunction with `typeorm` to run `.ts` migration files.
**`typeorm migration:create` and `typeorm migration:generate` will create `.ts` files, unless you use the `o` flag (see more in [Generating migrations](#generating-migrations)). The `migration:run` and `migration:revert` commands only work on `.js` files. Thus the typescript files need to be compiled before running the commands.** Alternatively you can use `ts-node` in conjunction with `typeorm` to run `.ts` migration files.

Example with `ts-node`:
```
Expand Down Expand Up @@ -194,8 +194,25 @@ export class PostRefactoringTIMESTAMP implements MigrationInterface {
}
```

Alternatively you can also output your migrations as Javascript files using the `o` (alias for `--outputJs`) flag. This is useful for Javascript only projects in which TypeScript additional packages are not installed. This command, will generate a new migration file `{TIMESTAMP}-PostRefactoring.js` with the following content:

```javascript
const { MigrationInterface, QueryRunner } = require("typeorm");

module.exports = class PostRefactoringTIMESTAMP {

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`);
}
}
```

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. To apply multi-line formatting to your generated migration queries, use the `p` (alias for `--pretty`) flag.
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
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions test/functional/commands/entity/Post.ts
Original file line number Diff line number Diff line change
@@ -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;
}
122 changes: 122 additions & 0 deletions test/functional/commands/migration-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import sinon from "sinon";
import { ConnectionOptions, ConnectionOptionsReader, DatabaseType } from "../../../src";
import {
setupTestingConnections,
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases
} from "../../utils/test-utils";
import { CommandUtils } from "../../../src/commands/CommandUtils";
import { MigrationCreateCommand } from "../../../src/commands/MigrationCreateCommand";
import { Post } from "./entity/Post";
import { resultsTemplates } from "./templates/result-templates-create";

describe("commands - migration create", () => {
let connectionOptions: ConnectionOptions[];
let createFileStub: sinon.SinonStub;
let timerStub: sinon.SinonFakeTimers;
let getConnectionOptionsStub: sinon.SinonStub;
let migrationCreateCommand: MigrationCreateCommand;
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<string, any>) => ({
"$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 = setupTestingConnections({
entities: [Post],
enabledDrivers
});
connectionOptionsReader = new ConnectionOptionsReader();
migrationCreateCommand = new MigrationCreateCommand();
createFileStub = sinon.stub(CommandUtils, "createFile");

timerStub = sinon.useFakeTimers(1610975184784);
});

after(async () => {
timerStub.restore();
createFileStub.restore();
});

afterEach(async () => {
getConnectionOptionsStub.restore();
});

it("should write regular empty 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: [Post]
});

await migrationCreateCommand.handler(testHandlerArgs({
"connection": connectionOption.name
}));

// compare against control test strings in results-templates.ts
sinon.assert.calledWith(
createFileStub,
sinon.match(/test-directory.*test-migration.ts/),
sinon.match(resultsTemplates.control)
);

getConnectionOptionsStub.restore();
}
});

it("should write Javascript empty migration file when 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: [Post]
});

await migrationCreateCommand.handler(testHandlerArgs({
"connection": connectionOption.name,
"outputJs": true
}));

// compare against control test strings in results-templates.ts
sinon.assert.calledWith(
createFileStub,
sinon.match(/test-directory.*test-migration.js/),
sinon.match(resultsTemplates.javascript)
);

getConnectionOptionsStub.restore();
}
});
});
110 changes: 110 additions & 0 deletions test/functional/commands/migration-generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import sinon from "sinon";
import { ConnectionOptions, ConnectionOptionsReader, DatabaseType } from "../../../src";
import {
setupTestingConnections,
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases
} from "../../utils/test-utils";
import { CommandUtils } from "../../../src/commands/CommandUtils";
import { MigrationGenerateCommand } from "../../../src/commands/MigrationGenerateCommand";
import { Post } from "./entity/Post";
import { resultsTemplates } from "./templates/result-templates-generate";

describe("commands - migration generate", () => {
let connectionOptions: ConnectionOptions[];
let createFileStub: sinon.SinonStub;
let timerStub: sinon.SinonFakeTimers;
let getConnectionOptionsStub: sinon.SinonStub;
let migrationGenerateCommand: MigrationGenerateCommand;
let connectionOptionsReader: ConnectionOptionsReader;
let baseConnectionOptions: ConnectionOptions;

const enabledDrivers = [
"mysql",
] as DatabaseType[];

// simulate args: `npm run typeorm migration:run -- -n test-migration -d test-directory`
const testHandlerArgs = (options: Record<string, any>) => ({
"$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 = setupTestingConnections({
entities: [Post],
enabledDrivers
});
connectionOptionsReader = new ConnectionOptionsReader();
migrationGenerateCommand = new MigrationGenerateCommand();
createFileStub = sinon.stub(CommandUtils, "createFile");

timerStub = sinon.useFakeTimers(1610975184784);
});

after(async () => {
timerStub.restore();
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: [Post]
});

await migrationGenerateCommand.handler(testHandlerArgs({
"connection": connectionOption.name
}));

// compare against control test strings in results-templates.ts
sinon.assert.calledWith(
createFileStub,
sinon.match(/test-directory.*test-migration.ts/),
sinon.match(resultsTemplates.control)
);

getConnectionOptionsStub.restore();
}
});

it("writes Javascript printed file when 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: [Post]
});

await migrationGenerateCommand.handler(testHandlerArgs({
"connection": connectionOption.name,
"outputJs": true
}));

// compare against "pretty" test strings in results-templates.ts
sinon.assert.calledWith(
createFileStub,
sinon.match(/test-directory.*test-migration.js/),
sinon.match(resultsTemplates.javascript)
);
getConnectionOptionsStub.restore();
}
});
});
23 changes: 23 additions & 0 deletions test/functional/commands/templates/result-templates-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const resultsTemplates: Record<string, any> = {
control: `import {MigrationInterface, QueryRunner} from "typeorm";
export class testMigration1610975184784 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}`,
javascript: `const { MigrationInterface, QueryRunner } = require("typeorm");
module.exports = class testMigration1610975184784 {
async up(queryRunner) {
}
async down(queryRunner) {
}
}`
};
29 changes: 29 additions & 0 deletions test/functional/commands/templates/result-templates-generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const resultsTemplates: Record<string, any> = {
control: `import {MigrationInterface, QueryRunner} from "typeorm";
export class testMigration1610975184784 implements MigrationInterface {
name = 'testMigration1610975184784'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query("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");
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query("DROP TABLE \`post\`");
}
}`,
javascript: `const { MigrationInterface, QueryRunner } = require("typeorm");
module.exports = class testMigration1610975184784 {
name = 'testMigration1610975184784'
async up(queryRunner) {
await queryRunner.query("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");
}
async down(queryRunner) {
await queryRunner.query("DROP TABLE \`post\`");
}
}`
};

0 comments on commit b97cc4f

Please sign in to comment.