Skip to content

Commit

Permalink
feat: add custom timestamp option in migration creation (#8501)
Browse files Browse the repository at this point in the history
* feat: add custom timestamp option in migration creation

An option in the CLI to specify a custom timestamp when creating or generating a migration.

Closes: #8500

* feat: add custom timestamp option in migration creation
docs: added description for the option

An option in the CLI to specify a custom timestamp when creating or generating a migration.

Closes: #8500
  • Loading branch information
carmi2214 committed Jan 31, 2022
1 parent f8154eb commit 4a7f242
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 22 deletions.
10 changes: 10 additions & 0 deletions docs/migrations.md
Expand Up @@ -230,6 +230,16 @@ If you need to run/revert your migrations for another connection rather than the
typeorm -c <your-config-name> migration:{run|revert}
```

## Timestamp option
If you need to specify a timestamp for the migration name, use the `-t` (alias for `--timestamp`) and pass the timestamp (should be a non-negative number)
```
typeorm -t <specific-timestamp> migration:{create|generate}
```
You can get a timestamp from:
```js
Date.now(); /* OR */ new Date().getTime();
```

## Using migration API to write migrations

In order to use an API to change a database schema you can use `QueryRunner`.
Expand Down
5 changes: 2 additions & 3 deletions package-lock.json

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

11 changes: 11 additions & 0 deletions src/commands/CommandUtils.ts
@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import mkdirp from "mkdirp";
import {TypeORMError} from "../error";

/**
* Command line utils functions.
Expand Down Expand Up @@ -40,4 +41,14 @@ export class CommandUtils {
static async fileExists(filePath: string) {
return fs.existsSync(filePath);
}

/**
* Gets migration timestamp and validates argument (if sent)
*/
static getTimestamp(timestampOptionArgument: any): number {
if (timestampOptionArgument && (isNaN(timestampOptionArgument) || timestampOptionArgument < 0)) {
throw new TypeORMError(`timestamp option should be a non-negative number. received: ${timestampOptionArgument}`);
}
return timestampOptionArgument ? new Date(Number(timestampOptionArgument)).getTime() : Date.now();
}
}
8 changes: 7 additions & 1 deletion src/commands/MigrationCreateCommand.ts
Expand Up @@ -39,6 +39,12 @@ export class MigrationCreateCommand implements yargs.CommandModule {
type: "boolean",
default: false,
describe: "Generate a migration file on Javascript instead of Typescript",
})
.option("t", {
alias: "timestamp",
type: "number",
default: false,
describe: "Custom timestamp for the migration name",
});
}

Expand All @@ -48,7 +54,7 @@ export class MigrationCreateCommand implements yargs.CommandModule {
}

try {
const timestamp = new Date().getTime();
const timestamp = CommandUtils.getTimestamp(args.timestamp);
const fileContent = args.outputJs ?
MigrationCreateCommand.getJavascriptTemplate(args.name as any, timestamp)
: MigrationCreateCommand.getTemplate(args.name as any, timestamp);
Expand Down
8 changes: 7 additions & 1 deletion src/commands/MigrationGenerateCommand.ts
Expand Up @@ -60,6 +60,12 @@ export class MigrationGenerateCommand implements yargs.CommandModule {
type: "boolean",
default: false,
describe: "Verifies that the current database is up to date and that no migrations are needed. Otherwise exits with code 1.",
})
.option("t", {
alias: "timestamp",
type: "number",
default: false,
describe: "Custom timestamp for the migration name",
});
}

Expand All @@ -68,7 +74,7 @@ export class MigrationGenerateCommand implements yargs.CommandModule {
console.log("'migrations:generate' is deprecated, please use 'migration:generate' instead");
}

const timestamp = new Date().getTime();
const timestamp = CommandUtils.getTimestamp(args.timestamp);
const extension = args.outputJs ? ".js" : ".ts";
const filename = timestamp + "-" + args.name + extension;
let directory = args.dir as string | undefined;
Expand Down
46 changes: 36 additions & 10 deletions test/functional/commands/migration-create.ts
@@ -1,15 +1,15 @@
import sinon from "sinon";
import { ConnectionOptions, ConnectionOptionsReader, DatabaseType } from "../../../src";
import {
setupTestingConnections,
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases
import {ConnectionOptions, ConnectionOptionsReader, DatabaseType} from "../../../src";
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
setupTestingConnections
} 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";
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[];
Expand Down Expand Up @@ -119,4 +119,30 @@ describe("commands - migration create", () => {
getConnectionOptionsStub.restore();
}
});

it("should use custom timestamp 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,
"timestamp": "1641163894670",
}));

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

getConnectionOptionsStub.restore();
}
});
});
39 changes: 33 additions & 6 deletions test/functional/commands/migration-generate.ts
@@ -1,10 +1,10 @@
import sinon from "sinon";
import { ConnectionOptions, ConnectionOptionsReader, DatabaseType } from "../../../src";
import {
setupTestingConnections,
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases
import {
setupTestingConnections,
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases
} from "../../utils/test-utils";
import { CommandUtils } from "../../../src/commands/CommandUtils";
import { MigrationGenerateCommand } from "../../../src/commands/MigrationGenerateCommand";
Expand Down Expand Up @@ -97,13 +97,40 @@ describe("commands - migration generate", () => {
"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();
}
});

it("writes migration file with custom timestamp 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,
"timestamp": "1641163894670",
}));

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

getConnectionOptionsStub.restore();
}
});
Expand Down
13 changes: 12 additions & 1 deletion test/functional/commands/templates/result-templates-create.ts
Expand Up @@ -19,5 +19,16 @@ module.exports = class testMigration1610975184784 {
async down(queryRunner) {
}
}`
}`,
timestamp: `import {MigrationInterface, QueryRunner} from "typeorm";
export class testMigration1641163894670 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}`,
};
14 changes: 14 additions & 0 deletions test/functional/commands/templates/result-templates-generate.ts
Expand Up @@ -25,5 +25,19 @@ module.exports = class testMigration1610975184784 {
async down(queryRunner) {
await queryRunner.query(\`DROP TABLE \\\`post\\\`\`);
}
}`,
timestamp: `import {MigrationInterface, QueryRunner} from "typeorm";
export class testMigration1641163894670 implements MigrationInterface {
name = 'testMigration1641163894670'
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\\\`\`);
}
}`
};

0 comments on commit 4a7f242

Please sign in to comment.