256 lines
7.6 KiB
TypeScript
256 lines
7.6 KiB
TypeScript
import {Container, Inject, Injectable} from '../../di'
|
|
import {Awaitable, collect, ErrorWithContext} from '../../util'
|
|
import {Migration} from './Migration'
|
|
import {Migrations} from '../services/Migrations'
|
|
import {ApplyingMigrationEvent} from './events/ApplyingMigrationEvent'
|
|
import {AppliedMigrationEvent} from './events/AppliedMigrationEvent'
|
|
import {RollingBackMigrationEvent} from './events/RollingBackMigrationEvent'
|
|
import {RolledBackMigrationEvent} from './events/RolledBackMigrationEvent'
|
|
import {NothingToMigrateError} from './NothingToMigrateError'
|
|
import {Bus} from '../../support/bus'
|
|
|
|
/**
|
|
* Manages single-run patches/migrations.
|
|
*/
|
|
@Injectable()
|
|
export abstract class Migrator {
|
|
@Inject(Migrations, { debug: true })
|
|
protected readonly migrations!: Migrations
|
|
|
|
@Inject()
|
|
protected readonly bus!: Bus
|
|
|
|
@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<boolean>
|
|
|
|
/**
|
|
* 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<void>
|
|
|
|
/**
|
|
* Should un-mark the given migrations as being applied.
|
|
* @param migration
|
|
*/
|
|
public abstract unmarkApplied(migration: Migration | Migration[]): Awaitable<void>
|
|
|
|
/**
|
|
* Get the identifiers of the last group of migrations that were applied.
|
|
*/
|
|
public abstract getLastApplyGroup(): Awaitable<string[]>
|
|
|
|
/**
|
|
* 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<void> {} // 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string>(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<string[]> {
|
|
const ids = await collect(identifiers)
|
|
.toAsync()
|
|
.filterOut(async id => this.has(this.migrations.getOrFail(id)))
|
|
|
|
return ids.all()
|
|
}
|
|
|
|
/**
|
|
* Given a list of migration identifiers, filter out those that have not been applied.
|
|
* @param identifiers
|
|
* @protected
|
|
*/
|
|
protected async filterPendingMigrations(identifiers: string[]): Promise<string[]> {
|
|
const ids = await collect(identifiers)
|
|
.toAsync()
|
|
.filter(async id => this.has(this.migrations.getOrFail(id)))
|
|
|
|
return ids.all()
|
|
}
|
|
|
|
/**
|
|
* Fire the ApplyingMigrationEvent.
|
|
* @param migration
|
|
* @protected
|
|
*/
|
|
protected async applying(migration: Migration): Promise<void> {
|
|
const event = <ApplyingMigrationEvent> this.injector.make(ApplyingMigrationEvent, migration)
|
|
await this.bus.push(event)
|
|
}
|
|
|
|
/**
|
|
* Fire the AppliedMigrationEvent.
|
|
* @param migration
|
|
* @protected
|
|
*/
|
|
protected async applied(migration: Migration): Promise<void> {
|
|
const event = <AppliedMigrationEvent> this.injector.make(AppliedMigrationEvent, migration)
|
|
await this.bus.push(event)
|
|
}
|
|
|
|
/**
|
|
* Fire the RollingBackMigrationEvent.
|
|
* @param migration
|
|
* @protected
|
|
*/
|
|
protected async rollingBack(migration: Migration): Promise<void> {
|
|
const event = <RollingBackMigrationEvent> this.injector.make(RollingBackMigrationEvent, migration)
|
|
await this.bus.push(event)
|
|
}
|
|
|
|
/**
|
|
* Fire the RolledBackMigrationEvent.
|
|
* @param migration
|
|
* @protected
|
|
*/
|
|
protected async rolledBack(migration: Migration): Promise<void> {
|
|
const event = <RolledBackMigrationEvent> this.injector.make(RolledBackMigrationEvent, migration)
|
|
await this.bus.push(event)
|
|
}
|
|
}
|