diff --git a/docs/docs/migrations.md b/docs/docs/migrations.md index 1beae92fc838..7024f48a7079 100644 --- a/docs/docs/migrations.md +++ b/docs/docs/migrations.md @@ -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. diff --git a/packages/cli/src/commands/MigrationCommandFactory.ts b/packages/cli/src/commands/MigrationCommandFactory.ts index a6b67ea66e4d..99327297adee 100644 --- a/packages/cli/src/commands/MigrationCommandFactory.ts +++ b/packages/cli/src/commands/MigrationCommandFactory.ts @@ -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, method: MigratorMethod): Promise { @@ -145,7 +150,7 @@ export class MigrationCommandFactory { } private static async handleCreateCommand(migrator: IMigrator, args: ArgumentsCamelCase, config: Configuration): Promise { - 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`)); @@ -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; diff --git a/packages/core/src/typings.ts b/packages/core/src/typings.ts index a26553966861..e6260cbfeea9 100644 --- a/packages/core/src/typings.ts +++ b/packages/core/src/typings.ts @@ -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; + createMigration(path?: string, blank?: boolean, initial?: boolean, name?: string): Promise; /** * Checks current schema for changes. @@ -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. diff --git a/packages/core/src/utils/Configuration.ts b/packages/core/src/utils/Configuration.ts index 674abf943f32..42cc5bf570c3 100644 --- a/packages/core/src/utils/Configuration.ts +++ b/packages/core/src/utils/Configuration.ts @@ -97,7 +97,7 @@ export class Configuration { safe: false, snapshot: true, emit: 'ts', - fileName: (timestamp: string) => `Migration${timestamp}`, + fileName: (timestamp: string, name?: string) => `Migration${timestamp}${name ? '_' + name : ''}`, }, schemaGenerator: { disableForeignKeys: true, @@ -452,7 +452,7 @@ export type MigrationsOptions = { snapshotName?: string; emit?: 'js' | 'ts' | 'cjs'; generator?: Constructor; - fileName?: (timestamp: string) => string; + fileName?: (timestamp: string, name?: string) => string; migrationsList?: MigrationObject[]; }; diff --git a/packages/migrations/src/MigrationGenerator.ts b/packages/migrations/src/MigrationGenerator.ts index 6e2e52577d12..cf57b70d745c 100644 --- a/packages/migrations/src/MigrationGenerator.ts +++ b/packages/migrations/src/MigrationGenerator.ts @@ -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); diff --git a/packages/migrations/src/Migrator.ts b/packages/migrations/src/Migrator.ts index 85356f5081d6..912159bbf527 100644 --- a/packages/migrations/src/Migrator.ts +++ b/packages/migrations/src/Migrator.ts @@ -47,7 +47,7 @@ export class Migrator implements IMigrator { /** * @inheritDoc */ - async createMigration(path?: string, blank = false, initial = false): Promise { + async createMigration(path?: string, blank = false, initial = false, name?: string): Promise { if (initial) { return this.createInitialMigration(path); } @@ -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], diff --git a/tests/features/migrations/Migrator.postgres.test.ts b/tests/features/migrations/Migrator.postgres.test.ts index eaa16d50b243..9b3d902fcaf6 100644 --- a/tests/features/migrations/Migrator.postgres.test.ts +++ b/tests/features/migrations/Migrator.postgres.test.ts @@ -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'); diff --git a/tests/features/migrations/__snapshots__/Migrator.postgres.test.ts.snap b/tests/features/migrations/__snapshots__/Migrator.postgres.test.ts.snap index 06cd505fae77..439885cbbff2 100644 --- a/tests/features/migrations/__snapshots__/Migrator.postgres.test.ts.snap +++ b/tests/features/migrations/__snapshots__/Migrator.postgres.test.ts.snap @@ -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 { + 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 { + 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';