-
-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(migrations,schema): implement schema and migrations
used migrations from last version and improved upon them to support multi tenant migrations
- Loading branch information
1 parent
6892543
commit cc3aac4
Showing
6 changed files
with
914 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,355 @@ | ||
'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 CatLog = require('cat-log') | ||
const cf = require('co-functional') | ||
const logger = new CatLog('adonis:lucid') | ||
|
||
class Runner { | ||
|
||
constructor (Database, Config) { | ||
this.database = Database | ||
this.migrationsTable = Config.get('database.migrationsTable', 'adonis_schema') | ||
this.lockTable = `${this.migrationsTable}_lock` | ||
this.migrations = [] | ||
} | ||
|
||
/** | ||
* makes a new lock table if does not exists already | ||
* | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
_makeLockTable () { | ||
return this.database.schema | ||
.createTableIfNotExists(this.lockTable, function (table) { | ||
table.increments('id') | ||
table.boolean('is_locked') | ||
}) | ||
} | ||
|
||
/** | ||
* adds a lock on migrations | ||
* | ||
* @private | ||
*/ | ||
_addLock () { | ||
return this.database.insert({is_locked: 1}).into(this.lockTable) | ||
} | ||
|
||
/** | ||
* checks whether there is a lock on | ||
* migrations table or not. | ||
* | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
* _checkLock () { | ||
const result = yield this.database | ||
.from(this.lockTable) | ||
.where('is_locked', 1) | ||
.orderBy('id', 'desc') | ||
.limit(1) | ||
|
||
if (_.size(result)) { | ||
throw new Error(`Migrations are locked, make sure you are not running multiple migration process or delete ${this.lockTable} table from database`) | ||
} | ||
return false | ||
} | ||
|
||
/** | ||
* removes migrations lock by drop the | ||
* lock table | ||
* | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
_freeLock () { | ||
return this.database.schema.dropTable(this.lockTable) | ||
} | ||
|
||
/** | ||
* creates migrations table if it does not exists. | ||
* creating table needs to be first step | ||
* | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
_makeMigrationsTable () { | ||
return this.database.schema.createTableIfNotExists(this.migrationsTable, function (table) { | ||
table.increments('id') | ||
table.string('name') | ||
table.integer('batch') | ||
table.timestamp('migration_time') | ||
}) | ||
} | ||
|
||
/** | ||
* returns difference of migrations to be | ||
* used for creation or rollback. | ||
* | ||
* @param {Array} files | ||
* @param {Array} values | ||
* @param {String} direction | ||
* @return {Array} | ||
* | ||
* @private | ||
*/ | ||
_getDifference (files, values, direction) { | ||
let difference = _.difference(_.keys(files), values) | ||
if (direction === 'down') { | ||
difference = _.reverse(_.intersection(values, _.keys(files))) | ||
} | ||
_.each(difference, (file) => { | ||
if (typeof (files[file]) !== 'function') { | ||
throw new Error(`Make sure you are exporting a class from ${file}`) | ||
} | ||
this._migrateClass(new files[file](), direction) | ||
}) | ||
return difference | ||
} | ||
|
||
/** | ||
* this query sets dynamic where clause to select the | ||
* given batch | ||
* | ||
* @param {Object} builder | ||
* @param {Number} batch | ||
* | ||
* @private | ||
*/ | ||
_dynamicBatchQuery (builder, subquery, batch) { | ||
if (batch === 0 || batch) { | ||
builder.where('batch', '>', batch) | ||
} else { | ||
builder.where('batch', subquery) | ||
} | ||
} | ||
|
||
/** | ||
* returns difference of files to be used for migrations | ||
* also auto sets migrations on runner instance | ||
* | ||
* @param {Array} files | ||
* @param {Sring} direction | ||
* @param {Number} batch | ||
* @return {Array} | ||
* | ||
* @private | ||
*/ | ||
* _diff (files, direction, batch) { | ||
/** | ||
* pull all migrations from migrations table when direction | ||
* is up | ||
*/ | ||
let query = this.database.select('name').from(this.migrationsTable).pluck('name') | ||
|
||
if (direction === 'down') { | ||
const subquery = this.database.from(this.migrationsTable).max('batch as batch') | ||
query = this.database | ||
.select('name') | ||
.from(this.migrationsTable) | ||
.modify(this._dynamicBatchQuery, subquery, batch) | ||
.pluck('name') | ||
} | ||
yield this._makeMigrationsTable() | ||
const values = yield query | ||
const result = this._getDifference(files, values, direction) | ||
return result | ||
} | ||
|
||
/** | ||
* decorates schema callback table object by adding | ||
* new methods on it. | ||
* | ||
* @param {Object} table | ||
* | ||
* @private | ||
*/ | ||
_decorateTable (table) { | ||
table.softDeletes = function () { | ||
return table.timestamp('deleted_at').nullable() | ||
} | ||
table.nullableTimestamps = function () { | ||
return table.timestamps().nullable() | ||
} | ||
} | ||
|
||
/** | ||
* migrates a class by calling up or down | ||
* method on it. | ||
* | ||
* @param {Object} schema | ||
* @param {String} method | ||
* @return {void} | ||
* | ||
* @private | ||
*/ | ||
_migrateClass (migrationClass, method) { | ||
const self = this | ||
migrationClass.fn = this.database.fn | ||
if (!migrationClass[method]) { | ||
logger.warn('skipping migration as %s is not defined', method) | ||
return | ||
} | ||
migrationClass[method]() | ||
_.each(migrationClass.store, (schemas, name) => { | ||
const connection = this.database.connection(migrationClass.constructor.connection) | ||
_.each(schemas, (defination) => { | ||
const migration = connection.schema[name](defination.key, function (table) { | ||
self._decorateTable(table) | ||
defination.callback(table) | ||
}) | ||
this.migrations.push(migration) | ||
}) | ||
}) | ||
} | ||
|
||
/** | ||
* returns lastest batch inside migrations | ||
* table | ||
* | ||
* @return {Number} | ||
* | ||
* @private | ||
*/ | ||
* _getLatestBatch () { | ||
const result = yield this.database | ||
.table(this.migrationsTable) | ||
.max('batch as max_batch') | ||
|
||
const batchNumber = result[0].max_batch || 0 | ||
return batchNumber | ||
} | ||
|
||
/** | ||
* updates batch with all newly created | ||
* migrations | ||
* | ||
* @param {Array} difference | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
* _updateBatch (difference) { | ||
let batchNumber = yield this._getLatestBatch() | ||
batchNumber++ | ||
difference = _.map(difference, function (migration) { | ||
return {name: migration, batch: batchNumber, migration_time: new Date()} | ||
}) | ||
return yield this.database | ||
.table(this.migrationsTable) | ||
.insert(difference) | ||
} | ||
|
||
/** | ||
* deletes an entire batch from migrations table, required | ||
* when rolling back | ||
* | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
* _deleteBatch () { | ||
const batchNumber = yield this._getLatestBatch() | ||
return yield this.database | ||
.table(this.migrationsTable) | ||
.where('batch', batchNumber) | ||
.delete() | ||
} | ||
|
||
/** | ||
* sets up a queue of migrations to be run | ||
* in a sequence | ||
* | ||
* @method _waterFallMigrations | ||
* @return {Array} | ||
* | ||
* @private | ||
*/ | ||
* _waterFallMigrations () { | ||
return cf.forEachSerial(function * (item) { | ||
return yield item | ||
}, this.migrations) | ||
} | ||
|
||
/** | ||
* runs all migrations in a given direction | ||
* | ||
* @param {Array} difference | ||
* @param {String} direction | ||
* @return {Object} | ||
* | ||
* @private | ||
*/ | ||
* _run (difference, direction) { | ||
yield this._makeLockTable() | ||
yield this._checkLock() | ||
yield this._addLock() | ||
try { | ||
yield this._waterFallMigrations() | ||
if (direction === 'down') { | ||
yield this._deleteBatch() | ||
} else { | ||
yield this._updateBatch(difference) | ||
} | ||
yield this._freeLock() | ||
} catch (e) { | ||
yield this._freeLock() | ||
throw e | ||
} | ||
} | ||
|
||
/** | ||
* runs up on all new migrations | ||
* | ||
* @param {Array} files | ||
* @return {Object} | ||
* | ||
* @public | ||
*/ | ||
* up (files) { | ||
const migrate = yield this._diff(files, 'up') | ||
let status = 'skipped' | ||
if (_.size(migrate) > 0) { | ||
yield this._run(migrate, 'up') | ||
status = 'completed' | ||
} | ||
this.database.close() | ||
return {migrated: migrate, status} | ||
} | ||
|
||
/** | ||
* runs down on all latest batch migrations | ||
* | ||
* @param {Array} files | ||
* @return {Object} | ||
* | ||
* @public | ||
*/ | ||
* down (files, batch) { | ||
const migrate = yield this._diff(files, 'down', batch) | ||
let status = 'skipped' | ||
if (_.size(migrate) > 0) { | ||
yield this._run(migrate, 'down') | ||
status = 'completed' | ||
} | ||
this.database.close() | ||
return {migrated: migrate, status} | ||
} | ||
} | ||
|
||
module.exports = Runner |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
'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 proxy = require('./proxy') | ||
require('harmony-reflect') | ||
|
||
class Schema { | ||
constructor () { | ||
this.store = {} | ||
return new Proxy(this, proxy) | ||
} | ||
|
||
static get connection () { | ||
return 'default' | ||
} | ||
} | ||
|
||
module.exports = Schema |
Oops, something went wrong.