From fb2879e21575a54d1b05c7d1fc250d5f713d9b44 Mon Sep 17 00:00:00 2001 From: AbdlrahmanSaber Date: Fri, 21 Apr 2023 21:40:47 +0200 Subject: [PATCH] feat(migrations): add support for custom migration names (#4250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/mikro-orm/mikro-orm/discussions/3987 --------- Co-authored-by: Martin Adámek --- docs/docs/migrations.md | 30 ++++++++++++++ .../src/commands/MigrationCommandFactory.ts | 9 ++++- packages/core/src/typings.ts | 4 +- packages/core/src/utils/Configuration.ts | 4 +- packages/migrations/src/MigrationGenerator.ts | 4 +- packages/migrations/src/Migrator.ts | 5 +-- .../migrations/Migrator.postgres.test.ts | 24 +++++++++++ .../Migrator.postgres.test.ts.snap | 40 +++++++++++++++++++ 8 files changed, 109 insertions(+), 11 deletions(-) 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';