lib/src/orm/migrations/Migrator.ts
2022-01-26 19:37:54 -06:00

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)
}
}