Skip to content

Commit

Permalink
feat: add rollback command
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Nov 26, 2019
1 parent 1cb0491 commit 8d1c946
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 93 deletions.
12 changes: 10 additions & 2 deletions adonis-typings/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +30,7 @@ declare module '@ioc:Adonis/Lucid/Migrator' {
dryRun?: boolean,
} | {
direction: 'down',
batch: number,
batch?: number,
connectionName?: string,
dryRun?: boolean,
}
Expand All @@ -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'
Expand All @@ -56,5 +57,12 @@ declare module '@ioc:Adonis/Lucid/Migrator' {
run (): Promise<void>
getList (): Promise<{ batch: number, name: string, migration_time: Date }[]>
close (): Promise<void>
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
}
}
94 changes: 5 additions & 89 deletions commands/Migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
*/
Expand All @@ -87,11 +58,6 @@ export default class Migrate extends BaseCommand {
return
}

/**
* A set of files processed and emitted using event emitter.
*/
const processedFiles: Set<string> = new Set()

/**
* New up migrator
*/
Expand All @@ -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)
}
}
109 changes: 109 additions & 0 deletions commands/MigrationsBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <[email protected]>
*
* 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<string> = 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
}
}
}
79 changes: 79 additions & 0 deletions commands/Rollback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <[email protected]>
*
* 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)
}
}
5 changes: 3 additions & 2 deletions src/Migrator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down

0 comments on commit 8d1c946

Please sign in to comment.