Skip to content

Commit

Permalink
feat(cli): add check for migrations command (#3923)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kpervin committed Jan 9, 2023
1 parent c8de84b commit a0ac946
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/docs/migrations.md
Expand Up @@ -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
```
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/commands/MigrationCommandFactory.ts
Expand Up @@ -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',
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -164,6 +168,15 @@ export class MigrationCommandFactory {
CLIHelper.dump(colors.green(`${ret.fileName} successfully created`));
}

private static async handleCheckCommand(migrator: IMigrator, orm: MikroORM): Promise<void> {
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<Options>, migrator: IMigrator, orm: MikroORM) {
const generator = orm.getSchemaGenerator();
await generator.dropSchema({ dropMigrationsTable: true });
Expand Down Expand Up @@ -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;
5 changes: 5 additions & 0 deletions packages/core/src/typings.ts
Expand Up @@ -560,6 +560,11 @@ export interface IMigrator {
*/
createMigration(path?: string, blank?: boolean, initial?: boolean): Promise<MigrationResult>;

/**
* Checks current schema for changes.
*/
checkMigrationNeeded(): Promise<boolean>;

/**
* 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.
Expand Down
7 changes: 7 additions & 0 deletions packages/migrations-mongodb/src/Migrator.ts
Expand Up @@ -49,6 +49,13 @@ export class Migrator implements IMigrator {
};
}

/**
* @inheritDoc
*/
async checkMigrationNeeded(): Promise<boolean> {
return true;
}

/**
* @inheritDoc
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/migrations/src/Migrator.ts
Expand Up @@ -69,6 +69,12 @@ export class Migrator implements IMigrator {
};
}

async checkMigrationNeeded(): Promise<boolean> {
await this.ensureMigrationsDirExists();
const diff = await this.getSchemaDiff(false, false);
return diff.up.length > 0;
}

/**
* @inheritDoc
*/
Expand Down
52 changes: 52 additions & 0 deletions 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<SqliteDriver>;

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');
});
});

0 comments on commit a0ac946

Please sign in to comment.