import {Container, Inject, Injectable} from '../../di' import {Awaitable, collect, ErrorWithContext} from '../../util' import {Migration} from './Migration' import {Migrations} from '../services/Migrations' import {EventBus} from '../../event/EventBus' import {ApplyingMigrationEvent} from './events/ApplyingMigrationEvent' import {AppliedMigrationEvent} from './events/AppliedMigrationEvent' import {RollingBackMigrationEvent} from './events/RollingBackMigrationEvent' import {RolledBackMigrationEvent} from './events/RolledBackMigrationEvent' import {NothingToMigrateError} from './NothingToMigrateError' /** * Manages single-run patches/migrations. */ @Injectable() export abstract class Migrator { @Inject() protected readonly migrations!: Migrations @Inject() protected readonly bus!: EventBus @Inject('injector') protected readonly injector!: Container /** * Should resolve true if the given migration has already been applied. * @param migration */ public abstract has(migration: Migration): Awaitable /** * Should mark the given migrations as being applied. * * If a date is specified, then that is the timestamp when the migrations * were applied, otherwise, use `new Date()`. * * @param migrations * @param date */ public abstract markApplied(migrations: Migration | Migration[], date?: Date): Awaitable /** * Should un-mark the given migrations as being applied. * @param migration */ public abstract unmarkApplied(migration: Migration | Migration[]): Awaitable /** * Get the identifiers of the last group of migrations that were applied. */ public abstract getLastApplyGroup(): Awaitable /** * Do any initial setup required to get the migrator ready. * This can be overridden by implementation classes to do any necessary setup. */ public initialize(): Awaitable {} // eslint-disable-line @typescript-eslint/no-empty-function /** * Apply pending migrations. * * If identifiers are specified, only the pending migrations with those * identifiers are applied. If none are specified, all pending migrations * will be applied. * * @param identifiers */ public async migrate(identifiers?: string[]): Promise { await this.initialize() if ( !identifiers ) { identifiers = this.getAllMigrationIdentifiers() } identifiers = (await this.filterAppliedMigrations(identifiers)).sort() if ( !identifiers.length ) { throw new NothingToMigrateError() } const migrations = collect(identifiers) .map(id => { const migration = this.migrations.get(id) if ( !migration ) { throw new ErrorWithContext(`Unable to find migration with identifier: ${id}`, { identifier: id, }) } return migration }) await migrations.promiseMap(migration => { return this.apply(migration) }) await this.markApplied(migrations.all()) } /** * Rollback applied migrations. * * If specified, only applied migrations with the given identifiers will * be rolled back. If not specified, then the last "batch" of applied * migrations will be rolled back. * * @param identifiers */ public async rollback(identifiers?: string[]): Promise { await this.initialize() if ( !identifiers ) { identifiers = await this.getLastApplyGroup() } identifiers = (await this.filterPendingMigrations(identifiers)).sort() if ( !identifiers.length ) { throw new NothingToMigrateError() } const migrations = collect(identifiers) .map(id => { const migration = this.migrations.get(id) if ( !migration ) { throw new ErrorWithContext(`Unable to find migration with identifier: ${id}`, { identifier: id, }) } return migration }) await migrations.promiseMap(migration => { return this.undo(migration) }) await this.unmarkApplied(migrations.all()) } /** * Apply a single migration. * @param migration */ public async apply(migration: Migration): Promise { await this.initialize() await this.applying(migration) await migration.up() await this.applied(migration) } /** * Rollback a single migration. * @param migration */ public async undo(migration: Migration): Promise { await this.initialize() await this.rollingBack(migration) await migration.down() await this.rolledBack(migration) } /** * Get all registered migrations, by their string-form identifiers. * @protected */ protected getAllMigrationIdentifiers(): string[] { return collect(this.migrations.namespaces()) .map(nsp => { return this.migrations.all(nsp) .map(id => `${nsp}:${id}`) }) .tap(coll => { // non-namespaced migrations coll.push(this.migrations.all()) return coll }) .reduce((current, item) => { return current.concat(item) }, []) } /** * Given a list of migration identifiers, filter out those that have been applied. * @param identifiers * @protected */ protected async filterAppliedMigrations(identifiers: string[]): Promise { return collect(identifiers) .partialMap(identifier => { const migration = this.migrations.get(identifier) if ( migration ) { return { identifier, migration, } } }) .asyncPipe() .tap(coll => { return coll.promiseMap(async group => { return { ...group, has: await this.has(group.migration), } }) }) .tap(coll => { return coll.filter(group => !group.has) .pluck('identifier') .all() }) .resolve() } /** * Given a list of migration identifiers, filter out those that have not been applied. * @param identifiers * @protected */ protected async filterPendingMigrations(identifiers: string[]): Promise { return collect(identifiers) .partialMap(identifier => { const migration = this.migrations.get(identifier) if ( migration ) { return { identifier, migration, } } }) .asyncPipe() .tap(coll => { return coll.promiseMap(async group => { return { ...group, has: await this.has(group.migration), } }) }) .tap(coll => { return coll.filter(group => group.has) .pluck('identifier') .all() }) .resolve() } /** * Fire the ApplyingMigrationEvent. * @param migration * @protected */ protected async applying(migration: Migration): Promise { const event = this.injector.make(ApplyingMigrationEvent, migration) await this.bus.dispatch(event) } /** * Fire the AppliedMigrationEvent. * @param migration * @protected */ protected async applied(migration: Migration): Promise { const event = this.injector.make(AppliedMigrationEvent, migration) await this.bus.dispatch(event) } /** * Fire the RollingBackMigrationEvent. * @param migration * @protected */ protected async rollingBack(migration: Migration): Promise { const event = this.injector.make(RollingBackMigrationEvent, migration) await this.bus.dispatch(event) } /** * Fire the RolledBackMigrationEvent. * @param migration * @protected */ protected async rolledBack(migration: Migration): Promise { const event = this.injector.make(RolledBackMigrationEvent, migration) await this.bus.dispatch(event) } }