From 8d1c9468f1151b06bd6087ca4f11595660256039 Mon Sep 17 00:00:00 2001 From: Harminder virk Date: Tue, 26 Nov 2019 18:48:45 +0530 Subject: [PATCH] feat: add rollback command --- adonis-typings/migrator.ts | 12 +++- commands/Migrate.ts | 94 ++------------------------------ commands/MigrationsBase.ts | 109 +++++++++++++++++++++++++++++++++++++ commands/Rollback.ts | 79 +++++++++++++++++++++++++++ src/Migrator/index.ts | 5 +- 5 files changed, 206 insertions(+), 93 deletions(-) create mode 100644 commands/MigrationsBase.ts create mode 100644 commands/Rollback.ts diff --git a/adonis-typings/migrator.ts b/adonis-typings/migrator.ts index 3a96e5a6..0f39c395 100644 --- a/adonis-typings/migrator.ts +++ b/adonis-typings/migrator.ts @@ -9,6 +9,7 @@ declare module '@ioc:Adonis/Lucid/Migrator' { import { SchemaConstructorContract } from '@ioc:Adonis/Lucid/Schema' + import { EventEmitter } from 'events' /** * Migration node returned by the migration source @@ -29,7 +30,7 @@ declare module '@ioc:Adonis/Lucid/Migrator' { dryRun?: boolean, } | { direction: 'down', - batch: number, + batch?: number, connectionName?: string, dryRun?: boolean, } @@ -47,7 +48,7 @@ declare module '@ioc:Adonis/Lucid/Migrator' { /** * Shape of the migrator */ - export interface MigratorContract { + export interface MigratorContract extends EventEmitter { dryRun: boolean direction: 'up' | 'down' status: 'completed' | 'skipped' | 'pending' | 'error' @@ -56,5 +57,12 @@ declare module '@ioc:Adonis/Lucid/Migrator' { run (): Promise getList (): Promise<{ batch: number, name: string, migration_time: Date }[]> close (): Promise + on (event: 'start', callback: () => void): this + on (event: 'acquire:lock', callback: () => void): this + on (event: 'release:lock', callback: () => void): this + on (event: 'create:schema:table', callback: () => void): this + on (event: 'migration:start', callback: (file: MigratedFileNode) => void): this + on (event: 'migration:completed', callback: (file: MigratedFileNode) => void): this + on (event: 'migration:error', callback: (file: MigratedFileNode) => void): this } } diff --git a/commands/Migrate.ts b/commands/Migrate.ts index cbb7fc41..1d909953 100644 --- a/commands/Migrate.ts +++ b/commands/Migrate.ts @@ -7,19 +7,19 @@ * file that was distributed with this source code. */ -import logUpdate from 'log-update' +import { flags } from '@adonisjs/ace' import { inject } from '@adonisjs/fold' -import { BaseCommand, flags } from '@adonisjs/ace' import { DatabaseContract } from '@ioc:Adonis/Lucid/Database' -import { MigratedFileNode } from '@ioc:Adonis/Lucid/Migrator' import { ApplicationContract } from '@ioc:Adonis/Core/Application' +import MigrationsBase from './MigrationsBase' + /** * The command is meant to migrate the database by execute migrations * in `up` direction. */ @inject([null, 'Adonis/Lucid/Database']) -export default class Migrate extends BaseCommand { +export default class Migrate extends MigrationsBase { public static commandName = 'migration:run' public static description = 'Run pending migrations' @@ -41,35 +41,6 @@ export default class Migrate extends BaseCommand { super(app) } - /** - * Returns beautified log message string - */ - private _getLogMessage (file: MigratedFileNode): string { - const message = `${file.migration.name} ${this.colors.gray(`(batch: ${file.batch})`)}` - - if (file.status === 'pending') { - return `${this.colors.yellow('pending')} ${message}` - } - - const lines: string[] = [] - - if (file.status === 'completed') { - lines.push(`${this.colors.green('completed')} ${message}`) - } else { - lines.push(`${this.colors.red('error')} ${message}`) - } - - if (file.queries.length) { - lines.push(' START QUERIES') - lines.push(' ================') - file.queries.forEach((query) => lines.push(` ${query}`)) - lines.push(' ================') - lines.push(' END QUERIES') - } - - return lines.join('\n') - } - /** * Handle command */ @@ -87,11 +58,6 @@ export default class Migrate extends BaseCommand { return } - /** - * A set of files processed and emitted using event emitter. - */ - const processedFiles: Set = new Set() - /** * New up migrator */ @@ -102,56 +68,6 @@ export default class Migrate extends BaseCommand { dryRun: this.dryRun, }) - /** - * Starting to process a new migration file - */ - migrator.on('migration:start', (file) => { - processedFiles.add(file.migration.name) - logUpdate(this._getLogMessage(file)) - }) - - /** - * Migration completed - */ - migrator.on('migration:completed', (file) => { - logUpdate(this._getLogMessage(file)) - logUpdate.done() - }) - - /** - * Migration error - */ - migrator.on('migration:error', (file) => { - logUpdate(this._getLogMessage(file)) - logUpdate.done() - }) - - /** - * Run and close db connection - */ - await migrator.run() - await migrator.close() - - /** - * Log all pending files. This will happen, when one of the migration - * fails with an error and then the migrator stops emitting events. - */ - Object.keys(migrator.migratedFiles).forEach((file) => { - if (!processedFiles.has(file)) { - console.log(this._getLogMessage(migrator.migratedFiles[file])) - } - }) - - /** - * Log final status - */ - switch (migrator.status) { - case 'skipped': - console.log(this.colors.cyan('Already upto date')) - break - case 'error': - this.logger.fatal(migrator.error!) - break - } + await this.$runMigrations(migrator) } } diff --git a/commands/MigrationsBase.ts b/commands/MigrationsBase.ts new file mode 100644 index 00000000..d29a53fa --- /dev/null +++ b/commands/MigrationsBase.ts @@ -0,0 +1,109 @@ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +import logUpdate from 'log-update' +import { BaseCommand } from '@adonisjs/ace' +import { MigratedFileNode, MigratorContract } from '@ioc:Adonis/Lucid/Migrator' + +/** + * Base class to execute migrations and print logs + */ +export default abstract class MigrationsBase extends BaseCommand { + /** + * Returns beautified log message string + */ + protected $getLogMessage (file: MigratedFileNode): string { + const message = `${file.migration.name} ${this.colors.gray(`(batch: ${file.batch})`)}` + + if (file.status === 'pending') { + return `${this.colors.yellow('pending')} ${message}` + } + + const lines: string[] = [] + + if (file.status === 'completed') { + lines.push(`${this.colors.green('completed')} ${message}`) + } else { + lines.push(`${this.colors.red('error')} ${message}`) + } + + if (file.queries.length) { + lines.push(' START QUERIES') + lines.push(' ================') + file.queries.forEach((query) => lines.push(` ${query}`)) + lines.push(' ================') + lines.push(' END QUERIES') + } + + return lines.join('\n') + } + + /** + * Runs the migrations using the migrator + */ + protected async $runMigrations (migrator: MigratorContract) { + /** + * A set of files processed and emitted using event emitter. + */ + const processedFiles: Set = new Set() + + /** + * Starting to process a new migration file + */ + migrator.on('migration:start', (file) => { + processedFiles.add(file.migration.name) + logUpdate(this.$getLogMessage(file)) + }) + + /** + * Migration completed + */ + migrator.on('migration:completed', (file) => { + logUpdate(this.$getLogMessage(file)) + logUpdate.done() + }) + + /** + * Migration error + */ + migrator.on('migration:error', (file) => { + logUpdate(this.$getLogMessage(file)) + logUpdate.done() + }) + + /** + * Run and close db connection + */ + await migrator.run() + await migrator.close() + + /** + * Log all pending files. This will happen, when one of the migration + * fails with an error and then the migrator stops emitting events. + */ + Object.keys(migrator.migratedFiles).forEach((file) => { + if (!processedFiles.has(file)) { + console.log(this.$getLogMessage(migrator.migratedFiles[file])) + } + }) + + /** + * Log final status + */ + switch (migrator.status) { + case 'skipped': + const message = migrator.direction === 'up' ? 'Already upto date' : 'Already at latest batch' + console.log(this.colors.cyan(message)) + break + case 'error': + this.logger.fatal(migrator.error!) + break + } + } +} diff --git a/commands/Rollback.ts b/commands/Rollback.ts new file mode 100644 index 00000000..27f4f3c1 --- /dev/null +++ b/commands/Rollback.ts @@ -0,0 +1,79 @@ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +import { flags } from '@adonisjs/ace' +import { inject } from '@adonisjs/fold' +import { DatabaseContract } from '@ioc:Adonis/Lucid/Database' +import { ApplicationContract } from '@ioc:Adonis/Core/Application' + +import MigrationsBase from './MigrationsBase' + +/** + * The command is meant to migrate the database by execute migrations + * in `up` direction. + */ +@inject([null, 'Adonis/Lucid/Database']) +export default class Migrate extends MigrationsBase { + public static commandName = 'migration:rollback' + public static description = 'Rollback migrations to a given batch number' + + @flags.string({ description: 'Define a custom database connection' }) + public connection: string + + @flags.boolean({ description: 'Print SQL queries, instead of running the migrations' }) + public dryRun: boolean + + @flags.number({ + description: 'Define custom batch number for rollback. Use 0 to rollback to initial state', + }) + public batch: number + + /** + * This command loads the application, since we need the runtime + * to find the migration directories for a given connection + */ + public static settings = { + loadApp: true, + } + + constructor (app: ApplicationContract, private _db: DatabaseContract) { + super(app) + } + + /** + * Handle command + */ + public async handle () { + const connection = this._db.getRawConnection(this.connection || this._db.primaryConnectionName) + + /** + * Ensure the define connection name does exists in the + * config file + */ + if (!connection) { + this.logger.error( + `${this.connection} is not a valid connection name. Double check config/database file`, + ) + return + } + + /** + * New up migrator + */ + const { Migrator } = await import('../src/Migrator') + const migrator = new Migrator(this._db, this.application, { + direction: 'down', + batch: this.batch, + connectionName: this.connection, + dryRun: this.dryRun, + }) + + await this.$runMigrations(migrator) + } +} diff --git a/src/Migrator/index.ts b/src/Migrator/index.ts index d3ffbe9c..19162bd3 100644 --- a/src/Migrator/index.ts +++ b/src/Migrator/index.ts @@ -297,7 +297,7 @@ export class Migrator extends EventEmitter implements MigratorContract { * Returns an array of files migrated till now. The latest * migrations are on top */ - private async _getMigratedFilesTillBatch (batch) { + private async _getMigratedFilesTillBatch (batch: number) { return this._client .query<{ name: string, batch: number }[]>() .from(this._migrationsConfig.tableName) @@ -355,7 +355,8 @@ export class Migrator extends EventEmitter implements MigratorContract { /** * Migrate down (aka rollback) */ - private async _runDown (batch: number) { + private async _runDown (batch?: number) { + batch = batch || await this._getLatestBatch() const existing = await this._getMigratedFilesTillBatch(batch) const collected = await this._migrationSource.getMigrations()