Skip to content

Commit

Permalink
feat(cli): add migrations commands to cli
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Oct 21, 2019
1 parent f783390 commit 6773ac5
Show file tree
Hide file tree
Showing 23 changed files with 855 additions and 304 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -14,6 +14,7 @@ services:
- postgresql

cache:
yarn: true
directories:
- 'node_modules'

Expand Down
101 changes: 101 additions & 0 deletions docs/migrations.md
@@ -0,0 +1,101 @@
---
---

# Migrations

To generate entities from existing database schema, you can use `Migrator` helper. It uses
[umzug](https://github.com/sequelize/umzug) under the hood.

## Migration class

Migrations are classes that extend Migration abstract class:

```typescript
export class Migration20191019195930 extends Migration {

async up(): Promise<void> {
this.addSql('select 1 + 1');
}

}
```

To support migrating back, you can implement `down` method, which by default throws an error.

Transactions are by default wrapped in a transactions. You can override this behaviour on
per transaction basis by implementing `isTransactional(): boolean` method.

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

## Configuration

```typescript
await MikroORM.init({
// default values:
migrations: {
tableName: 'mikro_orm_migrations',
path: './migrations',
transactional: true,
disableForeignKeys: true,
},
})
```

## Using via CLI

You can use it via CLI:

```sh
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:pending # List all pending migrations
```

For `migration:up` and `migration:down` commands you can specify `--from` (`-f`), `--to` (`-t`)
and `--only` (`-o`) options to run only a subset of migrations:

```sh
npx mikro-orm migration:up --from 2019101911 --to 2019102117 # the same as above
npx mikro-orm migration:up --only 2019101923 # apply a single migration
npx mikro-orm migration:down --to 0 # migratee down all migrations
```

> To run TS migration files, you will need to [enable `useTsNode` flag](installation.md)
> in your `package.json`.
## Using the Migrator programmatically

Or you can create simple script where you initialize MikroORM like this:

**`./migrate.ts`**

```typescript
import { MikroORM } from 'mikro-orm';

(async () => {
const orm = await MikroORM.init({
dbName: 'your-db-name',
// ...
});

const migrator = orm.getMigrator();
await migrator.createMigration(); // creates file Migration20191019195930.ts
await migrator.up(); // runs migrations up to the latest
await migrator.up('up-to-name'); // runs migrations up to given version
await migrator.down('down-to-name'); // runs migrations down to given version
await migrator.down(); // migrates one step down
await migrator.down({ to: 0 }); // migrates down to the first version

await orm.close(true);
})();
```

Then run this script via `ts-node` (or compile it to plain JS and use `node`):

```sh
$ ts-node migrate
```

[&larr; Back to table of contents](index.md#table-of-contents)
17 changes: 17 additions & 0 deletions lib/cli/CLIHelper.ts
@@ -1,5 +1,6 @@
import yargs, { Argv } from 'yargs';
import { pathExists } from 'fs-extra';
import CliTable3, { HorizontalTable } from 'cli-table3';
import highlight from 'cli-highlight';
import chalk from 'chalk';

Expand All @@ -8,6 +9,7 @@ import { Configuration, Utils } from '../utils';
import { ClearCacheCommand } from './ClearCacheCommand';
import { GenerateEntitiesCommand } from './GenerateEntitiesCommand';
import { SchemaCommandFactory } from './SchemaCommandFactory';
import { MigrationCommandFactory } from './MigrationCommandFactory';
import { DebugCommand } from './DebugCommand';
import { Dictionary } from '../types';

Expand Down Expand Up @@ -64,6 +66,11 @@ export class CLIHelper {
.command(SchemaCommandFactory.create('create'))
.command(SchemaCommandFactory.create('drop'))
.command(SchemaCommandFactory.create('update'))
.command(MigrationCommandFactory.create('create'))
.command(MigrationCommandFactory.create('up'))
.command(MigrationCommandFactory.create('down'))
.command(MigrationCommandFactory.create('list'))
.command(MigrationCommandFactory.create('pending'))
.command(new DebugCommand())
.recommendCommands()
.strict();
Expand Down Expand Up @@ -157,6 +164,16 @@ export class CLIHelper {
return chalk.red('not-found');
}

static dumpTable(options: { columns: string[]; rows: string[][]; empty: string }): void {
if (options.rows.length === 0) {
return CLIHelper.dump(options.empty);
}

const table = new CliTable3({ head: options.columns, style: { compact: true } }) as HorizontalTable;
table.push(...options.rows);
CLIHelper.dump(table.toString());
}

}

export interface Settings {
Expand Down
131 changes: 131 additions & 0 deletions lib/cli/MigrationCommandFactory.ts
@@ -0,0 +1,131 @@
import { Arguments, Argv, CommandModule } from 'yargs';
import chalk from 'chalk';
import { CLIHelper } from './CLIHelper';
import { Utils } from '../utils';

type MigratorMethod = 'create' | 'up' | 'down' | 'list' | 'pending';

export class MigrationCommandFactory {

static readonly DESCRIPTIONS = {
create: 'Create new migration with current schema diff',
up: 'Migrate up to the latest version',
down: 'Migrate one step down',
list: 'List all executed migrations',
pending: 'List all pending migrations',
};

static readonly SUCCESS_MESSAGES = {
create: (name?: string) => `${name} successfully created`,
up: (name?: string) => `Successfully migrated up to ${name ? `version ${name}` : 'the latest version'}`,
down: (name?: string) => `Successfully migrated down to ${name ? `version ${name}` : 'the first version'}`,
};

static create<U extends Options = Options>(command: MigratorMethod): CommandModule<{}, U> & { builder: (args: Argv) => Argv<U>; handler: (args: Arguments<U>) => Promise<void> } {
return {
command: `migration:${command}`,
describe: MigrationCommandFactory.DESCRIPTIONS[command],
builder: (args: Argv) => MigrationCommandFactory.configureMigrationCommand(args, command) as Argv<U>,
handler: (args: Arguments<U>) => MigrationCommandFactory.handleMigrationCommand(args, command),
};
}

static configureMigrationCommand(args: Argv, method: MigratorMethod) {
if (method === 'create') {
args.option('b', {
alias: 'blank',
type: 'boolean',
desc: 'Create blank migration',
});
args.option('d', {
alias: 'dump',
type: 'boolean',
desc: 'Dumps all queries to console',
});
args.option('disable-fk-checks', {
type: 'boolean',
desc: 'Do not skip foreign key checks',
});
args.option('p', {
alias: 'path',
type: 'string',
desc: 'Sets path to directory where to save entities',
});
}

if (['up', 'down'].includes(method)) {
args.option('t', {
alias: 'to',
type: 'string',
desc: `Migrate ${method} to specific version`,
});
args.option('f', {
alias: 'from',
type: 'string',
desc: 'Start migration from specific version',
});
args.option('o', {
alias: 'only',
type: 'string',
desc: 'Migrate only specified versions',
});
}

return args;
}

static async handleMigrationCommand(args: Arguments<Options>, method: MigratorMethod) {
const successMessage = MigrationCommandFactory.SUCCESS_MESSAGES[method];
const orm = await CLIHelper.getORM();
const migrator = orm.getMigrator();

switch (method) {
case 'create':
const ret = await migrator.createMigration(args.path, args.blank);

if (args.dump) {
CLIHelper.dump(chalk.green('Creating migration with following queries:'));
CLIHelper.dump(ret[2].map(sql => ' ' + sql).join('\n'), orm.config, 'sql');
}

CLIHelper.dump(chalk.green(successMessage(ret[1])));
break;
case 'list':
const executed = await migrator.getExecutedMigrations();

CLIHelper.dumpTable({
columns: ['Name', 'Executed at'],
rows: executed.map(row => [row.name.replace(/\.[jt]s$/, ''), row.executed_at.toISOString()]),
empty: 'No migrations executed yet',
});
break;
case 'pending':
const pending = await migrator.getPendingMigrations();
CLIHelper.dumpTable({
columns: ['Name'],
rows: pending.map(row => [row.file.replace(/\.[jt]s$/, '')]),
empty: 'No pending migrations',
});
break;
case 'up':
case 'down':
await migrator[method as 'up' | 'down'](MigrationCommandFactory.getUpDownOptions(args) as string[]);
CLIHelper.dump(chalk.green(successMessage(Utils.isString(args) ? args : args.to)));
}

await orm.close(true);
}

private static getUpDownOptions(flags: Arguments<Options>) {
if (!flags.to && !flags.from && flags.only) {
return flags.only.split(/[, ]+/);
}

['from', 'to'].forEach(k => flags[k] === '0' ? flags[k] = 0 : flags[k]);

return flags;
}

}

export type Options = { dump: boolean; blank: boolean; path: string; target: string; disableFkChecks: boolean; to: string | number; from: string | number; only: string };
4 changes: 2 additions & 2 deletions lib/cli/SchemaCommandFactory.ts
Expand Up @@ -2,8 +2,6 @@ import yargs, { Arguments, Argv, CommandModule } from 'yargs';
import chalk from 'chalk';
import { CLIHelper } from './CLIHelper';

export type Options = { dump: boolean; run: boolean; fkChecks: boolean };

export class SchemaCommandFactory {

static readonly DESCRIPTIONS = {
Expand Down Expand Up @@ -71,3 +69,5 @@ export class SchemaCommandFactory {
}

}

export type Options = { dump: boolean; run: boolean; fkChecks: boolean };
10 changes: 5 additions & 5 deletions lib/entity/EntityTransformer.ts
Expand Up @@ -7,19 +7,19 @@ import { wrap } from './EntityHelper';

export class EntityTransformer {

static toObject<T extends AnyEntity<T>>(entity: T, ignoreFields: string[] = [], visited = new WeakMap()): EntityData<T> {
static toObject<T extends AnyEntity<T>>(entity: T, ignoreFields: string[] = [], visited: string[] = []): EntityData<T> {
const wrapped = wrap(entity);
const platform = wrapped.__em.getDriver().getPlatform();
const pk = platform.getSerializedPrimaryKeyField(wrapped.__meta.primaryKey);
const meta = wrapped.__meta;
const pkProp = meta.properties[meta.primaryKey];
const ret = (wrapped.__primaryKey && !pkProp.hidden ? { [pk]: platform.normalizePrimaryKey(wrapped.__primaryKey as IPrimaryKey) } : {}) as EntityData<T>;

if ((!wrapped.isInitialized() && wrapped.__primaryKey) || visited.has(entity)) {
if ((!wrapped.isInitialized() && wrapped.__primaryKey) || visited.includes(wrap(entity).__uuid)) {
return ret;
}

visited.set(entity, true);
visited.push(wrap(entity).__uuid);

// normal properties
Object.keys(entity)
Expand All @@ -46,7 +46,7 @@ export class EntityTransformer {
return hidden && prop !== meta.primaryKey && !prop.startsWith('_') && !ignoreFields.includes(prop);
}

private static processProperty<T extends AnyEntity<T>>(prop: keyof T, entity: T, ignoreFields: string[], visited: WeakMap<T, boolean>): T[keyof T] | undefined {
private static processProperty<T extends AnyEntity<T>>(prop: keyof T, entity: T, ignoreFields: string[], visited: string[]): T[keyof T] | undefined {
if (entity[prop] as unknown instanceof ArrayCollection) {
return EntityTransformer.processCollection(prop, entity);
}
Expand All @@ -58,7 +58,7 @@ export class EntityTransformer {
return entity[prop];
}

private static processEntity<T extends AnyEntity<T>>(prop: keyof T, entity: T, ignoreFields: string[], visited: WeakMap<T, boolean>): T[keyof T] | undefined {
private static processEntity<T extends AnyEntity<T>>(prop: keyof T, entity: T, ignoreFields: string[], visited: string[]): T[keyof T] | undefined {
const child = wrap(entity[prop] as unknown as T | Reference<T>);
const platform = child.__em.getDriver().getPlatform();

Expand Down
16 changes: 9 additions & 7 deletions lib/migrations/MigrationGenerator.ts
@@ -1,22 +1,24 @@
import { ensureDir, writeFile } from 'fs-extra';
import { CodeBlockWriter, Project, QuoteKind } from 'ts-morph';
import { CodeBlockWriter, IndentationText, Project, QuoteKind } from 'ts-morph';

import { AbstractSqlDriver } from '../drivers';
import { MigrationsOptions } from '../utils';
import { MigrationsOptions, Utils } from '../utils';

export class MigrationGenerator {

private readonly project = new Project();

constructor(protected readonly driver: AbstractSqlDriver,
protected readonly options: MigrationsOptions) {
this.project.manipulationSettings.set({ quoteKind: QuoteKind.Single });
this.project.manipulationSettings.set({ quoteKind: QuoteKind.Single, indentationText: IndentationText.TwoSpaces });
}

async generate(diff: string[]): Promise<string> {
await ensureDir(this.options.path!);
async generate(diff: string[], path?: string): Promise<[string, string]> {
path = Utils.normalizePath(path || this.options.path!);
await ensureDir(path);
const time = new Date().toISOString().replace(/[-T:]|\.\d{3}z$/ig, '');
const migration = this.project.createSourceFile(this.options.path + '/Migration' + time + '.ts', writer => {
const name = `Migration${time}.ts`;
const migration = this.project.createSourceFile(path + '/' + name, writer => {
writer.writeLine(`import { Migration } from 'mikro-orm';`);
writer.blankLine();
writer.write(`export class Migration${time} extends Migration`);
Expand All @@ -32,7 +34,7 @@ export class MigrationGenerator {
const ret = migration.getFullText();
await writeFile(migration.getFilePath(), ret);

return ret;
return [ret, name];
}

createStatement(writer: CodeBlockWriter, sql: string): void {
Expand Down

0 comments on commit 6773ac5

Please sign in to comment.