Skip to content

Commit

Permalink
feat(migrations): add support for custom migration names (#4250)
Browse files Browse the repository at this point in the history
This PR adds a new option to the `migrations:create` command that allows
developers to specify custom names for migration files. Previously, the
migration files were automatically named based on the current timestamp,
but with this new option, developers can now give the migration file a
more descriptive name that is easier to understand and manage.

This feature was inspired by the functionality in Laravel and Ruby on
Rails, and I believe it will bring many benefits, including improved
readability and organization of migration files.

I created a discussion before on this:
#3987

---------

Co-authored-by: Martin Adámek <banan23@gmail.com>
  • Loading branch information
AbdlrahmanSaberAbdo and B4nan committed Apr 21, 2023
1 parent 3b64a75 commit fb2879e
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 11 deletions.
30 changes: 30 additions & 0 deletions docs/docs/migrations.md
Expand Up @@ -263,6 +263,36 @@ await MikroORM.init({
});
```

## Using custom migration names

Since v5.7, you can specify a custom migration name via `--name` CLI option. It will be appended to the generated prefix:

```sh
# generates file Migration20230421212713_add_email_property_to_user_table.ts
npx mikro-orm migration:create --name=add_email_property_to_user_table
```

You can customize the naming convention for your migration file by utilizing the `fileName` callback, or even use it to enforce migrations with names:

```ts
migrations: {
fileName: (timestamp: string, name?: string) => {
// force user to provide the name, otherwise we would end up with `Migration20230421212713_undefined`
if (!name) {
throw new Error('Specify migration name via `mikro-orm migration:create --name=...`');
}

return `Migration${timestamp}_${name}`;
},
},
```

:::caution Warning

When overriding the `migrations.fileName` strategy, keep in mind that your migration files need to be sortable, you should never start the filename with the custom `name` option as it could result in wrong order of execution.

:::

## MongoDB support

Support for migrations in MongoDB has been added in v5.3. It uses its own package: `@mikro-orm/migrations-mongodb`, and should be otherwise compatible with the current CLI commands. Use `this.driver` or `this.getCollection()` to manipulate with the database.
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/commands/MigrationCommandFactory.ts
Expand Up @@ -80,6 +80,11 @@ export class MigrationCommandFactory {
type: 'string',
desc: 'Sets path to directory where to save entities',
});
args.option('name', {
alias: 'name',
type: 'string',
desc: 'Specify custom name for the file',
});
}

static async handleMigrationCommand(args: ArgumentsCamelCase<Options>, method: MigratorMethod): Promise<void> {
Expand Down Expand Up @@ -145,7 +150,7 @@ export class MigrationCommandFactory {
}

private static async handleCreateCommand(migrator: IMigrator, args: ArgumentsCamelCase<Options>, config: Configuration): Promise<void> {
const ret = await migrator.createMigration(args.path, args.blank, args.initial);
const ret = await migrator.createMigration(args.path, args.blank, args.initial, args.name);

if (ret.diff.up.length === 0) {
return CLIHelper.dump(colors.green(`No changes required, schema is up-to-date`));
Expand Down Expand Up @@ -237,5 +242,5 @@ export class MigrationCommandFactory {

type MigratorMethod = 'create' | 'check' | 'up' | 'down' | 'list' | 'pending' | 'fresh';
type CliUpDownOptions = { to?: string | number; from?: string | number; only?: string };
type GenerateOptions = { dump?: boolean; blank?: boolean; initial?: boolean; path?: string; disableFkChecks?: boolean; seed: string };
type GenerateOptions = { dump?: boolean; blank?: boolean; initial?: boolean; path?: string; disableFkChecks?: boolean; seed: string; name?: string };
type Options = GenerateOptions & CliUpDownOptions;
4 changes: 2 additions & 2 deletions packages/core/src/typings.ts
Expand Up @@ -599,7 +599,7 @@ export interface IMigrator {
/**
* Checks current schema for changes, generates new migration if there are any.
*/
createMigration(path?: string, blank?: boolean, initial?: boolean): Promise<MigrationResult>;
createMigration(path?: string, blank?: boolean, initial?: boolean, name?: string): Promise<MigrationResult>;

/**
* Checks current schema for changes.
Expand Down Expand Up @@ -649,7 +649,7 @@ export interface IMigrationGenerator {
/**
* Generates the full contents of migration file. Uses `generateMigrationFile` to get the file contents.
*/
generate(diff: MigrationDiff, path?: string): Promise<[string, string]>;
generate(diff: MigrationDiff, path?: string, name?: string): Promise<[string, string]>;

/**
* Creates single migration statement. By default adds `this.addSql(sql);` to the code.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/Configuration.ts
Expand Up @@ -97,7 +97,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
safe: false,
snapshot: true,
emit: 'ts',
fileName: (timestamp: string) => `Migration${timestamp}`,
fileName: (timestamp: string, name?: string) => `Migration${timestamp}${name ? '_' + name : ''}`,
},
schemaGenerator: {
disableForeignKeys: true,
Expand Down Expand Up @@ -452,7 +452,7 @@ export type MigrationsOptions = {
snapshotName?: string;
emit?: 'js' | 'ts' | 'cjs';
generator?: Constructor<IMigrationGenerator>;
fileName?: (timestamp: string) => string;
fileName?: (timestamp: string, name?: string) => string;
migrationsList?: MigrationObject[];
};

Expand Down
4 changes: 2 additions & 2 deletions packages/migrations/src/MigrationGenerator.ts
Expand Up @@ -12,14 +12,14 @@ export abstract class MigrationGenerator implements IMigrationGenerator {
/**
* @inheritDoc
*/
async generate(diff: { up: string[]; down: string[] }, path?: string): Promise<[string, string]> {
async generate(diff: { up: string[]; down: string[] }, path?: string, name?: string): Promise<[string, string]> {
/* istanbul ignore next */
const defaultPath = this.options.emit === 'ts' && this.options.pathTs ? this.options.pathTs : this.options.path!;
path = Utils.normalizePath(this.driver.config.get('baseDir'), path ?? defaultPath);
await ensureDir(path);
const timestamp = new Date().toISOString().replace(/[-T:]|\.\d{3}z$/ig, '');
const className = this.namingStrategy.classToMigrationName(timestamp);
const fileName = `${this.options.fileName!(timestamp)}.${this.options.emit}`;
const fileName = `${this.options.fileName!(timestamp, name)}.${this.options.emit}`;
const ret = this.generateMigrationFile(className, diff);
await writeFile(path + '/' + fileName, ret);

Expand Down
5 changes: 2 additions & 3 deletions packages/migrations/src/Migrator.ts
Expand Up @@ -47,7 +47,7 @@ export class Migrator implements IMigrator {
/**
* @inheritDoc
*/
async createMigration(path?: string, blank = false, initial = false): Promise<MigrationResult> {
async createMigration(path?: string, blank = false, initial = false, name?: string): Promise<MigrationResult> {
if (initial) {
return this.createInitialMigration(path);
}
Expand All @@ -60,8 +60,7 @@ export class Migrator implements IMigrator {
}

await this.storeCurrentSchema();
const migration = await this.generator.generate(diff, path);

const migration = await this.generator.generate(diff, path, name);
return {
fileName: migration[1],
code: migration[0],
Expand Down
24 changes: 24 additions & 0 deletions tests/features/migrations/Migrator.postgres.test.ts
Expand Up @@ -120,6 +120,30 @@ describe('Migrator (postgres)', () => {
downMock.mockRestore();
});

test('generate migration with custom name with name option', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrationsSettings = orm.config.get('migrations');
orm.config.set('migrations', { ...migrationsSettings, fileName: (time, name) => `migration${time}_${name}` });
const migrator = orm.migrator;
const migration = await migrator.createMigration(undefined, false, false, 'custom_name');
expect(migration).toMatchSnapshot('migration-dump');
expect(migration.fileName).toEqual('migration20191013214813_custom_name.ts');
const upMock = jest.spyOn(Umzug.prototype, 'up');
upMock.mockImplementation(() => void 0 as any);
const downMock = jest.spyOn(Umzug.prototype, 'down');
downMock.mockImplementation(() => void 0 as any);
await migrator.up();
await migrator.down(migration.fileName.replace('.ts', ''));
await migrator.up();
await migrator.down(migration.fileName);
await migrator.up();
orm.config.set('migrations', migrationsSettings); // Revert migration config changes
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
upMock.mockRestore();
downMock.mockRestore();
});

test('generate schema migration', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
Expand Down
Expand Up @@ -475,6 +475,46 @@ export class Migration20191013214813 extends Migration {
}
`;
exports[`Migrator (postgres) generate migration with custom name with name option: migration-dump 1`] = `
{
"code": "import { Migration } from '@mikro-orm/migrations';
export class Migration20191013214813 extends Migration {
async up(): Promise<void> {
this.addSql('alter table "custom"."book2" alter column "double" type double precision using ("double"::double precision);');
this.addSql('alter table "custom"."book2" drop column "foo";');
this.addSql('alter table "custom"."test2" drop column "path";');
}
async down(): Promise<void> {
this.addSql('alter table "custom"."book2" add column "foo" varchar null default \\'lol\\';');
this.addSql('alter table "custom"."book2" alter column "double" type numeric using ("double"::numeric);');
this.addSql('alter table "custom"."test2" add column "path" polygon null default null;');
}
}
",
"diff": {
"down": [
"alter table "custom"."book2" add column "foo" varchar null default 'lol';",
"alter table "custom"."book2" alter column "double" type numeric using ("double"::numeric);",
"",
"alter table "custom"."test2" add column "path" polygon null default null;",
],
"up": [
"alter table "custom"."book2" alter column "double" type double precision using ("double"::double precision);",
"alter table "custom"."book2" drop column "foo";",
"",
"alter table "custom"."test2" drop column "path";",
],
},
"fileName": "migration20191013214813_custom_name.ts",
}
`;
exports[`Migrator (postgres) generate migration with custom name: migration-dump 1`] = `
{
"code": "import { Migration } from '@mikro-orm/migrations';
Expand Down

0 comments on commit fb2879e

Please sign in to comment.