Skip to content

Commit

Permalink
feat(migrations): allow providing custom MigrationGenerator
Browse files Browse the repository at this point in the history
Closes #1913
  • Loading branch information
B4nan committed Aug 27, 2021
1 parent 02dd67c commit 3cc366b
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 85 deletions.
36 changes: 36 additions & 0 deletions docs/docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,46 @@ await MikroORM.init({
safe: false, // allow to disable table and column dropping
snapshot: true, // save snapshot when creating new migrations
emit: 'ts', // migration generation mode
generator: TSMigrationGenerator, // migration generator, e.g. to allow custom formatting
},
})
```

## Using custom `MigrationGenerator`

When we generate new migrations, `MigrationGenerator` class is responsible for
generating the file contents. We can provide our own implementation to do things
like formatting the SQL statement.

```ts
import { TSMigrationGenerator } from '@mikro-orm/migrations';
import { format } from 'sql-formatter';

class CustomMigrationGenerator extends TSMigrationGenerator {

generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string {
const comment = '// this file was generated via custom migration generator\n\n';
return comment + super.generateMigrationFile(className, diff);
}

createStatement(sql: string, padLeft: number): string {
sql = format(sql, { language: 'postgresql' });
// a bit of indenting magic
sql = sql.split('\n').map((l, i) => i === 0 ? l : `${' '.repeat(padLeft + 13)}${l}`).join('\n');

return super.createStatement(sql, padLeft);
}

}

await MikroORM.init({
// ...
migrations: {
generator: CustomMigrationGenerator,
},
});
```

## Using via CLI

You can use it via CLI:
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@
"separateMajorMinor": false,
"packageRules": [
{
"matchUpdateTypes": ["patch", "minor"],
"matchUpdateTypes": [
"patch",
"minor"
],
"groupName": "patch/minor dependencies",
"groupSlug": "all-non-major",
"automerge": true,
Expand Down Expand Up @@ -140,6 +143,7 @@
"lint-staged": "11.1.2",
"rimraf": "3.0.2",
"run-rs": "0.7.5",
"sql-formatter": "^4.0.2",
"ts-jest": "26.5.6",
"ts-node": "10.2.1",
"typescript": "4.4.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
/* istanbul ignore file */
export {
Constructor, Dictionary, PrimaryKeyType, PrimaryKeyProp, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter,
AnyEntity, EntityProperty, EntityMetadata, QBFilterQuery, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection,
GetRepository, EntityRepositoryType, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, PlainObject,
AnyEntity, EntityProperty, EntityMetadata, QBFilterQuery, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator,
GetRepository, EntityRepositoryType, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, PlainObject, MigrationDiff,
} from './typings';
export * from './enums';
export * from './errors';
Expand Down
50 changes: 49 additions & 1 deletion packages/core/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,18 +391,66 @@ export interface IEntityGenerator {

type UmzugMigration = { path?: string; file: string };
type MigrateOptions = { from?: string | number; to?: string | number; migrations?: string[] };
type MigrationResult = { fileName: string; code: string; diff: string[] };
type MigrationResult = { fileName: string; code: string; diff: MigrationDiff };
type MigrationRow = { name: string; executed_at: Date };

export interface IMigrator {
/**
* Checks current schema for changes, generates new migration if there are any.
*/
createMigration(path?: string, blank?: boolean, initial?: boolean): Promise<MigrationResult>;

/**
* 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.
* Initial migration can be created only if the schema is already aligned with the metadata, or when no schema
* is present - in such case regular migration would have the same effect.
*/
createInitialMigration(path?: string): Promise<MigrationResult>;

/**
* Returns list of already executed migrations.
*/
getExecutedMigrations(): Promise<MigrationRow[]>;

/**
* Returns list of pending (not yet executed) migrations found in the migration directory.
*/
getPendingMigrations(): Promise<UmzugMigration[]>;

/**
* Executes specified migrations. Without parameter it will migrate up to the latest version.
*/
up(options?: string | string[] | MigrateOptions): Promise<UmzugMigration[]>;

/**
* Executes down migrations to the given point. Without parameter it will migrate one version down.
*/
down(options?: string | string[] | MigrateOptions): Promise<UmzugMigration[]>;
}

export interface MigrationDiff {
up: string[];
down: string[];
}

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]>;

/**
* Creates single migration statement. By default adds `this.addSql(sql);` to the code.
*/
createStatement(sql: string, padLeft: number): string;

/**
* Returns the file contents of given migration.
*/
generateMigrationFile(className: string, diff: MigrationDiff): string;
}

export interface Migration {
up(): Promise<void>;
down(): Promise<void>;
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/utils/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import { inspect } from 'util';
import { NamingStrategy } from '../naming-strategy';
import { CacheAdapter, FileCacheAdapter, NullCacheAdapter } from '../cache';
import { EntityRepository } from '../entity';
import { AnyEntity, Constructor, Dictionary, EntityClass, EntityClassGroup, FilterDef, Highlighter, HydratorConstructor, IHydrator, IPrimaryKey, MaybePromise, MigrationObject } from '../typings';
import {
AnyEntity,
Constructor,
Dictionary,
EntityClass,
EntityClassGroup,
FilterDef,
Highlighter,
HydratorConstructor,
IHydrator,
IMigrationGenerator,
IPrimaryKey,
MaybePromise,
MigrationObject,
} from '../typings';
import { ObjectHydrator } from '../hydration';
import { NullHighlighter } from '../utils/NullHighlighter';
import { Logger, LoggerNamespace } from '../utils/Logger';
Expand Down Expand Up @@ -318,6 +332,7 @@ export type MigrationsOptions = {
safe?: boolean;
snapshot?: boolean;
emit?: 'js' | 'ts';
generator?: Constructor<IMigrationGenerator>;
fileName?: (timestamp: string) => string;
migrationsList?: MigrationObject[];
};
Expand Down
30 changes: 30 additions & 0 deletions packages/migrations/src/JSMigrationGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MigrationGenerator } from './MigrationGenerator';

export class JSMigrationGenerator extends MigrationGenerator {

/**
* @inheritDoc
*/
generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string {
let ret = `'use strict';\n`;
ret += `Object.defineProperty(exports, '__esModule', { value: true });\n`;
ret += `const Migration = require('@mikro-orm/migrations').Migration;\n\n`;
ret += `class ${className} extends Migration {\n\n`;
ret += ` async up() {\n`;
diff.up.forEach(sql => ret += this.createStatement(sql, 4));
ret += ` }\n\n`;

/* istanbul ignore else */
if (diff.down.length > 0) {
ret += ` async down() {\n`;
diff.down.forEach(sql => ret += this.createStatement(sql, 4));
ret += ` }\n\n`;
}

ret += `}\n`;
ret += `exports.${className} = ${className};\n`;

return ret;
}

}
62 changes: 13 additions & 49 deletions packages/migrations/src/MigrationGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import { ensureDir, writeFile } from 'fs-extra';
import { MigrationsOptions, NamingStrategy, Utils } from '@mikro-orm/core';
import { IMigrationGenerator, MigrationsOptions, NamingStrategy, Utils } from '@mikro-orm/core';
import { AbstractSqlDriver } from '@mikro-orm/knex';

export class MigrationGenerator {
export abstract class MigrationGenerator implements IMigrationGenerator {

constructor(protected readonly driver: AbstractSqlDriver,
protected readonly namingStrategy: NamingStrategy,
protected readonly options: MigrationsOptions) { }

/**
* @inheritDoc
*/
async generate(diff: { up: string[]; down: string[] }, path?: string): Promise<[string, string]> {
path = Utils.normalizePath(path || this.options.path!);
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}`;
let ret: string;

if (this.options.emit === 'js') {
ret = this.generateJSMigrationFile(className, diff);
} else {
ret = this.generateTSMigrationFile(className, diff);
}

const ret = this.generateMigrationFile(className, diff);
await writeFile(path + '/' + fileName, ret);

return [ret, fileName];
}

/**
* @inheritDoc
*/
createStatement(sql: string, padLeft: number): string {
if (sql) {
const padding = ' '.repeat(padLeft);
Expand All @@ -36,44 +35,9 @@ export class MigrationGenerator {
return '\n';
}

generateJSMigrationFile(className: string, diff: { up: string[]; down: string[] }): string {
let ret = `'use strict';\n`;
ret += `Object.defineProperty(exports, '__esModule', { value: true });\n`;
ret += `const Migration = require('@mikro-orm/migrations').Migration;\n\n`;
ret += `class ${className} extends Migration {\n\n`;
ret += ` async up() {\n`;
diff.up.forEach(sql => ret += this.createStatement(sql, 4));
ret += ` }\n\n`;

/* istanbul ignore else */
if (diff.down.length > 0) {
ret += ` async down() {\n`;
diff.down.forEach(sql => ret += this.createStatement(sql, 4));
ret += ` }\n\n`;
}

ret += `}\n`;
ret += `exports.${className} = ${className};\n`;

return ret;
}

generateTSMigrationFile(className: string, diff: { up: string[]; down: string[] }): string {
let ret = `import { Migration } from '@mikro-orm/migrations';\n\n`;
ret += `export class ${className} extends Migration {\n\n`;
ret += ` async up(): Promise<void> {\n`;
diff.up.forEach(sql => ret += this.createStatement(sql, 4));
ret += ` }\n\n`;

if (diff.down.length > 0) {
ret += ` async down(): Promise<void> {\n`;
diff.down.forEach(sql => ret += this.createStatement(sql, 4));
ret += ` }\n\n`;
}

ret += `}\n`;

return ret;
}
/**
* @inheritDoc
*/
abstract generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string;

}
35 changes: 31 additions & 4 deletions packages/migrations/src/Migrator.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import umzug, { migrationsList, Umzug } from 'umzug';
import { join } from 'path';
import { ensureDir, pathExists, writeJSON } from 'fs-extra';
import { Constructor, Dictionary, Transaction, Utils, t, Type } from '@mikro-orm/core';
import { Constructor, Dictionary, Transaction, Utils, t, Type, IMigrator, IMigrationGenerator } from '@mikro-orm/core';
import { DatabaseSchema, DatabaseTable, EntityManager, SchemaGenerator } from '@mikro-orm/knex';
import { Migration } from './Migration';
import { MigrationRunner } from './MigrationRunner';
import { MigrationGenerator } from './MigrationGenerator';
import { MigrationStorage } from './MigrationStorage';
import { MigrateOptions, MigrationResult, MigrationRow, UmzugMigration } from './typings';
import { TSMigrationGenerator } from './TSMigrationGenerator';
import { JSMigrationGenerator } from './JSMigrationGenerator';

export class Migrator {
export class Migrator implements IMigrator {

private readonly umzug: Umzug;
private readonly driver = this.em.getDriver();
private readonly schemaGenerator = new SchemaGenerator(this.em);
private readonly config = this.em.config;
private readonly options = this.config.get('migrations');
private readonly runner = new MigrationRunner(this.driver, this.options, this.config);
private readonly generator = new MigrationGenerator(this.driver, this.config.getNamingStrategy(), this.options);
private readonly generator: IMigrationGenerator;
private readonly storage = new MigrationStorage(this.driver, this.options);
private readonly absolutePath = Utils.absolutePath(this.options.path!, this.config.get('baseDir'));
private readonly snapshotPath = join(this.absolutePath, `.snapshot-${this.config.get('dbName')}.json`);
Expand All @@ -39,8 +40,19 @@ export class Migrator {
logging: this.config.get('logger'),
migrations,
});

if (this.options.generator) {
this.generator = new this.options.generator(this.driver, this.config.getNamingStrategy(), this.options);
} else if (this.options.emit === 'js') {
this.generator = new JSMigrationGenerator(this.driver, this.config.getNamingStrategy(), this.options);
} else {
this.generator = new TSMigrationGenerator(this.driver, this.config.getNamingStrategy(), this.options);
}
}

/**
* @inheritDoc
*/
async createMigration(path?: string, blank = false, initial = false): Promise<MigrationResult> {
if (initial) {
return this.createInitialMigration(path);
Expand All @@ -63,6 +75,9 @@ export class Migrator {
};
}

/**
* @inheritDoc
*/
async createInitialMigration(path?: string): Promise<MigrationResult> {
await this.ensureMigrationsDirExists();
const schemaExists = await this.validateInitialMigration();
Expand Down Expand Up @@ -124,24 +139,36 @@ export class Migrator {
return expected.size === exists.size;
}

/**
* @inheritDoc
*/
async getExecutedMigrations(): Promise<MigrationRow[]> {
await this.ensureMigrationsDirExists();
await this.schemaGenerator.ensureDatabase();
await this.storage.ensureTable();
return this.storage.getExecutedMigrations();
}

/**
* @inheritDoc
*/
async getPendingMigrations(): Promise<UmzugMigration[]> {
await this.ensureMigrationsDirExists();
await this.schemaGenerator.ensureDatabase();
await this.storage.ensureTable();
return this.umzug.pending();
}

/**
* @inheritDoc
*/
async up(options?: string | string[] | MigrateOptions): Promise<UmzugMigration[]> {
return this.runMigrations('up', options);
}

/**
* @inheritDoc
*/
async down(options?: string | string[] | MigrateOptions): Promise<UmzugMigration[]> {
return this.runMigrations('down', options);
}
Expand Down

0 comments on commit 3cc366b

Please sign in to comment.