From a0ac946be35e6dd5bebd263036ce10c068a81af6 Mon Sep 17 00:00:00 2001 From: Kristofer Pervin <7747148+kpervin@users.noreply.github.com> Date: Mon, 9 Jan 2023 17:36:36 -0400 Subject: [PATCH] feat(cli): add check for migrations command (#3923) Allow checking for migrations. This would be useful for adding to git hooks to ensure that any changes to ORM entities are captured in migrations. --- docs/docs/migrations.md | 1 + .../src/commands/MigrationCommandFactory.ts | 15 +++++- packages/core/src/typings.ts | 5 ++ packages/migrations-mongodb/src/Migrator.ts | 7 +++ packages/migrations/src/Migrator.ts | 6 +++ .../cli/CheckMigrationsCommand.test.ts | 52 +++++++++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/features/cli/CheckMigrationsCommand.test.ts diff --git a/docs/docs/migrations.md b/docs/docs/migrations.md index f962198866d6..14341737c39a 100644 --- a/docs/docs/migrations.md +++ b/docs/docs/migrations.md @@ -164,6 +164,7 @@ npx mikro-orm migration:create # Create new migration with current schema diff npx mikro-orm migration:up # Migrate up to the latest version npx mikro-orm migration:down # Migrate one step down npx mikro-orm migration:list # List all executed migrations +npx mikro-orm migration:check # Check if schema is up to date npx mikro-orm migration:pending # List all pending migrations npx mikro-orm migration:fresh # Drop the database and migrate up to the latest version ``` diff --git a/packages/cli/src/commands/MigrationCommandFactory.ts b/packages/cli/src/commands/MigrationCommandFactory.ts index 5ff4d31d9fa1..a6b67ea66e4d 100644 --- a/packages/cli/src/commands/MigrationCommandFactory.ts +++ b/packages/cli/src/commands/MigrationCommandFactory.ts @@ -11,6 +11,7 @@ export class MigrationCommandFactory { up: 'Migrate up to the latest version', down: 'Migrate one step down', list: 'List all executed migrations', + check: 'Check if migrations are needed. Useful for bash scripts.', pending: 'List all pending migrations', fresh: 'Clear the database and rerun all migrations', }; @@ -90,6 +91,9 @@ export class MigrationCommandFactory { case 'create': await this.handleCreateCommand(migrator, args, orm.config); break; + case 'check': + await this.handleCheckCommand(migrator, orm); + break; case 'list': await this.handleListCommand(migrator); break; @@ -164,6 +168,15 @@ export class MigrationCommandFactory { CLIHelper.dump(colors.green(`${ret.fileName} successfully created`)); } + private static async handleCheckCommand(migrator: IMigrator, orm: MikroORM): Promise { + if (!await migrator.checkMigrationNeeded()) { + return CLIHelper.dump(colors.green(`No changes required, schema is up-to-date`)); + } + await orm.close(true); + CLIHelper.dump(colors.yellow(`Changes detected. Please create migration to update schema.`)); + process.exit(1); + } + private static async handleFreshCommand(args: ArgumentsCamelCase, migrator: IMigrator, orm: MikroORM) { const generator = orm.getSchemaGenerator(); await generator.dropSchema({ dropMigrationsTable: true }); @@ -222,7 +235,7 @@ export class MigrationCommandFactory { } -type MigratorMethod = 'create' | 'up' | 'down' | 'list' | 'pending' | 'fresh'; +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 Options = GenerateOptions & CliUpDownOptions; diff --git a/packages/core/src/typings.ts b/packages/core/src/typings.ts index 5e0179e0ebb2..3a347fc6eabc 100644 --- a/packages/core/src/typings.ts +++ b/packages/core/src/typings.ts @@ -560,6 +560,11 @@ export interface IMigrator { */ createMigration(path?: string, blank?: boolean, initial?: boolean): Promise; + /** + * Checks current schema for changes. + */ + checkMigrationNeeded(): Promise; + /** * Creates initial migration. This generates the schema based on metadata, and checks whether all the tables * are already present. If yes, it will also automatically log the migration as executed. diff --git a/packages/migrations-mongodb/src/Migrator.ts b/packages/migrations-mongodb/src/Migrator.ts index 89fd0b08dfee..5503750fd1c3 100644 --- a/packages/migrations-mongodb/src/Migrator.ts +++ b/packages/migrations-mongodb/src/Migrator.ts @@ -49,6 +49,13 @@ export class Migrator implements IMigrator { }; } + /** + * @inheritDoc + */ + async checkMigrationNeeded(): Promise { + return true; + } + /** * @inheritDoc */ diff --git a/packages/migrations/src/Migrator.ts b/packages/migrations/src/Migrator.ts index 58eb8975a5d9..37ff883c613b 100644 --- a/packages/migrations/src/Migrator.ts +++ b/packages/migrations/src/Migrator.ts @@ -69,6 +69,12 @@ export class Migrator implements IMigrator { }; } + async checkMigrationNeeded(): Promise { + await this.ensureMigrationsDirExists(); + const diff = await this.getSchemaDiff(false, false); + return diff.up.length > 0; + } + /** * @inheritDoc */ diff --git a/tests/features/cli/CheckMigrationsCommand.test.ts b/tests/features/cli/CheckMigrationsCommand.test.ts new file mode 100644 index 000000000000..b16e07ba1741 --- /dev/null +++ b/tests/features/cli/CheckMigrationsCommand.test.ts @@ -0,0 +1,52 @@ +(global as any).process.env.FORCE_COLOR = 0; + +import { Migrator } from '@mikro-orm/migrations'; +import { MikroORM } from '@mikro-orm/core'; +import type { SqliteDriver } from '@mikro-orm/sqlite'; +import { CLIHelper } from '@mikro-orm/cli'; +import { MigrationCommandFactory } from '../../../packages/cli/src/commands/MigrationCommandFactory'; +import { initORMSqlite } from '../../bootstrap'; + +const closeSpy = jest.spyOn(MikroORM.prototype, 'close'); +jest.spyOn(CLIHelper, 'showHelp').mockImplementation(() => void 0); +const checkMigrationMock = jest.spyOn(Migrator.prototype, 'checkMigrationNeeded'); +checkMigrationMock.mockResolvedValue(true); +const dumpMock = jest.spyOn(CLIHelper, 'dump'); +dumpMock.mockImplementation(() => void 0); + +describe('CheckMigrationCommand', () => { + + let orm: MikroORM; + + beforeAll(async () => { + orm = await initORMSqlite(); + const getORMMock = jest.spyOn(CLIHelper, 'getORM'); + getORMMock.mockResolvedValue(orm); + }); + + afterAll(async () => await orm.close(true)); + + test('builder', async () => { + const cmd = MigrationCommandFactory.create('check'); + const args = { option: jest.fn() }; + cmd.builder(args as any); + }); + + test('handler', async () => { + const cmd = MigrationCommandFactory.create('check'); + + const mockExit = jest.spyOn(process, 'exit').mockImplementationOnce(() => { throw new Error('Mock'); }); + + await expect(cmd.handler({} as any)).rejects.toThrowError('Mock'); + expect(checkMigrationMock.mock.calls.length).toBe(1); + expect(closeSpy).toBeCalledTimes(1); + expect(dumpMock).toHaveBeenLastCalledWith('Changes detected. Please create migration to update schema.'); + expect(mockExit).toBeCalledTimes(1); + + checkMigrationMock.mockImplementationOnce(async () => false); + await expect(cmd.handler({} as any)).resolves.toBeUndefined(); + expect(checkMigrationMock.mock.calls.length).toBe(2); + expect(closeSpy).toBeCalledTimes(2); + expect(dumpMock).toHaveBeenLastCalledWith('No changes required, schema is up-to-date'); + }); +});