Skip to content

Commit

Permalink
feat(migrations): add support for initial migrations (#818)
Browse files Browse the repository at this point in the history
If you want to start using migrations, and you already have the schema generated,
you can do so by creating so called initial migration:

> Initial migration can be created only if there are no migrations previously
> generated or executed.

```sh
npx mikro-orm migration:create --initial
```

This will create the initial migration, containing the schema dump from
`schema:create` command. The migration will be automatically marked as executed.

Closes #772
  • Loading branch information
B4nan committed Sep 6, 2020
1 parent 5936086 commit 26b2228
Show file tree
Hide file tree
Showing 5 changed files with 404 additions and 8 deletions.
20 changes: 20 additions & 0 deletions docs/docs/migrations.md
Expand Up @@ -29,6 +29,26 @@ per migration basis by implementing the `isTransactional(): boolean` method.

`Configuration` object and driver instance are available in the `Migration` class context.

You can execute queries in the migration via `Migration.execute()` method, which
will run queries in the same transaction as the rest of the migration. The
`Migration.addSql()` method also accepts instances of knex. Knex instance can be
accessed via `Migration.getKnex()`;

## Initial migration

If you want to start using migrations, and you already have the schema generated,
you can do so by creating so called initial migration:

> Initial migration can be created only if there are no migrations previously
> generated or executed.
```sh
npx mikro-orm migration:create --initial
```

This will create the initial migration, containing the schema dump from
`schema:create` command. The migration will be automatically marked as executed.

## Configuration

```typescript
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/commands/MigrationCommandFactory.ts
Expand Up @@ -60,6 +60,11 @@ export class MigrationCommandFactory {
type: 'boolean',
desc: 'Create blank migration',
});
args.option('i', {
alias: 'initial',
type: 'boolean',
desc: 'Create initial migration',
});
args.option('d', {
alias: 'dump',
type: 'boolean',
Expand Down Expand Up @@ -122,7 +127,7 @@ export class MigrationCommandFactory {
}

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

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

type MigratorMethod = 'create' | 'up' | 'down' | 'list' | 'pending';
type CliUpDownOptions = { to?: string | number; from?: string | number; only?: string };
type GenerateOptions = { dump?: boolean; blank?: boolean; path?: string; disableFkChecks?: boolean };
type GenerateOptions = { dump?: boolean; blank?: boolean; initial?: boolean; path?: string; disableFkChecks?: boolean };
type Options = GenerateOptions & CliUpDownOptions;
40 changes: 35 additions & 5 deletions packages/migrations/src/Migrator.ts
Expand Up @@ -48,22 +48,39 @@ export class Migrator {
});
}

async createMigration(path?: string, blank = false): Promise<MigrationResult> {
const diff = blank ? ['select 1'] : await this.getSchemaDiff();
async createMigration(path?: string, blank = false, initial = false): Promise<MigrationResult> {
if (initial) {
await this.validateInitialMigration();
}

const diff = await this.getSchemaDiff(blank, initial);

if (diff.length === 0) {
return { fileName: '', code: '', diff };
}

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

if (initial) {
await this.storage.logMigration(migration[1]);
}

return {
fileName: migration[1],
code: migration[0],
diff,
};
}

async validateInitialMigration() {
const executed = await this.getExecutedMigrations();
const pending = await this.getPendingMigrations();

if (executed.length > 0 || pending.length > 0) {
throw new Error('Initial migration cannot be created, as some migrations already exist');
}
}

async getExecutedMigrations(): Promise<MigrationRow[]> {
await this.storage.ensureTable();
return this.storage.getExecutedMigrations();
Expand All @@ -82,6 +99,10 @@ export class Migrator {
return this.runMigrations('down', options);
}

getStorage(): MigrationStorage {
return this.storage;
}

protected resolve(file: string) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const migration = require(file);
Expand All @@ -100,9 +121,18 @@ export class Migrator {
};
}

private async getSchemaDiff(): Promise<string[]> {
const dump = await this.schemaGenerator.getUpdateSchemaSQL(false, this.options.safe, this.options.dropTables);
const lines = dump.split('\n');
private async getSchemaDiff(blank: boolean, initial: boolean): Promise<string[]> {
const lines: string[] = [];

if (blank) {
lines.push('select 1');
} else if (initial) {
const dump = await this.schemaGenerator.getCreateSchemaSQL(false);
lines.push(...dump.split('\n'));
} else {
const dump = await this.schemaGenerator.getUpdateSchemaSQL(false, this.options.safe, this.options.dropTables);
lines.push(...dump.split('\n'));
}

for (let i = lines.length - 1; i > 0; i--) {
if (lines[i]) {
Expand Down
26 changes: 25 additions & 1 deletion tests/Migrator.test.ts
@@ -1,7 +1,7 @@
(global as any).process.env.FORCE_COLOR = 0;
import umzug from 'umzug';
import { Logger, MikroORM } from '@mikro-orm/core';
import { Migration, Migrator } from '@mikro-orm/migrations';
import { Migration, MigrationStorage, Migrator } from '@mikro-orm/migrations';
import { MySqlDriver } from '@mikro-orm/mysql';
import { remove, writeFile } from 'fs-extra';
import { initORMMySql } from './bootstrap';
Expand Down Expand Up @@ -76,6 +76,30 @@ describe('Migrator', () => {
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
});

test('generate initial migration', async () => {
const getExecutedMigrationsMock = jest.spyOn<any, any>(Migrator.prototype, 'getExecutedMigrations');
getExecutedMigrationsMock.mockResolvedValueOnce(['test.ts']);
const migrator = new Migrator(orm.em);
const err = 'Initial migration cannot be created, as some migrations already exist';
await expect(migrator.createMigration(undefined, false, true)).rejects.toThrowError(err);

getExecutedMigrationsMock.mockResolvedValueOnce([]);
const logMigrationMock = jest.spyOn<any, any>(MigrationStorage.prototype, 'logMigration');
logMigrationMock.mockImplementationOnce(i => i);
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');

const migration = await migrator.createMigration(undefined, false, true);
expect(logMigrationMock).toBeCalledWith('Migration20191013214813.ts');
expect(migration).toMatchSnapshot('initial-migration-dump');
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
});

test('migration storage getter', async () => {
const migrator = new Migrator(orm.em);
expect(migrator.getStorage()).toBeInstanceOf(MigrationStorage);
});

test('migration is skipped when no diff', async () => {
const migrator = new Migrator(orm.em);
const getSchemaDiffMock = jest.spyOn<any, any>(Migrator.prototype, 'getSchemaDiff');
Expand Down

0 comments on commit 26b2228

Please sign in to comment.