mirror of
https://github.com/knex/knex.git
synced 2026-01-05 11:38:53 +00:00
Merge branch 'master' of github.com:tgriesser/knex
This commit is contained in:
commit
03f4993642
2
.jshintignore
Normal file
2
.jshintignore
Normal file
@ -0,0 +1,2 @@
|
||||
test/coverage/.
|
||||
test/integration/migrate/migration/.
|
||||
@ -10,5 +10,6 @@
|
||||
"undef": true,
|
||||
"trailing": true,
|
||||
"unused": true,
|
||||
"esnext": true,
|
||||
"predef": [ "-Promise", "before", "after", "beforeEach", "afterEach" ]
|
||||
}
|
||||
|
||||
4554
build/knex.js
4554
build/knex.js
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,11 @@
|
||||
// -------
|
||||
"use strict";
|
||||
|
||||
exports.__esModule = true;
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
|
||||
|
||||
@ -13,6 +17,13 @@ var mkdirp = require('mkdirp');
|
||||
var Promise = require('../promise');
|
||||
var helpers = require('../helpers');
|
||||
var assign = require('lodash/object/assign');
|
||||
var inherits = require('inherits');
|
||||
|
||||
function LockError(msg) {
|
||||
this.name = 'MigrationLocked';
|
||||
this.message = msg;
|
||||
}
|
||||
inherits(LockError, Error);
|
||||
|
||||
// The new migration we're performing, typically called from the `knex.migrate`
|
||||
// interface on the main `knex` object. Passes the `knex` instance performing
|
||||
@ -30,270 +41,381 @@ var Migrator = (function () {
|
||||
|
||||
// Migrators to the latest configuration.
|
||||
|
||||
Migrator.prototype.latest = function latest(config) {
|
||||
var _this = this;
|
||||
_createClass(Migrator, [{
|
||||
key: 'latest',
|
||||
value: function latest(config) {
|
||||
var _this = this;
|
||||
|
||||
this.config = this.setConfig(config);
|
||||
return this._migrationData().tap(validateMigrationList).spread(function (all, completed) {
|
||||
return _this._runBatch(_.difference(all, completed), 'up');
|
||||
});
|
||||
};
|
||||
|
||||
// Rollback the last "batch" of migrations that were run.
|
||||
|
||||
Migrator.prototype.rollback = function rollback(config) {
|
||||
var _this2 = this;
|
||||
|
||||
return Promise['try'](function () {
|
||||
_this2.config = _this2.setConfig(config);
|
||||
return _this2._migrationData().tap(validateMigrationList).then(function (val) {
|
||||
return _this2._getLastBatch(val);
|
||||
}).then(function (migrations) {
|
||||
return _this2._runBatch(_.pluck(migrations, 'name'), 'down');
|
||||
this.config = this.setConfig(config);
|
||||
return this._migrationData().tap(validateMigrationList).spread(function (all, completed) {
|
||||
return _this._runBatch(_.difference(all, completed), 'up');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Migrator.prototype.status = function status(config) {
|
||||
this.config = this.setConfig(config);
|
||||
|
||||
return Promise.all([this.knex(this.config.tableName).select('*'), this._listAll()]).spread(function (db, code) {
|
||||
return db.length - code.length;
|
||||
});
|
||||
};
|
||||
|
||||
// Retrieves and returns the current migration version
|
||||
// we're on, as a promise. If there aren't any migrations run yet,
|
||||
// return "none" as the value for the `currentVersion`.
|
||||
|
||||
Migrator.prototype.currentVersion = function currentVersion(config) {
|
||||
this.config = this.setConfig(config);
|
||||
return this._listCompleted(config).then(function (completed) {
|
||||
var val = _.chain(completed).map(function (value) {
|
||||
return value.split('_')[0];
|
||||
}).max().value();
|
||||
return val === -Infinity ? 'none' : val;
|
||||
});
|
||||
};
|
||||
|
||||
// Creates a new migration, with a given name.
|
||||
|
||||
Migrator.prototype.make = function make(name, config) {
|
||||
var _this3 = this;
|
||||
|
||||
this.config = this.setConfig(config);
|
||||
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
|
||||
return this._ensureFolder(config).then(function (val) {
|
||||
return _this3._generateStubTemplate(val);
|
||||
}).then(function (val) {
|
||||
return _this3._writeNewMigration(name, val);
|
||||
});
|
||||
};
|
||||
|
||||
// Lists all available migration versions, as a sorted array.
|
||||
|
||||
Migrator.prototype._listAll = function _listAll(config) {
|
||||
this.config = this.setConfig(config);
|
||||
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir()).then(function (migrations) {
|
||||
return _.filter(migrations, function (value) {
|
||||
var extension = path.extname(value);
|
||||
return _.contains(['.co', '.coffee', '.eg', '.iced', '.js', '.litcoffee', '.ls'], extension);
|
||||
}).sort();
|
||||
});
|
||||
};
|
||||
|
||||
// Ensures a folder for the migrations exist, dependent on the
|
||||
// migration config settings.
|
||||
|
||||
Migrator.prototype._ensureFolder = function _ensureFolder() {
|
||||
var dir = this._absoluteConfigDir();
|
||||
return Promise.promisify(fs.stat, fs)(dir)['catch'](function () {
|
||||
return Promise.promisify(mkdirp)(dir);
|
||||
});
|
||||
};
|
||||
|
||||
// Ensures that the proper table has been created,
|
||||
// dependent on the migration config settings.
|
||||
|
||||
Migrator.prototype._ensureTable = function _ensureTable() {
|
||||
var _this4 = this;
|
||||
|
||||
var table = this.config.tableName;
|
||||
return this.knex.schema.hasTable(table).then(function (exists) {
|
||||
if (!exists) return _this4._createMigrationTable(table);
|
||||
});
|
||||
};
|
||||
|
||||
// Create the migration table, if it doesn't already exist.
|
||||
|
||||
Migrator.prototype._createMigrationTable = function _createMigrationTable(tableName) {
|
||||
return this.knex.schema.createTable(tableName, function (t) {
|
||||
t.increments();
|
||||
t.string('name');
|
||||
t.integer('batch');
|
||||
t.timestamp('migration_time');
|
||||
});
|
||||
};
|
||||
|
||||
// Run a batch of current migrations, in sequence.
|
||||
|
||||
Migrator.prototype._runBatch = function _runBatch(migrations, direction) {
|
||||
var _this5 = this;
|
||||
|
||||
return Promise.all(_.map(migrations, this._validateMigrationStructure, this)).then(function () {
|
||||
return _this5._latestBatchNumber();
|
||||
}).then(function (batchNo) {
|
||||
if (direction === 'up') batchNo++;
|
||||
return batchNo;
|
||||
}).then(function (batchNo) {
|
||||
return _this5._waterfallBatch(batchNo, migrations, direction);
|
||||
})['catch'](function (error) {
|
||||
helpers.warn('migrations failed with error: ' + error.message);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// Validates some migrations by requiring and checking for an `up` and `down` function.
|
||||
|
||||
Migrator.prototype._validateMigrationStructure = function _validateMigrationStructure(name) {
|
||||
var migration = require(path.join(this._absoluteConfigDir(), name));
|
||||
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
|
||||
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
// Lists all migrations that have been completed for the current db, as an array.
|
||||
// Rollback the last "batch" of migrations that were run.
|
||||
}, {
|
||||
key: 'rollback',
|
||||
value: function rollback(config) {
|
||||
var _this2 = this;
|
||||
|
||||
Migrator.prototype._listCompleted = function _listCompleted() {
|
||||
var _this6 = this;
|
||||
return Promise['try'](function () {
|
||||
_this2.config = _this2.setConfig(config);
|
||||
return _this2._migrationData().tap(validateMigrationList).then(function (val) {
|
||||
return _this2._getLastBatch(val);
|
||||
}).then(function (migrations) {
|
||||
return _this2._runBatch(_.pluck(migrations, 'name'), 'down');
|
||||
});
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'status',
|
||||
value: function status(config) {
|
||||
this.config = this.setConfig(config);
|
||||
|
||||
var tableName = this.config.tableName;
|
||||
return this._ensureTable(tableName).then(function () {
|
||||
return _this6.knex(tableName).orderBy('id').select('name');
|
||||
}).then(function (migrations) {
|
||||
return _.pluck(migrations, 'name');
|
||||
});
|
||||
};
|
||||
return Promise.all([this.knex(this.config.tableName).select('*'), this._listAll()]).spread(function (db, code) {
|
||||
return db.length - code.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the migration list from the specified migration directory,
|
||||
// as well as the list of completed migrations to check what
|
||||
// should be run.
|
||||
// Retrieves and returns the current migration version
|
||||
// we're on, as a promise. If there aren't any migrations run yet,
|
||||
// return "none" as the value for the `currentVersion`.
|
||||
}, {
|
||||
key: 'currentVersion',
|
||||
value: function currentVersion(config) {
|
||||
this.config = this.setConfig(config);
|
||||
return this._listCompleted(config).then(function (completed) {
|
||||
var val = _.chain(completed).map(function (value) {
|
||||
return value.split('_')[0];
|
||||
}).max().value();
|
||||
return val === -Infinity ? 'none' : val;
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'forceFreeMigrationsLock',
|
||||
value: function forceFreeMigrationsLock(config) {
|
||||
var _this3 = this;
|
||||
|
||||
Migrator.prototype._migrationData = function _migrationData() {
|
||||
return Promise.all([this._listAll(), this._listCompleted()]);
|
||||
};
|
||||
this.config = this.setConfig(config);
|
||||
var lockTable = this._getLockTableName();
|
||||
return this.knex.schema.hasTable(lockTable).then(function (exist) {
|
||||
return exist && _this3._freeLock();
|
||||
});
|
||||
}
|
||||
|
||||
// Generates the stub template for the current migration, returning a compiled template.
|
||||
// Creates a new migration, with a given name.
|
||||
}, {
|
||||
key: 'make',
|
||||
value: function make(name, config) {
|
||||
var _this4 = this;
|
||||
|
||||
Migrator.prototype._generateStubTemplate = function _generateStubTemplate() {
|
||||
var stubPath = this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub');
|
||||
return Promise.promisify(fs.readFile, fs)(stubPath).then(function (stub) {
|
||||
return _.template(stub.toString(), null, { variable: 'd' });
|
||||
});
|
||||
};
|
||||
this.config = this.setConfig(config);
|
||||
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
|
||||
return this._ensureFolder(config).then(function (val) {
|
||||
return _this4._generateStubTemplate(val);
|
||||
}).then(function (val) {
|
||||
return _this4._writeNewMigration(name, val);
|
||||
});
|
||||
}
|
||||
|
||||
// Write a new migration to disk, using the config and generated filename,
|
||||
// passing any `variables` given in the config to the template.
|
||||
// Lists all available migration versions, as a sorted array.
|
||||
}, {
|
||||
key: '_listAll',
|
||||
value: function _listAll(config) {
|
||||
this.config = this.setConfig(config);
|
||||
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir()).then(function (migrations) {
|
||||
return _.filter(migrations, function (value) {
|
||||
var extension = path.extname(value);
|
||||
return _.contains(['.co', '.coffee', '.eg', '.iced', '.js', '.litcoffee', '.ls'], extension);
|
||||
}).sort();
|
||||
});
|
||||
}
|
||||
|
||||
Migrator.prototype._writeNewMigration = function _writeNewMigration(name, tmpl) {
|
||||
var config = this.config;
|
||||
var dir = this._absoluteConfigDir();
|
||||
if (name[0] === '-') name = name.slice(1);
|
||||
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
||||
return Promise.promisify(fs.writeFile, fs)(path.join(dir, filename), tmpl(config.variables || {}))['return'](path.join(dir, filename));
|
||||
};
|
||||
// Ensures a folder for the migrations exist, dependent on the
|
||||
// migration config settings.
|
||||
}, {
|
||||
key: '_ensureFolder',
|
||||
value: function _ensureFolder() {
|
||||
var dir = this._absoluteConfigDir();
|
||||
return Promise.promisify(fs.stat, fs)(dir)['catch'](function () {
|
||||
return Promise.promisify(mkdirp)(dir);
|
||||
});
|
||||
}
|
||||
|
||||
// Get the last batch of migrations, by name, ordered by insert id
|
||||
// in reverse order.
|
||||
// Ensures that the proper table has been created,
|
||||
// dependent on the migration config settings.
|
||||
}, {
|
||||
key: '_ensureTable',
|
||||
value: function _ensureTable() {
|
||||
var _this5 = this;
|
||||
|
||||
Migrator.prototype._getLastBatch = function _getLastBatch() {
|
||||
var tableName = this.config.tableName;
|
||||
return this.knex(tableName).where('batch', function (qb) {
|
||||
qb.max('batch').from(tableName);
|
||||
}).orderBy('id', 'desc');
|
||||
};
|
||||
|
||||
// Returns the latest batch number.
|
||||
|
||||
Migrator.prototype._latestBatchNumber = function _latestBatchNumber() {
|
||||
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
|
||||
return obj[0].max_batch || 0;
|
||||
});
|
||||
};
|
||||
|
||||
// If transaction conf for a single migration is defined, use that.
|
||||
// Otherwise, rely on the common config. This allows enabling/disabling
|
||||
// transaction for a single migration by will, regardless of the common
|
||||
// config.
|
||||
|
||||
Migrator.prototype._useTransaction = function _useTransaction(migration, allTransactionsDisabled) {
|
||||
var singleTransactionValue = _.get(migration, 'config.transaction');
|
||||
|
||||
return _.isBoolean(singleTransactionValue) ? singleTransactionValue : !allTransactionsDisabled;
|
||||
};
|
||||
|
||||
// Runs a batch of `migrations` in a specified `direction`,
|
||||
// saving the appropriate database information as the migrations are run.
|
||||
|
||||
Migrator.prototype._waterfallBatch = function _waterfallBatch(batchNo, migrations, direction) {
|
||||
var _this7 = this;
|
||||
|
||||
var knex = this.knex;
|
||||
var _config = this.config;
|
||||
var tableName = _config.tableName;
|
||||
var disableTransactions = _config.disableTransactions;
|
||||
|
||||
var directory = this._absoluteConfigDir();
|
||||
var current = Promise.bind({ failed: false, failedOn: 0 });
|
||||
var log = [];
|
||||
_.each(migrations, function (migration) {
|
||||
var name = migration;
|
||||
migration = require(directory + '/' + name);
|
||||
|
||||
// We're going to run each of the migrations in the current "up"
|
||||
current = current.then(function () {
|
||||
if (_this7._useTransaction(migration, disableTransactions)) {
|
||||
return _this7._transaction(migration, direction, name);
|
||||
}
|
||||
return warnPromise(migration[direction](knex, Promise), name);
|
||||
var table = this.config.tableName;
|
||||
var lockTable = this._getLockTableName();
|
||||
return this.knex.schema.hasTable(table).then(function (exists) {
|
||||
return !exists && _this5._createMigrationTable(table);
|
||||
}).then(function () {
|
||||
log.push(path.join(directory, name));
|
||||
if (direction === 'up') {
|
||||
return knex(tableName).insert({
|
||||
name: name,
|
||||
batch: batchNo,
|
||||
migration_time: new Date()
|
||||
});
|
||||
}
|
||||
if (direction === 'down') {
|
||||
return knex(tableName).where({ name: name }).del();
|
||||
}
|
||||
return _this5.knex.schema.hasTable(lockTable);
|
||||
}).then(function (exists) {
|
||||
return !exists && _this5._createMigrationLockTable(lockTable);
|
||||
}).then(function () {
|
||||
return _this5.knex(lockTable).select('*');
|
||||
}).then(function (data) {
|
||||
return !data.length && _this5.knex(lockTable).insert({ is_locked: 0 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return current.thenReturn([batchNo, log]);
|
||||
};
|
||||
|
||||
Migrator.prototype._transaction = function _transaction(migration, direction, name) {
|
||||
return this.knex.transaction(function (trx) {
|
||||
return warnPromise(migration[direction](trx, Promise), name, function () {
|
||||
trx.commit();
|
||||
// Create the migration table, if it doesn't already exist.
|
||||
}, {
|
||||
key: '_createMigrationTable',
|
||||
value: function _createMigrationTable(tableName) {
|
||||
return this.knex.schema.createTable(tableName, function (t) {
|
||||
t.increments();
|
||||
t.string('name');
|
||||
t.integer('batch');
|
||||
t.timestamp('migration_time');
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}, {
|
||||
key: '_createMigrationLockTable',
|
||||
value: function _createMigrationLockTable(tableName) {
|
||||
return this.knex.schema.createTable(tableName, function (t) {
|
||||
t.integer('is_locked');
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: '_getLockTableName',
|
||||
value: function _getLockTableName() {
|
||||
return this.config.tableName + '_lock';
|
||||
}
|
||||
}, {
|
||||
key: '_isLocked',
|
||||
value: function _isLocked(trx) {
|
||||
var tableName = this._getLockTableName();
|
||||
return this.knex(tableName).transacting(trx).forUpdate().select('*').then(function (data) {
|
||||
return data[0].is_locked;
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: '_lockMigrations',
|
||||
value: function _lockMigrations(trx) {
|
||||
var tableName = this._getLockTableName();
|
||||
return this.knex(tableName).transacting(trx).update({ is_locked: 1 });
|
||||
}
|
||||
}, {
|
||||
key: '_getLock',
|
||||
value: function _getLock() {
|
||||
var _this6 = this;
|
||||
|
||||
Migrator.prototype._absoluteConfigDir = function _absoluteConfigDir() {
|
||||
return path.resolve(process.cwd(), this.config.directory);
|
||||
};
|
||||
return this.knex.transaction(function (trx) {
|
||||
return _this6._isLocked(trx).then(function (isLocked) {
|
||||
if (isLocked) {
|
||||
throw new Error("Migration table is already locked");
|
||||
}
|
||||
}).then(function () {
|
||||
return _this6._lockMigrations(trx);
|
||||
});
|
||||
})['catch'](function (err) {
|
||||
throw new LockError(err.message);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: '_freeLock',
|
||||
value: function _freeLock() {
|
||||
var tableName = this._getLockTableName();
|
||||
return this.knex(tableName).update({ is_locked: 0 });
|
||||
}
|
||||
|
||||
Migrator.prototype.setConfig = function setConfig(config) {
|
||||
return assign({
|
||||
extension: 'js',
|
||||
tableName: 'knex_migrations',
|
||||
directory: './migrations'
|
||||
}, this.config || {}, config);
|
||||
};
|
||||
// Run a batch of current migrations, in sequence.
|
||||
}, {
|
||||
key: '_runBatch',
|
||||
value: function _runBatch(migrations, direction) {
|
||||
var _this7 = this;
|
||||
|
||||
return this._getLock().then(function () {
|
||||
return Promise.all(_.map(migrations, _this7._validateMigrationStructure, _this7));
|
||||
}).then(function () {
|
||||
return _this7._latestBatchNumber();
|
||||
}).then(function (batchNo) {
|
||||
if (direction === 'up') batchNo++;
|
||||
return batchNo;
|
||||
}).then(function (batchNo) {
|
||||
return _this7._waterfallBatch(batchNo, migrations, direction);
|
||||
}).then(function () {
|
||||
return _this7._freeLock();
|
||||
})['catch'](function (error) {
|
||||
var cleanupReady = Promise.resolve();
|
||||
|
||||
if (error instanceof LockError) {
|
||||
// if locking error do not free the lock
|
||||
helpers.warn('Cant take lock to run migrations: ' + error.message);
|
||||
helpers.warn('If you are sue migrations are not running you can release ' + 'lock manually by deleting all the rows from migrations lock table: ' + _this7._getLockTableName());
|
||||
} else {
|
||||
helpers.warn('migrations failed with error: ' + error.message);
|
||||
// If the error was not due to a locking issue, then
|
||||
// remove the lock.
|
||||
cleanupReady = _this7._freeLock();
|
||||
}
|
||||
|
||||
return cleanupReady['finally'](function () {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Validates some migrations by requiring and checking for an `up` and `down` function.
|
||||
}, {
|
||||
key: '_validateMigrationStructure',
|
||||
value: function _validateMigrationStructure(name) {
|
||||
var migration = require(path.join(this._absoluteConfigDir(), name));
|
||||
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
|
||||
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Lists all migrations that have been completed for the current db, as an array.
|
||||
}, {
|
||||
key: '_listCompleted',
|
||||
value: function _listCompleted() {
|
||||
var _this8 = this;
|
||||
|
||||
var tableName = this.config.tableName;
|
||||
return this._ensureTable(tableName).then(function () {
|
||||
return _this8.knex(tableName).orderBy('id').select('name');
|
||||
}).then(function (migrations) {
|
||||
return _.pluck(migrations, 'name');
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the migration list from the specified migration directory,
|
||||
// as well as the list of completed migrations to check what
|
||||
// should be run.
|
||||
}, {
|
||||
key: '_migrationData',
|
||||
value: function _migrationData() {
|
||||
return Promise.all([this._listAll(), this._listCompleted()]);
|
||||
}
|
||||
|
||||
// Generates the stub template for the current migration, returning a compiled template.
|
||||
}, {
|
||||
key: '_generateStubTemplate',
|
||||
value: function _generateStubTemplate() {
|
||||
var stubPath = this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub');
|
||||
return Promise.promisify(fs.readFile, fs)(stubPath).then(function (stub) {
|
||||
return _.template(stub.toString(), null, { variable: 'd' });
|
||||
});
|
||||
}
|
||||
|
||||
// Write a new migration to disk, using the config and generated filename,
|
||||
// passing any `variables` given in the config to the template.
|
||||
}, {
|
||||
key: '_writeNewMigration',
|
||||
value: function _writeNewMigration(name, tmpl) {
|
||||
var config = this.config;
|
||||
var dir = this._absoluteConfigDir();
|
||||
if (name[0] === '-') name = name.slice(1);
|
||||
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
||||
return Promise.promisify(fs.writeFile, fs)(path.join(dir, filename), tmpl(config.variables || {}))['return'](path.join(dir, filename));
|
||||
}
|
||||
|
||||
// Get the last batch of migrations, by name, ordered by insert id
|
||||
// in reverse order.
|
||||
}, {
|
||||
key: '_getLastBatch',
|
||||
value: function _getLastBatch() {
|
||||
var tableName = this.config.tableName;
|
||||
return this.knex(tableName).where('batch', function (qb) {
|
||||
qb.max('batch').from(tableName);
|
||||
}).orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
// Returns the latest batch number.
|
||||
}, {
|
||||
key: '_latestBatchNumber',
|
||||
value: function _latestBatchNumber() {
|
||||
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
|
||||
return obj[0].max_batch || 0;
|
||||
});
|
||||
}
|
||||
|
||||
// If transaction conf for a single migration is defined, use that.
|
||||
// Otherwise, rely on the common config. This allows enabling/disabling
|
||||
// transaction for a single migration by will, regardless of the common
|
||||
// config.
|
||||
}, {
|
||||
key: '_useTransaction',
|
||||
value: function _useTransaction(migration, allTransactionsDisabled) {
|
||||
var singleTransactionValue = _.get(migration, 'config.transaction');
|
||||
|
||||
return _.isBoolean(singleTransactionValue) ? singleTransactionValue : !allTransactionsDisabled;
|
||||
}
|
||||
|
||||
// Runs a batch of `migrations` in a specified `direction`,
|
||||
// saving the appropriate database information as the migrations are run.
|
||||
}, {
|
||||
key: '_waterfallBatch',
|
||||
value: function _waterfallBatch(batchNo, migrations, direction) {
|
||||
var _this9 = this;
|
||||
|
||||
var knex = this.knex;
|
||||
var _config = this.config;
|
||||
var tableName = _config.tableName;
|
||||
var disableTransactions = _config.disableTransactions;
|
||||
|
||||
var directory = this._absoluteConfigDir();
|
||||
var current = Promise.bind({ failed: false, failedOn: 0 });
|
||||
var log = [];
|
||||
_.each(migrations, function (migration) {
|
||||
var name = migration;
|
||||
migration = require(directory + '/' + name);
|
||||
|
||||
// We're going to run each of the migrations in the current "up"
|
||||
current = current.then(function () {
|
||||
if (_this9._useTransaction(migration, disableTransactions)) {
|
||||
return _this9._transaction(migration, direction, name);
|
||||
}
|
||||
return warnPromise(migration[direction](knex, Promise), name);
|
||||
}).then(function () {
|
||||
log.push(path.join(directory, name));
|
||||
if (direction === 'up') {
|
||||
return knex(tableName).insert({
|
||||
name: name,
|
||||
batch: batchNo,
|
||||
migration_time: new Date()
|
||||
});
|
||||
}
|
||||
if (direction === 'down') {
|
||||
return knex(tableName).where({ name: name }).del();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return current.thenReturn([batchNo, log]);
|
||||
}
|
||||
}, {
|
||||
key: '_transaction',
|
||||
value: function _transaction(migration, direction, name) {
|
||||
return this.knex.transaction(function (trx) {
|
||||
return warnPromise(migration[direction](trx, Promise), name, function () {
|
||||
trx.commit();
|
||||
});
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: '_absoluteConfigDir',
|
||||
value: function _absoluteConfigDir() {
|
||||
return path.resolve(process.cwd(), this.config.directory);
|
||||
}
|
||||
}, {
|
||||
key: 'setConfig',
|
||||
value: function setConfig(config) {
|
||||
return assign({
|
||||
extension: 'js',
|
||||
tableName: 'knex_migrations',
|
||||
directory: './migrations'
|
||||
}, this.config || {}, config);
|
||||
}
|
||||
}]);
|
||||
|
||||
return Migrator;
|
||||
})();
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
"test": "npm run babel && npm run jshint && istanbul --config=test/.istanbul.yml cover node_modules/mocha/bin/_mocha -- --check-leaks -t 5000 -b -R spec test/index.js && npm run tape",
|
||||
"plaintest": "mocha --check-leaks -t 10000 -b -R spec test/index.js && npm run tape",
|
||||
"coveralls": "cat ./test/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
||||
"jshint": "jshint --exclude=test/coverage/. test/. src/."
|
||||
"jshint": "jshint test/. src/."
|
||||
},
|
||||
"bin": {
|
||||
"knex": "./lib/bin/cli.js"
|
||||
|
||||
@ -9,6 +9,13 @@ var mkdirp = require('mkdirp');
|
||||
var Promise = require('../promise');
|
||||
var helpers = require('../helpers');
|
||||
var assign = require('lodash/object/assign');
|
||||
var inherits = require('inherits');
|
||||
|
||||
function LockError(msg) {
|
||||
this.name = 'MigrationLocked';
|
||||
this.message = msg;
|
||||
}
|
||||
inherits(LockError, Error);
|
||||
|
||||
// The new migration we're performing, typically called from the `knex.migrate`
|
||||
// interface on the main `knex` object. Passes the `knex` instance performing
|
||||
@ -70,6 +77,13 @@ export default class Migrator {
|
||||
})
|
||||
}
|
||||
|
||||
forceFreeMigrationsLock(config) {
|
||||
this.config = this.setConfig(config);
|
||||
var lockTable = this._getLockTableName();
|
||||
return this.knex.schema.hasTable(lockTable)
|
||||
.then(exist => exist && this._freeLock());
|
||||
}
|
||||
|
||||
// Creates a new migration, with a given name.
|
||||
make(name, config) {
|
||||
this.config = this.setConfig(config);
|
||||
@ -105,10 +119,13 @@ export default class Migrator {
|
||||
// dependent on the migration config settings.
|
||||
_ensureTable() {
|
||||
var table = this.config.tableName;
|
||||
var lockTable = this._getLockTableName();
|
||||
return this.knex.schema.hasTable(table)
|
||||
.then((exists) => {
|
||||
if (!exists) return this._createMigrationTable(table);
|
||||
});
|
||||
.then(exists => !exists && this._createMigrationTable(table))
|
||||
.then(() => this.knex.schema.hasTable(lockTable))
|
||||
.then(exists => !exists && this._createMigrationLockTable(lockTable))
|
||||
.then(() => this.knex(lockTable).select('*'))
|
||||
.then(data => !data.length && this.knex(lockTable).insert({ is_locked: 0 }));
|
||||
}
|
||||
|
||||
// Create the migration table, if it doesn't already exist.
|
||||
@ -121,22 +138,88 @@ export default class Migrator {
|
||||
});
|
||||
}
|
||||
|
||||
_createMigrationLockTable(tableName) {
|
||||
return this.knex.schema.createTable(tableName, function(t) {
|
||||
t.integer('is_locked');
|
||||
});
|
||||
}
|
||||
|
||||
_getLockTableName() {
|
||||
return this.config.tableName + '_lock';
|
||||
}
|
||||
|
||||
_isLocked(trx) {
|
||||
var tableName = this._getLockTableName();
|
||||
return this.knex(tableName)
|
||||
.transacting(trx)
|
||||
.forUpdate()
|
||||
.select('*')
|
||||
.then(data => data[0].is_locked);
|
||||
}
|
||||
|
||||
_lockMigrations(trx) {
|
||||
var tableName = this._getLockTableName();
|
||||
return this.knex(tableName)
|
||||
.transacting(trx)
|
||||
.update({ is_locked: 1 });
|
||||
}
|
||||
|
||||
_getLock() {
|
||||
return this.knex.transaction(trx => {
|
||||
return this._isLocked(trx)
|
||||
.then(isLocked => {
|
||||
if (isLocked) {
|
||||
throw new Error("Migration table is already locked");
|
||||
}
|
||||
})
|
||||
.then(() => this._lockMigrations(trx));
|
||||
}).catch(err => {
|
||||
throw new LockError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
_freeLock() {
|
||||
var tableName = this._getLockTableName();
|
||||
return this.knex(tableName)
|
||||
.update({ is_locked: 0 });
|
||||
}
|
||||
|
||||
// Run a batch of current migrations, in sequence.
|
||||
_runBatch(migrations, direction) {
|
||||
return Promise.all(_.map(migrations, this._validateMigrationStructure, this))
|
||||
.then(() => this._latestBatchNumber())
|
||||
.then((batchNo) => {
|
||||
if (direction === 'up') batchNo++;
|
||||
return batchNo;
|
||||
})
|
||||
.then((batchNo) => {
|
||||
return this._waterfallBatch(batchNo, migrations, direction)
|
||||
})
|
||||
.catch((error) => {
|
||||
return this._getLock()
|
||||
.then(() => Promise.all(_.map(migrations, this._validateMigrationStructure, this)))
|
||||
.then(() => this._latestBatchNumber())
|
||||
.then(batchNo => {
|
||||
if (direction === 'up') batchNo++;
|
||||
return batchNo;
|
||||
})
|
||||
.then(batchNo => {
|
||||
return this._waterfallBatch(batchNo, migrations, direction)
|
||||
})
|
||||
.then(() => this._freeLock())
|
||||
.catch(error => {
|
||||
var cleanupReady = Promise.resolve();
|
||||
|
||||
if (error instanceof LockError) {
|
||||
// if locking error do not free the lock
|
||||
helpers.warn('Cant take lock to run migrations: ' + error.message);
|
||||
helpers.warn(
|
||||
'If you are sue migrations are not running you can release ' +
|
||||
'lock manually by deleting all the rows from migrations lock table: ' +
|
||||
this._getLockTableName()
|
||||
);
|
||||
} else {
|
||||
helpers.warn('migrations failed with error: ' + error.message)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
// If the error was not due to a locking issue, then
|
||||
// remove the lock.
|
||||
cleanupReady = this._freeLock();
|
||||
}
|
||||
|
||||
return cleanupReady.finally(function() {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Validates some migrations by requiring and checking for an `up` and `down` function.
|
||||
_validateMigrationStructure(name) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/*global after, before, describe, expect, it*/
|
||||
/*jshint -W030 */
|
||||
'use strict';
|
||||
|
||||
var equal = require('assert').equal;
|
||||
@ -10,6 +11,11 @@ module.exports = function(knex) {
|
||||
|
||||
require('rimraf').sync(path.join(__dirname, './migration'));
|
||||
|
||||
before(function() {
|
||||
// make sure lock was not left from previous failed test run
|
||||
return knex.migrate.forceFreeMigrationsLock({directory: 'test/integration/migrate/test'});
|
||||
});
|
||||
|
||||
describe('knex.migrate', function () {
|
||||
|
||||
it('should create a new migration file with the create method', function() {
|
||||
@ -31,13 +37,24 @@ module.exports = function(knex) {
|
||||
describe('knex.migrate.status', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'}).catch(function() {});
|
||||
// ignore errors from failed migrations
|
||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'}).catch(function () {});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return knex.migrate.rollback({directory: 'test/integration/migrate/test'});
|
||||
});
|
||||
|
||||
it('should create a migrations lock table', function() {
|
||||
return knex.schema.hasTable('knex_migrations_lock').then(function(exists) {
|
||||
expect(exists).to.equal(true);
|
||||
|
||||
return knex.schema.hasColumn('knex_migrations_lock', 'is_locked').then(function(exists) {
|
||||
expect(exists).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 0 if code matches DB', function() {
|
||||
// Put in a couple dummy migrations. Needed
|
||||
// since the migrations directory has a couple
|
||||
@ -115,7 +132,51 @@ module.exports = function(knex) {
|
||||
describe('knex.migrate.latest', function() {
|
||||
|
||||
before(function() {
|
||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'}).catch(function() {});
|
||||
// ignore errors from failed migrations
|
||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'}).catch(function () {});
|
||||
});
|
||||
|
||||
it('should remove the record in the lock table once finished', function() {
|
||||
return knex('knex_migrations_lock').select('*').then(function(data) {
|
||||
expect(data[0]).to.have.property('is_locked');
|
||||
expect(data[0].is_locked).to.not.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if the migrations are already running', function() {
|
||||
return knex('knex_migrations_lock')
|
||||
.update({ is_locked: 1 })
|
||||
.then(function() {
|
||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'})
|
||||
.then(function() {
|
||||
throw new Error('then should not execute');
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
expect(error).to.have.property('message', 'Migration table is already locked');
|
||||
return knex('knex_migrations_lock').select('*');
|
||||
})
|
||||
.then(function(data) {
|
||||
expect(data[0].is_locked).to.equal(1);
|
||||
|
||||
// Clean up lock for other tests
|
||||
return knex('knex_migrations_lock').update({ is_locked: 0 })
|
||||
});
|
||||
});
|
||||
|
||||
it('should release lock if non-locking related error is thrown', function() {
|
||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'})
|
||||
.then(function() {
|
||||
throw new Error('then should not execute');
|
||||
})
|
||||
.catch(function(error) {
|
||||
// This will fail because of the invalid migration
|
||||
expect(error).to.have.property('message');
|
||||
return knex('knex_migrations_lock').select('*')
|
||||
})
|
||||
.then(function(data) {
|
||||
expect(data[0].is_locked).to.not.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
it('should run all migration files in the specified directory', function() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user