@adonisjs/lucid
Version:
- [x] Paginate method - [x] forPage method - [ ] chunk ( removed ) - [ ] pluckAll ( removed ) - [x] withPrefix - [x] transactions - [x] global transactions
373 lines (340 loc) • 8.11 kB
JavaScript
'use strict'
/*
* adonis-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.
*/
const _ = require('lodash')
const CE = require('../Exceptions')
/**
* Migration class is used to migrate the database by
* calling actions defined inside schema class.
*
* @binding Adonis/Src/Migration
* @singleton
* @alias Migration
* @group Database
* @uses (['Adonis/Src/Config', 'Adonis/Src/Database'])
*
* @class Migration
* @constructor
*/
class Migration {
constructor (Config, Database) {
this.db = Database
this._migrationsTable = Config.get('database.migrationsTable', 'adonis_schema')
this._lockTable = `${this._migrationsTable}_lock`
}
/**
* Makes the migrations table only if doesn't exists
*
* @method _makeMigrationsTable
* @async
*
* @return {void}
*
* @private
*/
_makeMigrationsTable () {
return this.db.schema.createTableIfNotExists(this._migrationsTable, (table) => {
table.increments()
table.string('name')
table.integer('batch')
table.timestamp('migration_time').defaultsTo(this.db.fn.now())
})
}
/**
* Creates the lock table if it doesn't exists
*
* @method _makeLockTable
* @async
*
* @return {void}
*
* @private
*/
_makeLockTable () {
return this.db.schema.createTableIfNotExists(this._lockTable, (table) => {
table.increments()
table.boolean('is_locked')
})
}
/**
* Adds lock to make sure only one migration
* process runs at a time
*
* @method _addLock
* @async
*
* @private
*/
_addLock () {
return this.db.insert({ is_locked: true }).into(this._lockTable)
}
/**
* Remove the added lock
*
* @method _removeLock
* @async
*
* @return {void}
*
* @private
*/
_removeLock () {
return this.db.schema.dropTableIfExists(this._lockTable)
}
/**
* Checks for lock and throws exception if
* lock exists
*
* @method _checkForLock
* @async
*
* @return {void}
*
* @private
*/
async _checkForLock () {
const hasLock = await this.db
.from(this._lockTable)
.where('is_locked', 1)
.orderBy('id', 'desc')
.first()
if (hasLock) {
throw CE.RuntimeException.migrationsAreLocked(this._lockTable)
}
}
/**
* Returns the latest batch number. Any inserts
* should be done by incrementing the batch
* number
*
* @method _getLatestBatch
* @async
*
* @return {Number}
*
* @private
*/
async _getLatestBatch () {
const batch = await this.db.table(this._migrationsTable).max('batch as batch')
return Number(_.get(batch, '0.batch', 0))
}
/**
* Add a new row to the migrations table for
* a given batch
*
* @method _addForBatch
* @async
*
* @param {String} name
* @param {Number} batch
*
* @return {void}
*
* @private
*/
_addForBatch (name, batch) {
return this.db.table(this._migrationsTable).insert({name, batch})
}
/**
* Remove row for a given schema defination from
* the migrations table
*
* @method _remove
* @async
*
* @param {String} name
*
* @return {void}
*
* @private
*/
_remove (name) {
return this.db.table(this._migrationsTable).where('name', name).delete()
}
/**
* Get all the schema definations after a batch
* number.
*
* Note: This method will return all the rows when
* batch is defined as zero
*
* @method _getAfterBatch
* @async
*
* @param {Number} [batch = 0]
*
* @return {Array}
*
* @private
*/
_getAfterBatch (batch = 0) {
const query = this.db.table(this._migrationsTable)
if (batch > 0) {
query.where('batch', '>', batch)
}
return query.orderBy('name').pluck('name')
}
/**
* Returns an array of schema names to be executed for
* a given action.
*
* @method _getDiff
*
* @param {Array} names - Name of all schemas
* @param {String} direction - The direction for the migration
* @param {String} [batch]
*
* @return {Array}
*
* @private
*/
async _getDiff (names, direction = 'up', batch) {
const schemas = direction === 'down'
? await this._getAfterBatch(batch)
: await this.db.table(this._migrationsTable).pluck('name')
return direction === 'down' ? _.reverse(_.intersection(names, schemas)) : _.difference(names, schemas)
}
/**
* Execute all schemas one by one in sequence
*
* @method _execute
*
* @param {Object} Schemas
* @param {String} direction
*
* @return {void}
*
* @private
*/
async _execute (schemas, direction, batch, toSQL) {
for (let schema in schemas) {
const schemaInstance = new schemas[schema](this.db)
await schemaInstance[direction](toSQL)
await schemaInstance.executeActions()
direction === 'up' ? await this._addForBatch(schema, batch) : await this._remove(schema)
}
}
/**
* Clean up state by removing lock and
* close the db connection
*
* @method _cleanup
*
* @return {void}
*/
async _cleanup () {
await this._removeLock()
this.db.close()
}
/**
* Migrate the database using defined schema
*
* @method up
*
* @param {Object} schemas
*
* @return {Object}
*
* @throws {Error} If any of schema file throws exception
*/
async up (schemas, toSQL) {
await this._makeMigrationsTable()
await this._makeLockTable()
await this._checkForLock()
await this._addLock()
const diff = await this._getDiff(_.keys(schemas), 'up')
/**
* Can't do much since all is good
*/
if (!_.size(diff)) {
await this._cleanup()
return { migrated: [], status: 'skipped' }
}
/**
* Getting a list of filtered schemas in the correct order
*/
const filteredSchemas = _.transform(diff, (result, name) => {
result[name] = schemas[name]
return name
}, {})
try {
const batch = await this._getLatestBatch()
await this._execute(filteredSchemas, 'up', batch + 1, toSQL)
await this._cleanup()
return { migrated: diff, status: 'completed' }
} catch (error) {
await this._cleanup()
throw error
}
}
/**
* Rollback migrations to a given batch, latest
* batch or the way upto to first batch.
*
* @method down
*
* @param {Object} schemas
* @param {Number} batch
* @param {Boolean} toSQL
*
* @return {Object}
*
* @throws {Error} If something blows in schema file
*/
async down (schemas, batch, toSQL = false) {
await this._makeMigrationsTable()
await this._makeLockTable()
await this._checkForLock()
await this._addLock()
if (batch === null || typeof (batch) === 'undefined') {
batch = await this._getLatestBatch()
batch = batch - 1
}
const diff = await this._getDiff(_.keys(schemas), 'down', batch)
/**
* Can't do much since all is good
*/
if (!_.size(diff)) {
await this._cleanup()
return { migrated: [], status: 'skipped' }
}
/**
* Getting a list of filtered schemas in the correct order
*/
const filteredSchemas = _.transform(diff, (result, name) => {
result[name] = schemas[name]
return name
}, {})
try {
await this._execute(filteredSchemas, 'down', batch, toSQL)
await this._cleanup()
return { migrated: diff, status: 'completed' }
} catch (error) {
await this._cleanup()
throw error
}
}
/**
* Returns the status of all the schemas
*
* @method status
*
* @param {Object} schemas
*
* @return {Object}
*/
async status (schemas) {
const migratedFiles = await this._getAfterBatch(0)
return _.transform(schemas, (result, schema, name) => {
result[name] = _.includes(migratedFiles, name) ? 'Y' : 'N'
return result
}, {})
}
}
module.exports = Migration