import {Inject, Injectable} from '../../di' import {Migrator} from './Migrator' import {DatabaseService} from '../DatabaseService' import {FieldType} from '../types' import {Migration} from './Migration' import {Builder} from '../builder/Builder' /** * Migrator implementation that tracks applied migrations in a database table. * @todo allow configuring more of this */ @Injectable() export class DatabaseMigrator extends Migrator { @Inject() protected readonly db!: DatabaseService /** True if we've initialized the migrator. */ protected initialized = false public async initialize(): Promise { await super.initialize() if ( this.initialized ) { return } const schema = this.db.get().schema() if ( !(await schema.hasTable('migrations')) ) { const table = await schema.table('migrations') table.primaryKey('id', FieldType.serial).required() table.column('identifier') .type(FieldType.varchar) .required() table.column('applygroup') .type(FieldType.integer) .required() table.column('applydate') .type(FieldType.timestamp) .required() await schema.commit(table) } this.initialized = true } async has(migration: Migration): Promise { return this.builder() .connection('default') .select('id') .from('migrations') .where('identifier', '=', migration.identifier) .exists() } async markApplied(migrations: Migration | Migration[], applyDate: Date = new Date()): Promise { if ( !Array.isArray(migrations) ) { migrations = [migrations] } const applyGroup = await this.getNextGroupIdentifier() const rows = migrations.map(migration => { return { applygroup: applyGroup, applydate: applyDate, identifier: migration.identifier, } }) await this.builder() .connection('default') .table('migrations') .insert(rows) } async unmarkApplied(migrations: Migration | Migration[]): Promise { if ( !Array.isArray(migrations) ) { migrations = [migrations] } const identifiers = migrations.map(migration => migration.identifier) await this.builder() .connection('default') .table('migrations') .whereIn('identifier', identifiers) .delete() } async getLastApplyGroup(): Promise { const applyGroup = await this.builder() .connection('default') .select('applygroup') .from('migrations') .get() .max('applygroup') return this.builder() .connection('default') .select('identifier') .from('migrations') .where('applygroup', '=', applyGroup) .get() .asyncPipe() .tap(coll => { return coll.pluck('identifier') }) .tap(coll => { return coll.all() }) .resolve() } /** * Helper method to look up the next `applygroup` that should be used. * @protected */ protected async getNextGroupIdentifier(): Promise { const current = await this.builder() .connection('default') .select('applygroup') .from('migrations') .get() .max('applygroup') return (current ?? 0) + 1 } /** * Given a list of migration identifiers, filter out those that have been applied. * @override to make this more efficient * @param identifiers * @protected */ protected async filterAppliedMigrations(identifiers: string[]): Promise { const existing = (await this.builder() .connection('default') .select('identifier') .from('migrations') .whereIn('identifier', identifiers) .get() .collect()) .pluck('identifier') return identifiers.filter(id => !existing.includes(id)) } /** * Given a list of migration identifiers, filter out those that have not been applied. * @override to make this more efficient * @param identifiers * @protected */ protected async filterPendingMigrations(identifiers: string[]): Promise { const existing = await this.builder() .connection('default') .select('identifier') .from('migrations') .whereIn('identifier', identifiers) .get() .pluck('identifier') return existing.all() } /** * Get a query builder instance. * @protected */ protected builder(): Builder { return this.injector.make(Builder) } }