From 5a0f2a6caebff1d40b47fcb49adb15d87413b4b8 Mon Sep 17 00:00:00 2001 From: Thomas Schaaf Date: Thu, 13 Aug 2020 15:48:31 +0200 Subject: [PATCH] feat(migrations): allow specifying list of migrations (#741) This allows using migrations with webpack. Related: #705 --- docs/docs/migrations.md | 48 +++++++++++++++++++++++ packages/core/src/index.ts | 2 +- packages/core/src/typings.ts | 10 +++++ packages/core/src/utils/Configuration.ts | 3 +- packages/migrations/src/Migrator.ts | 34 ++++++++++++---- tests/Migrator.test.ts | 42 ++++++++++++++++++++ tests/__snapshots__/Migrator.test.ts.snap | 17 ++++++++ tests/bootstrap.ts | 11 +++--- 8 files changed, 153 insertions(+), 14 deletions(-) diff --git a/docs/docs/migrations.md b/docs/docs/migrations.md index 63b811ce4172..06e4d76e6135 100644 --- a/docs/docs/migrations.md +++ b/docs/docs/migrations.md @@ -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 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8005705e2402..5a1acd19cca7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'; diff --git a/packages/core/src/typings.ts b/packages/core/src/typings.ts index af89a2712113..c81eb91cd0d7 100644 --- a/packages/core/src/typings.ts +++ b/packages/core/src/typings.ts @@ -216,6 +216,16 @@ export interface IMigrator { down(options?: string | string[] | MigrateOptions): Promise; } +export interface Migration { + up(): Promise; + down(): Promise; +} + +export interface MigrationObject { + name: string; + class: Constructor; +} + export type FilterDef> = { name: string; cond: FilterQuery | ((args: Dictionary, type: 'read' | 'update' | 'delete') => FilterQuery); diff --git a/packages/core/src/utils/Configuration.ts b/packages/core/src/utils/Configuration.ts index a869a329f879..eb8a3ebe84c1 100644 --- a/packages/core/src/utils/Configuration.ts +++ b/packages/core/src/utils/Configuration.ts @@ -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'; @@ -267,6 +267,7 @@ export type MigrationsOptions = { safe?: boolean; emit?: 'js' | 'ts'; fileName?: (timestamp: string) => string; + migrationsList?: MigrationObject[]; }; export interface PoolConfig { diff --git a/packages/migrations/src/Migrator.ts b/packages/migrations/src/Migrator.ts index 1efaa14ede11..8528813d909a 100644 --- a/packages/migrations/src/Migrator.ts +++ b/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'; @@ -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.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, }); } @@ -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; + + return this.initialize(MigrationClass); + } + + protected initialize(MigrationClass: Constructor, 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'), }; diff --git a/tests/Migrator.test.ts b/tests/Migrator.test.ts index dc9d327f7b1c..dcd1232d2985 100644 --- a/tests/Migrator.test.ts +++ b/tests/Migrator.test.ts @@ -222,3 +222,45 @@ describe('Migrator', () => { }); }); + +describe('Migrator - with explicit migrations', () => { + + let orm: MikroORM; + + 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'); + }); + +}); diff --git a/tests/__snapshots__/Migrator.test.ts.snap b/tests/__snapshots__/Migrator.test.ts.snap index e93acb7436be..9f8bf58f0ba7 100644 --- a/tests/__snapshots__/Migrator.test.ts.snap +++ b/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'; diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index 77320b3bb817..e6c4bf832390 100644 --- a/tests/bootstrap.ts +++ b/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'; @@ -47,8 +47,8 @@ export async function initORMMongo() { return orm; } -export async function initORMMySql(type: 'mysql' | 'mariadb' = 'mysql') { - let orm = await MikroORM.init({ +export async function initORMMySql(type: 'mysql' | 'mariadb' = 'mysql', additionalOptions: Partial = {}) { + let orm = await MikroORM.init(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`, @@ -57,13 +57,14 @@ export async function initORMMySql 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();