Skip to content

Commit

Permalink
feat: add migrate command
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Nov 26, 2019
1 parent b3af184 commit 1cb0491
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 17 deletions.
19 changes: 11 additions & 8 deletions adonis-typings/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ declare module '@ioc:Adonis/Lucid/Migrator' {
dryRun?: boolean,
}

/**
* Shape of migrated file within migrator
*/
export type MigratedFileNode = {
status: 'completed' | 'error' | 'pending',
queries: string[],
migration: MigrationNode,
batch: number,
}

/**
* Shape of the migrator
*/
Expand All @@ -42,14 +52,7 @@ declare module '@ioc:Adonis/Lucid/Migrator' {
direction: 'up' | 'down'
status: 'completed' | 'skipped' | 'pending' | 'error'
error: null | Error
migratedFiles: {
[file: string]: {
status: 'completed' | 'error' | 'pending',
queries: string[],
migration: MigrationNode,
batch: number,
},
}
migratedFiles: { [file: string]: MigratedFileNode }
run (): Promise<void>
getList (): Promise<{ batch: number, name: string, migration_time: Date }[]>
close (): Promise<void>
Expand Down
157 changes: 157 additions & 0 deletions commands/Migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* 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 { 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'

/**
* 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 {
public static commandName = 'migration:run'
public static description = 'Run pending migrations'

@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

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

/**
* 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
*/
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
}

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

/**
* New up migrator
*/
const { Migrator } = await import('../src/Migrator')
const migrator = new Migrator(this._db, this.application, {
direction: 'up',
connectionName: this.connection,
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
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"camelcase": "^5.3.1",
"knex": "^0.20.2",
"knex-dynamic-connection": "^1.0.2",
"log-update": "^3.3.0",
"pluralize": "^8.0.0",
"snake-case": "^2.1.0",
"ts-essentials": "^3.0.5"
Expand Down
34 changes: 25 additions & 9 deletions src/Migrator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@

/// <reference path="../../adonis-typings/index.ts" />

import { EventEmitter } from 'events'
import { Exception } from '@poppinss/utils'
import { ApplicationContract } from '@ioc:Adonis/Core/Application'

import {
MigrationNode,
MigratorOptions,
MigratedFileNode,
MigratorContract,
} from '@ioc:Adonis/Lucid/Migrator'

Expand All @@ -30,7 +32,7 @@ import { MigrationSource } from './MigrationSource'
* Migrator exposes the API to execute migrations using the schema files
* for a given connection at a time.
*/
export class Migrator implements MigratorContract {
export class Migrator extends EventEmitter implements MigratorContract {
private _client = this._db.connection(this._options.connectionName || this._db.primaryConnectionName)
private _config = this._db.getRawConnection(this._client.connectionName)!.config

Expand Down Expand Up @@ -69,14 +71,7 @@ export class Migrator implements MigratorContract {
* An array of files we have successfully migrated. The files are
* collected regardless of `up` or `down` methods
*/
public migratedFiles: {
[file: string]: {
status: 'completed' | 'error' | 'pending',
queries: string[],
migration: MigrationNode,
batch: number,
},
} = {}
public migratedFiles: { [file: string]: MigratedFileNode } = {}

/**
* Last error occurred when executing migrations
Expand All @@ -101,6 +96,7 @@ export class Migrator implements MigratorContract {
private _app: ApplicationContract,
private _options: MigratorOptions,
) {
super()
}

/**
Expand Down Expand Up @@ -190,6 +186,7 @@ export class Migrator implements MigratorContract {

try {
const schema = new migration.source(client, migration.name, this.dryRun)
this.emit('migration:start', this.migratedFiles[migration.name])

if (this.direction === 'up') {
const response = await schema.execUp() // Handles dry run itself
Expand All @@ -201,8 +198,12 @@ export class Migrator implements MigratorContract {

await this._commit(client)
this.migratedFiles[migration.name].status = 'completed'
this.emit('migration:completed', this.migratedFiles[migration.name])
} catch (error) {
this.error = error
this.migratedFiles[migration.name].status = 'error'
this.emit('migration:error', this.migratedFiles[migration.name])

await this._rollback(client)
throw error
}
Expand All @@ -227,6 +228,7 @@ export class Migrator implements MigratorContract {
if (!acquired) {
throw new Exception('Unable to acquire lock. Concurrent migrations are not allowed')
}
this.emit('acquire:lock')
}

/**
Expand All @@ -242,6 +244,7 @@ export class Migrator implements MigratorContract {
if (!released) {
throw new Exception('Migration completed, but unable to release database lock')
}
this.emit('release:lock')
}

/**
Expand All @@ -257,6 +260,7 @@ export class Migrator implements MigratorContract {
return
}

this.emit('create:schema:table')
await client.schema.createTable(this._migrationsConfig.tableName, (table) => {
table.increments().notNullable()
table.string('name').notNullable()
Expand Down Expand Up @@ -307,6 +311,7 @@ export class Migrator implements MigratorContract {
* work regardless of dryRun is enabled or not.
*/
private async _boot () {
this.emit('start')
this._booted = true
await this._acquireLock()
await this._makeMigrationsTable()
Expand Down Expand Up @@ -382,6 +387,17 @@ export class Migrator implements MigratorContract {
}
}

public on (event: 'start', callback: () => void): this
public on (event: 'acquire:lock', callback: () => void): this
public on (event: 'release:lock', callback: () => void): this
public on (event: 'create:schema:table', callback: () => void): this
public on (event: 'migration:start', callback: (file: MigratedFileNode) => void): this
public on (event: 'migration:completed', callback: (file: MigratedFileNode) => void): this
public on (event: 'migration:error', callback: (file: MigratedFileNode) => void): this
public on (event: string, callback: (...args: any[]) => void): this {
return super.on(event, callback)
}

/**
* Returns list of migrated files, along with their
* batch
Expand Down
Binary file removed test-helpers/tmp/db.sqlite
Binary file not shown.

0 comments on commit 1cb0491

Please sign in to comment.