Skip to content

Commit

Permalink
feat(migrations): allow specifying list of migrations (#741)
Browse files Browse the repository at this point in the history
This allows using migrations with webpack. 

Related: #705
  • Loading branch information
thomaschaaf committed Aug 13, 2020
1 parent c7907db commit 5a0f2a6
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 14 deletions.
48 changes: 48 additions & 0 deletions docs/docs/migrations.md
Expand Up @@ -105,6 +105,54 @@ Then run this script via `ts-node` (or compile it to plain JS and use `node`):
$ ts-node migrate
```

## Importing migrations statically

If you do not want to dynamically import a folder (e.g. when bundling your code with webpack) you can import migrations
directly.

```typescript
import { Migration20191019195930 } from '../migrations/Migration20191019195930.ts';

await MikroORM.init({
migrations: {
migrationsList: [
{
name: 'Migration20191019195930.ts',
class: Migration20191019195930,
},
],
},
});
```

With the help of (webpacks context module api)[https://webpack.js.org/guides/dependency-management/#context-module-api]
we can dynamically import the migrations making it possible to import all files in a folder.

```typescript
import { basename } from 'path';

const migrations = {};

function importAll(r) {
r.keys().forEach(
(key) => (migrations[basename(key)] = Object.values(r(key))[0])
);
}

importAll(require.context('../migrations', false, /\.ts$/));

const migrationsList = Object.keys(migrations).map((migrationName) => ({
name: migrationName,
class: migrations[migrationName],
}));

await MikroORM.init({
migrations: {
migrationsList,
},
});
```

## Limitations

### MySQL
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Expand Up @@ -2,7 +2,7 @@
export {
Constructor, Dictionary, PrimaryKeyType, Primary, IPrimaryKey, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter,
AnyEntity, EntityProperty, EntityMetadata, QBFilterQuery, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection,
GetRepository, EntityRepositoryType,
GetRepository, EntityRepositoryType, MigrationObject,
} from './typings';
export * from './enums';
export * from './exceptions';
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/typings.ts
Expand Up @@ -216,6 +216,16 @@ export interface IMigrator {
down(options?: string | string[] | MigrateOptions): Promise<UmzugMigration[]>;
}

export interface Migration {
up(): Promise<void>;
down(): Promise<void>;
}

export interface MigrationObject {
name: string;
class: Constructor<Migration>;
}

export type FilterDef<T extends AnyEntity<T>> = {
name: string;
cond: FilterQuery<T> | ((args: Dictionary, type: 'read' | 'update' | 'delete') => FilterQuery<T>);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/utils/Configuration.ts
Expand Up @@ -3,7 +3,7 @@ import { inspect } from 'util';
import { NamingStrategy } from '../naming-strategy';
import { CacheAdapter, FileCacheAdapter, NullCacheAdapter } from '../cache';
import { EntityFactory, EntityRepository } from '../entity';
import { AnyEntity, Constructor, Dictionary, EntityClass, EntityClassGroup, FilterDef, Highlighter, IPrimaryKey } from '../typings';
import { AnyEntity, Constructor, Dictionary, EntityClass, EntityClassGroup, FilterDef, Highlighter, IPrimaryKey, Migration, MigrationObject } from '../typings';
import { Hydrator, ObjectHydrator } from '../hydration';
import { NullHighlighter } from '../utils/NullHighlighter';
import { Logger, LoggerNamespace, NotFoundError, Utils } from '../utils';
Expand Down Expand Up @@ -267,6 +267,7 @@ export type MigrationsOptions = {
safe?: boolean;
emit?: 'js' | 'ts';
fileName?: (timestamp: string) => string;
migrationsList?: MigrationObject[];
};

export interface PoolConfig {
Expand Down
34 changes: 27 additions & 7 deletions packages/migrations/src/Migrator.ts
@@ -1,5 +1,6 @@
import umzug, { Umzug } from 'umzug';
import { Utils, Constructor } from '@mikro-orm/core';
// @ts-ignore
import umzug, { Umzug, migrationsList } from 'umzug';
import { Utils, Constructor, MigrationObject } from '@mikro-orm/core';
import { SchemaGenerator, EntityManager } from '@mikro-orm/knex';
import { Migration } from './Migration';
import { MigrationRunner } from './MigrationRunner';
Expand All @@ -18,14 +19,27 @@ export class Migrator {
private readonly storage = new MigrationStorage(this.driver, this.options);

constructor(private readonly em: EntityManager) {
let migrations = {
path: Utils.absolutePath(this.options.path!, this.config.get('baseDir')),
pattern: this.options.pattern,
customResolver: (file: string) => this.resolve(file),
};

if (this.options.migrationsList?.length) {
migrations = migrationsList(
this.options.migrationsList.map((migration: MigrationObject) =>
this.initialize(
migration.class as unknown as Constructor<Migration>,
migration.name
)
)
);
}

this.umzug = new umzug({
storage: this.storage,
logging: this.config.get('logger'),
migrations: {
path: Utils.absolutePath(this.options.path!, this.config.get('baseDir')),
pattern: this.options.pattern,
customResolver: file => this.resolve(file),
},
migrations,
});
}

Expand Down Expand Up @@ -67,9 +81,15 @@ export class Migrator {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const migration = require(file);
const MigrationClass = Object.values(migration)[0] as Constructor<Migration>;

return this.initialize(MigrationClass);
}

protected initialize(MigrationClass: Constructor<Migration>, name?: string) {
const instance = new MigrationClass(this.driver.getConnection(), this.config);

return {
name,
up: () => this.runner.run(instance, 'up'),
down: () => this.runner.run(instance, 'down'),
};
Expand Down
42 changes: 42 additions & 0 deletions tests/Migrator.test.ts
Expand Up @@ -222,3 +222,45 @@ describe('Migrator', () => {
});

});

describe('Migrator - with explicit migrations', () => {

let orm: MikroORM<MySqlDriver>;

beforeAll(async () => {
orm = await initORMMySql(undefined, {
migrations: {
migrationsList: [
{
name: 'test.ts',
class: MigrationTest1,
},
],
},
});
});
afterAll(async () => orm.close(true));

test('runner', async () => {
await orm.em.getConnection().getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!);
const migrator = new Migrator(orm.em);
// @ts-ignore
await migrator.storage.ensureTable();

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.config, { logger });

const spy1 = jest.spyOn(Migration.prototype, 'addSql');
await migrator.up();
expect(spy1).toBeCalledWith('select 1 + 1');
const calls = mock.mock.calls.map(call => {
return call[0]
.replace(/ \[took \d+ ms]/, '')
.replace(/\[query] /, '')
.replace(/ trx\d+/, 'trx_xx');
});
expect(calls).toMatchSnapshot('migrator-migrations-list');
});

});
17 changes: 17 additions & 0 deletions tests/__snapshots__/Migrator.test.ts.snap
@@ -1,5 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Migrator - with explicit migrations runner: migrator-migrations-list 1`] = `
Array [
"select table_name as table_name from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema() (via write connection '127.0.0.1')",
"begin (via write connection '127.0.0.1')",
"select * from \`mikro_orm_migrations\` order by \`id\` asc (via write connection '127.0.0.1')",
"select * from \`mikro_orm_migrations\` order by \`id\` asc (via write connection '127.0.0.1')",
"savepointtrx_xx (via write connection '127.0.0.1')",
"set names utf8mb4; (via write connection '127.0.0.1')",
"set foreign_key_checks = 0; (via write connection '127.0.0.1')",
"select 1 + 1 (via write connection '127.0.0.1')",
"set foreign_key_checks = 1; (via write connection '127.0.0.1')",
"release savepointtrx_xx (via write connection '127.0.0.1')",
"insert into \`mikro_orm_migrations\` (\`name\`) values (?) (via write connection '127.0.0.1')",
"commit (via write connection '127.0.0.1')",
]
`;

exports[`Migrator generate js schema migration: migration-js-dump 1`] = `
Object {
"code": "'use strict';
Expand Down
11 changes: 6 additions & 5 deletions tests/bootstrap.ts
@@ -1,5 +1,5 @@
import 'reflect-metadata';
import { EntityManager, JavaScriptMetadataProvider, MikroORM } from '@mikro-orm/core';
import { Configuration, EntityManager, JavaScriptMetadataProvider, MikroORM, Options, Utils } from '@mikro-orm/core';
import { AbstractSqlDriver, SchemaGenerator, SqlEntityManager, SqlEntityRepository } from '@mikro-orm/knex';
import { SqliteDriver } from '@mikro-orm/sqlite';
import { MongoDriver } from '@mikro-orm/mongodb';
Expand Down Expand Up @@ -47,8 +47,8 @@ export async function initORMMongo() {
return orm;
}

export async function initORMMySql<D extends MySqlDriver | MariaDbDriver = MySqlDriver>(type: 'mysql' | 'mariadb' = 'mysql') {
let orm = await MikroORM.init<AbstractSqlDriver>({
export async function initORMMySql<D extends MySqlDriver | MariaDbDriver = MySqlDriver>(type: 'mysql' | 'mariadb' = 'mysql', additionalOptions: Partial<Options> = {}) {
let orm = await MikroORM.init<AbstractSqlDriver>(Utils.merge({
entities: ['entities-sql/**/*.js', '!**/Label2.js'],
entitiesTs: ['entities-sql/**/*.ts', '!**/Label2.ts'],
clientUrl: `mysql://root@127.0.0.1:3306/mikro_orm_test`,
Expand All @@ -57,13 +57,14 @@ export async function initORMMySql<D extends MySqlDriver | MariaDbDriver = MySql
debug: ['query'],
timezone: 'Z',
charset: 'utf8mb4',
logger: i => i,
logger: (i: any) => i,
multipleStatements: true,
entityRepository: SqlEntityRepository,
type,
replicas: [{ name: 'read-1' }, { name: 'read-2' }], // create two read replicas with same configuration, just for testing purposes
migrations: { path: BASE_DIR + '/../temp/migrations' },
});

}, additionalOptions));

const schemaGenerator = new SchemaGenerator(orm.em);
await schemaGenerator.ensureDatabase();
Expand Down

0 comments on commit 5a0f2a6

Please sign in to comment.