mirror of
https://github.com/knex/knex.git
synced 2025-07-09 18:11:17 +00:00
Add disableTransactions option, #834
This commit is contained in:
parent
234a26e501
commit
01bae6a598
@ -1,3 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.__esModule = true;
|
||||||
|
|
||||||
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
|
||||||
|
|
||||||
// Migrator
|
// Migrator
|
||||||
// -------
|
// -------
|
||||||
'use strict';
|
'use strict';
|
||||||
@ -8,212 +14,264 @@ var _ = require('lodash');
|
|||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var Promise = require('../promise');
|
var Promise = require('../promise');
|
||||||
var helpers = require('../helpers');
|
var helpers = require('../helpers');
|
||||||
|
var assign = require('lodash/object/assign');
|
||||||
|
|
||||||
// The new migration we're performing, typically called from the `knex.migrate`
|
// The new migration we're performing, typically called from the `knex.migrate`
|
||||||
// interface on the main `knex` object. Passes the `knex` instance performing
|
// interface on the main `knex` object. Passes the `knex` instance performing
|
||||||
// the migration.
|
// the migration.
|
||||||
function Migrator(knex) {
|
|
||||||
this.knex = knex;
|
|
||||||
this.config = this.setConfig(knex.client.config.migrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrators to the latest configuration.
|
var Migrator = (function () {
|
||||||
Migrator.prototype.latest = Promise.method(function (config) {
|
function Migrator(knex) {
|
||||||
this.config = this.setConfig(config);
|
_classCallCheck(this, Migrator);
|
||||||
return this._migrationData().tap(validateMigrationList).bind(this).spread(function (all, completed) {
|
|
||||||
return this._runBatch(_.difference(all, completed), 'up');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rollback the last "batch" of migrations that were run.
|
this.knex = knex;
|
||||||
Migrator.prototype.rollback = Promise.method(function (config) {
|
this.config = this.setConfig(knex.client.config.migrations);
|
||||||
this.config = this.setConfig(config);
|
}
|
||||||
return this._migrationData().tap(validateMigrationList).bind(this).then(this._getLastBatch).then(function (migrations) {
|
|
||||||
return this._runBatch(_.pluck(migrations, 'name'), 'down');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieves and returns the current migration version
|
// Migrators to the latest configuration.
|
||||||
// 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 (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.latest = function latest(config) {
|
||||||
Migrator.prototype.make = function (name, config) {
|
var _this = 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).bind(this).then(this._generateStubTemplate).then(this._writeNewMigration(name));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lists all available migration versions, as a sorted array.
|
this.config = this.setConfig(config);
|
||||||
Migrator.prototype._listAll = Promise.method(function (config) {
|
return this._migrationData().tap(validateMigrationList).spread(function (all, completed) {
|
||||||
this.config = this.setConfig(config);
|
return _this._runBatch(_.difference(all, completed), 'up');
|
||||||
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir()).bind(this).then(function (migrations) {
|
});
|
||||||
return _.filter(migrations, function (value) {
|
};
|
||||||
var extension = path.extname(value);
|
|
||||||
return _.contains(['.co', '.coffee', '.iced', '.js', '.litcoffee', '.ls'], extension);
|
|
||||||
}).sort();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensures a folder for the migrations exist, dependent on the
|
// Rollback the last "batch" of migrations that were run.
|
||||||
// migration config settings.
|
|
||||||
Migrator.prototype._ensureFolder = function () {
|
|
||||||
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,
|
Migrator.prototype.rollback = function rollback(config) {
|
||||||
// dependent on the migration config settings.
|
var _this2 = this;
|
||||||
Migrator.prototype._ensureTable = Promise.method(function () {
|
|
||||||
var table = this.config.tableName;
|
|
||||||
return this.knex.schema.hasTable(table).bind(this).then(function (exists) {
|
|
||||||
if (!exists) return this._createMigrationTable(table);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the migration table, if it doesn't already exist.
|
return Promise['try'](function () {
|
||||||
Migrator.prototype._createMigrationTable = function (tableName) {
|
_this2.config = _this2.setConfig(config);
|
||||||
return this.knex.schema.createTable(tableName, function (t) {
|
return _this2._migrationData().tap(validateMigrationList).then(function (val) {
|
||||||
t.increments();
|
return _this2._getLastBatch(val);
|
||||||
t.string('name');
|
}).then(function (migrations) {
|
||||||
t.integer('batch');
|
return _this2._runBatch(_.pluck(migrations, 'name'), 'down');
|
||||||
t.timestamp('migration_time');
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run a batch of current migrations, in sequence.
|
// Retrieves and returns the current migration version
|
||||||
Migrator.prototype._runBatch = function (migrations, direction) {
|
// we're on, as a promise. If there aren't any migrations run yet,
|
||||||
return Promise.all(_.map(migrations, this._validateMigrationStructure, this)).bind(this).then(function (migrations) {
|
// return "none" as the value for the `currentVersion`.
|
||||||
return Promise.bind(this).then(this._latestBatchNumber).then(function (batchNo) {
|
|
||||||
|
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', '.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++;
|
if (direction === 'up') batchNo++;
|
||||||
return batchNo;
|
return batchNo;
|
||||||
}).then(function (batchNo) {
|
}).then(function (batchNo) {
|
||||||
return this._waterfallBatch(batchNo, migrations, direction);
|
return _this5._waterfallBatch(batchNo, migrations, direction);
|
||||||
})['catch'](function (error) {
|
})['catch'](function (error) {
|
||||||
helpers.warn('migrations failed with error: ' + error.message);
|
helpers.warn('migrations failed with error: ' + error.message);
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Validates some migrations by requiring and checking for an `up` and `down` function.
|
// Validates some migrations by requiring and checking for an `up` and `down` function.
|
||||||
Migrator.prototype._validateMigrationStructure = function (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.
|
Migrator.prototype._validateMigrationStructure = function _validateMigrationStructure(name) {
|
||||||
Migrator.prototype._listCompleted = Promise.method(function () {
|
var migration = require(path.join(this._absoluteConfigDir(), name));
|
||||||
var tableName = this.config.tableName;
|
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
|
||||||
return this._ensureTable(tableName).bind(this).then(function () {
|
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
|
||||||
return this.knex(tableName).orderBy('id').select('name');
|
}
|
||||||
}).then(function (migrations) {
|
return name;
|
||||||
return _.pluck(migrations, 'name');
|
};
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gets the migration list from the specified migration directory,
|
// Lists all migrations that have been completed for the current db, as an array.
|
||||||
// as well as the list of completed migrations to check what
|
|
||||||
// should be run.
|
|
||||||
Migrator.prototype._migrationData = function () {
|
|
||||||
return Promise.all([this._listAll(), this._listCompleted()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generates the stub template for the current migration, returning a compiled template.
|
Migrator.prototype._listCompleted = function _listCompleted() {
|
||||||
Migrator.prototype._generateStubTemplate = function () {
|
var _this6 = this;
|
||||||
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,
|
var tableName = this.config.tableName;
|
||||||
// passing any `variables` given in the config to the template.
|
return this._ensureTable(tableName).then(function () {
|
||||||
Migrator.prototype._writeNewMigration = function (name) {
|
return _this6.knex(tableName).orderBy('id').select('name');
|
||||||
var config = this.config;
|
}).then(function (migrations) {
|
||||||
var dir = this._absoluteConfigDir();
|
return _.pluck(migrations, 'name');
|
||||||
return function (tmpl) {
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gets the migration list from the specified migration directory,
|
||||||
|
// as well as the list of completed migrations to check what
|
||||||
|
// should be run.
|
||||||
|
|
||||||
|
Migrator.prototype._migrationData = function _migrationData() {
|
||||||
|
return Promise.all([this._listAll(), this._listCompleted()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generates the stub template for the current migration, returning a compiled template.
|
||||||
|
|
||||||
|
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' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a new migration to disk, using the config and generated filename,
|
||||||
|
// passing any `variables` given in the config to the template.
|
||||||
|
|
||||||
|
Migrator.prototype._writeNewMigration = function _writeNewMigration(name, tmpl) {
|
||||||
|
var config = this.config;
|
||||||
|
var dir = this._absoluteConfigDir();
|
||||||
if (name[0] === '-') name = name.slice(1);
|
if (name[0] === '-') name = name.slice(1);
|
||||||
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
||||||
return Promise.promisify(fs.writeFile, fs)(path.join(dir, filename), tmpl(config.variables || {}))['return'](path.join(dir, filename));
|
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
|
// Get the last batch of migrations, by name, ordered by insert id
|
||||||
// in reverse order.
|
// in reverse order.
|
||||||
Migrator.prototype._getLastBatch = function () {
|
|
||||||
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._getLastBatch = function _getLastBatch() {
|
||||||
Migrator.prototype._latestBatchNumber = function () {
|
var tableName = this.config.tableName;
|
||||||
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
|
return this.knex(tableName).where('batch', function (qb) {
|
||||||
return obj[0].max_batch || 0;
|
qb.max('batch').from(tableName);
|
||||||
});
|
}).orderBy('id', 'desc');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Runs a batch of `migrations` in a specified `direction`,
|
// Returns the latest batch number.
|
||||||
// saving the appropriate database information as the migrations are run.
|
|
||||||
Migrator.prototype._waterfallBatch = function (batchNo, migrations, direction) {
|
|
||||||
var knex = this.knex;
|
|
||||||
var tableName = this.config.tableName;
|
|
||||||
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"
|
Migrator.prototype._latestBatchNumber = function _latestBatchNumber() {
|
||||||
current = current.then(function () {
|
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
|
||||||
return knex.transaction(function (trx) {
|
return obj[0].max_batch || 0;
|
||||||
return warnPromise(migration[direction](trx, Promise), 'migration ' + name + ' did not return a promise');
|
|
||||||
});
|
|
||||||
}).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]);
|
// Runs a batch of `migrations` in a specified `direction`,
|
||||||
};
|
// saving the appropriate database information as the migrations are run.
|
||||||
|
|
||||||
Migrator.prototype._absoluteConfigDir = function () {
|
Migrator.prototype._waterfallBatch = function _waterfallBatch(batchNo, migrations, direction) {
|
||||||
return path.resolve(process.cwd(), this.config.directory);
|
var knex = this.knex;
|
||||||
};
|
var _config = this.config;
|
||||||
|
var tableName = _config.tableName;
|
||||||
|
var disableTransactions = _config.disableTransactions;
|
||||||
|
|
||||||
Migrator.prototype.setConfig = function (config) {
|
var directory = this._absoluteConfigDir();
|
||||||
return _.extend({
|
var current = Promise.bind({ failed: false, failedOn: 0 });
|
||||||
extension: 'js',
|
var log = [];
|
||||||
tableName: 'knex_migrations',
|
_.each(migrations, function (migration) {
|
||||||
directory: './migrations'
|
var name = migration;
|
||||||
}, this.config || {}, config);
|
migration = require(directory + '/' + name);
|
||||||
};
|
|
||||||
|
// We're going to run each of the migrations in the current "up"
|
||||||
|
current = current.then(function () {
|
||||||
|
if (disableTransactions) {
|
||||||
|
return warnPromise(migration[direction](knex, Promise), 'migration ' + name + ' did not return a promise');
|
||||||
|
}
|
||||||
|
return knex.transaction(function (trx) {
|
||||||
|
return warnPromise(migration[direction](trx, Promise), 'migration ' + name + ' did not return a promise');
|
||||||
|
});
|
||||||
|
}).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]);
|
||||||
|
};
|
||||||
|
|
||||||
|
Migrator.prototype._absoluteConfigDir = function _absoluteConfigDir() {
|
||||||
|
return path.resolve(process.cwd(), this.config.directory);
|
||||||
|
};
|
||||||
|
|
||||||
|
Migrator.prototype.setConfig = function setConfig(config) {
|
||||||
|
return assign({
|
||||||
|
extension: 'js',
|
||||||
|
tableName: 'knex_migrations',
|
||||||
|
directory: './migrations'
|
||||||
|
}, this.config || {}, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Migrator;
|
||||||
|
})();
|
||||||
|
|
||||||
|
exports['default'] = Migrator;
|
||||||
|
|
||||||
// Validates that migrations are present in the appropriate directories.
|
// Validates that migrations are present in the appropriate directories.
|
||||||
function validateMigrationList(migrations) {
|
function validateMigrationList(migrations) {
|
||||||
@ -233,16 +291,15 @@ function warnPromise(value, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that we have 2 places for each of the date segments
|
// Ensure that we have 2 places for each of the date segments
|
||||||
var padDate = function padDate(segment) {
|
function padDate(segment) {
|
||||||
segment = segment.toString();
|
segment = segment.toString();
|
||||||
return segment[1] ? segment : '0' + segment;
|
return segment[1] ? segment : '0' + segment;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Get a date object in the correct format, without requiring
|
// Get a date object in the correct format, without requiring
|
||||||
// a full out library like "moment.js".
|
// a full out library like "moment.js".
|
||||||
var yyyymmddhhmmss = function yyyymmddhhmmss() {
|
function yyyymmddhhmmss() {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
return d.getFullYear().toString() + padDate(d.getMonth() + 1) + padDate(d.getDate()) + padDate(d.getHours()) + padDate(d.getMinutes()) + padDate(d.getSeconds());
|
return d.getFullYear().toString() + padDate(d.getMonth() + 1) + padDate(d.getDate()) + padDate(d.getHours()) + padDate(d.getMinutes()) + padDate(d.getSeconds());
|
||||||
};
|
}
|
||||||
|
module.exports = exports['default'];
|
||||||
module.exports = Migrator;
|
|
@ -42,7 +42,7 @@
|
|||||||
"through": "^2.3.4"
|
"through": "^2.3.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "babel -D -w src/ --out-dir lib/",
|
"dev": "babel -L -D -w src/ --out-dir lib/",
|
||||||
"build": "./scripts/build.sh",
|
"build": "./scripts/build.sh",
|
||||||
"tape": "node test/tape/index.js",
|
"tape": "node test/tape/index.js",
|
||||||
"test": "npm run jshint && istanbul --config=test/.istanbul.yml cover _mocha -- --check-leaks -t 5000 -b -R spec test/index.js && npm run tape",
|
"test": "npm run jshint && istanbul --config=test/.istanbul.yml cover _mocha -- --check-leaks -t 5000 -b -R spec test/index.js && npm run tape",
|
||||||
|
@ -8,247 +8,242 @@ var _ = require('lodash');
|
|||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var Promise = require('../promise');
|
var Promise = require('../promise');
|
||||||
var helpers = require('../helpers');
|
var helpers = require('../helpers');
|
||||||
|
var assign = require('lodash/object/assign');
|
||||||
|
|
||||||
// The new migration we're performing, typically called from the `knex.migrate`
|
// The new migration we're performing, typically called from the `knex.migrate`
|
||||||
// interface on the main `knex` object. Passes the `knex` instance performing
|
// interface on the main `knex` object. Passes the `knex` instance performing
|
||||||
// the migration.
|
// the migration.
|
||||||
function Migrator(knex) {
|
export default class Migrator {
|
||||||
this.knex = knex
|
|
||||||
this.config = this.setConfig(knex.client.config.migrations);
|
constructor(knex) {
|
||||||
}
|
this.knex = knex
|
||||||
|
this.config = this.setConfig(knex.client.config.migrations);
|
||||||
// Migrators to the latest configuration.
|
|
||||||
Migrator.prototype.latest = Promise.method(function(config) {
|
|
||||||
this.config = this.setConfig(config);
|
|
||||||
return this._migrationData()
|
|
||||||
.tap(validateMigrationList)
|
|
||||||
.bind(this)
|
|
||||||
.spread(function(all, completed) {
|
|
||||||
return this._runBatch(_.difference(all, completed), 'up');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rollback the last "batch" of migrations that were run.
|
|
||||||
Migrator.prototype.rollback = Promise.method(function(config) {
|
|
||||||
this.config = this.setConfig(config);
|
|
||||||
return this._migrationData()
|
|
||||||
.tap(validateMigrationList)
|
|
||||||
.bind(this)
|
|
||||||
.then(this._getLastBatch)
|
|
||||||
.then(function(migrations) {
|
|
||||||
return this._runBatch(_.pluck(migrations, 'name'), 'down');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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(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(name, config) {
|
|
||||||
this.config = this.setConfig(config);
|
|
||||||
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
|
|
||||||
return this._ensureFolder(config)
|
|
||||||
.bind(this)
|
|
||||||
.then(this._generateStubTemplate)
|
|
||||||
.then(this._writeNewMigration(name));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lists all available migration versions, as a sorted array.
|
|
||||||
Migrator.prototype._listAll = Promise.method(function(config) {
|
|
||||||
this.config = this.setConfig(config);
|
|
||||||
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir())
|
|
||||||
.bind(this)
|
|
||||||
.then(function(migrations) {
|
|
||||||
return _.filter(migrations, function(value) {
|
|
||||||
var extension = path.extname(value);
|
|
||||||
return _.contains(['.co', '.coffee', '.iced', '.js', '.litcoffee', '.ls'], extension);
|
|
||||||
}).sort();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensures a folder for the migrations exist, dependent on the
|
|
||||||
// migration config settings.
|
|
||||||
Migrator.prototype._ensureFolder = function() {
|
|
||||||
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 = Promise.method(function() {
|
|
||||||
var table = this.config.tableName;
|
|
||||||
return this.knex.schema.hasTable(table)
|
|
||||||
.bind(this)
|
|
||||||
.then(function(exists) {
|
|
||||||
if (!exists) return this._createMigrationTable(table);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the migration table, if it doesn't already exist.
|
|
||||||
Migrator.prototype._createMigrationTable = function(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(migrations, direction) {
|
|
||||||
return Promise.all(_.map(migrations, this._validateMigrationStructure, this))
|
|
||||||
.bind(this)
|
|
||||||
.then(function(migrations) {
|
|
||||||
return Promise.bind(this)
|
|
||||||
.then(this._latestBatchNumber)
|
|
||||||
.then(function(batchNo) {
|
|
||||||
if (direction === 'up') batchNo++;
|
|
||||||
return batchNo;
|
|
||||||
})
|
|
||||||
.then(function(batchNo) {
|
|
||||||
return this._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(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.
|
// Migrators to the latest configuration.
|
||||||
Migrator.prototype._listCompleted = Promise.method(function() {
|
latest(config) {
|
||||||
var tableName = this.config.tableName;
|
this.config = this.setConfig(config);
|
||||||
return this._ensureTable(tableName)
|
return this._migrationData()
|
||||||
.bind(this)
|
.tap(validateMigrationList)
|
||||||
.then(function () {
|
.spread((all, completed) => {
|
||||||
return this.knex(tableName).orderBy('id').select('name');
|
return this._runBatch(_.difference(all, completed), 'up');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback the last "batch" of migrations that were run.
|
||||||
|
rollback(config) {
|
||||||
|
return Promise.try(() => {
|
||||||
|
this.config = this.setConfig(config);
|
||||||
|
return this._migrationData()
|
||||||
|
.tap(validateMigrationList)
|
||||||
|
.then((val) => this._getLastBatch(val))
|
||||||
|
.then((migrations) => {
|
||||||
|
return this._runBatch(_.pluck(migrations, 'name'), 'down');
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(function(migrations) {
|
}
|
||||||
return _.pluck(migrations, 'name');
|
|
||||||
|
// 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`.
|
||||||
|
currentVersion(config) {
|
||||||
|
this.config = this.setConfig(config);
|
||||||
|
return this._listCompleted(config)
|
||||||
|
.then((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.
|
||||||
|
make(name, config) {
|
||||||
|
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((val) => this._generateStubTemplate(val))
|
||||||
|
.then((val) => this._writeNewMigration(name, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists all available migration versions, as a sorted array.
|
||||||
|
_listAll(config) {
|
||||||
|
this.config = this.setConfig(config);
|
||||||
|
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir())
|
||||||
|
.then((migrations) => {
|
||||||
|
return _.filter(migrations, function(value) {
|
||||||
|
var extension = path.extname(value);
|
||||||
|
return _.contains(['.co', '.coffee', '.iced', '.js', '.litcoffee', '.ls'], extension);
|
||||||
|
}).sort();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures a folder for the migrations exist, dependent on the
|
||||||
|
// migration config settings.
|
||||||
|
_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.
|
||||||
|
_ensureTable() {
|
||||||
|
var table = this.config.tableName;
|
||||||
|
return this.knex.schema.hasTable(table)
|
||||||
|
.then((exists) => {
|
||||||
|
if (!exists) return this._createMigrationTable(table);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the migration table, if it doesn't already exist.
|
||||||
|
_createMigrationTable(tableName) {
|
||||||
|
return this.knex.schema.createTable(tableName, function(t) {
|
||||||
|
t.increments();
|
||||||
|
t.string('name');
|
||||||
|
t.integer('batch');
|
||||||
|
t.timestamp('migration_time');
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Gets the migration list from the specified migration directory,
|
// Run a batch of current migrations, in sequence.
|
||||||
// as well as the list of completed migrations to check what
|
_runBatch(migrations, direction) {
|
||||||
// should be run.
|
return Promise.all(_.map(migrations, this._validateMigrationStructure, this))
|
||||||
Migrator.prototype._migrationData = function() {
|
.then(() => this._latestBatchNumber())
|
||||||
return Promise.all([
|
.then((batchNo) => {
|
||||||
this._listAll(),
|
if (direction === 'up') batchNo++;
|
||||||
this._listCompleted()
|
return batchNo;
|
||||||
]);
|
})
|
||||||
};
|
.then((batchNo) => {
|
||||||
|
return this._waterfallBatch(batchNo, migrations, direction)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
helpers.warn('migrations failed with error: ' + error.message)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Generates the stub template for the current migration, returning a compiled template.
|
// Validates some migrations by requiring and checking for an `up` and `down` function.
|
||||||
Migrator.prototype._generateStubTemplate = function() {
|
_validateMigrationStructure(name) {
|
||||||
var stubPath = this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub');
|
var migration = require(path.join(this._absoluteConfigDir(), name));
|
||||||
return Promise.promisify(fs.readFile, fs)(stubPath).then(function(stub) {
|
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
|
||||||
return _.template(stub.toString(), null, {variable: 'd'});
|
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
|
||||||
});
|
}
|
||||||
};
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
// Write a new migration to disk, using the config and generated filename,
|
// Lists all migrations that have been completed for the current db, as an array.
|
||||||
// passing any `variables` given in the config to the template.
|
_listCompleted() {
|
||||||
Migrator.prototype._writeNewMigration = function(name) {
|
var tableName = this.config.tableName
|
||||||
var config = this.config;
|
return this._ensureTable(tableName)
|
||||||
var dir = this._absoluteConfigDir();
|
.then(() => this.knex(tableName).orderBy('id').select('name'))
|
||||||
return function(tmpl) {
|
.then((migrations) => _.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.
|
||||||
|
_migrationData() {
|
||||||
|
return Promise.all([
|
||||||
|
this._listAll(),
|
||||||
|
this._listCompleted()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the stub template for the current migration, returning a compiled template.
|
||||||
|
_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.
|
||||||
|
_writeNewMigration(name, tmpl) {
|
||||||
|
var config = this.config;
|
||||||
|
var dir = this._absoluteConfigDir();
|
||||||
if (name[0] === '-') name = name.slice(1);
|
if (name[0] === '-') name = name.slice(1);
|
||||||
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
||||||
return Promise.promisify(fs.writeFile, fs)(
|
return Promise.promisify(fs.writeFile, fs)(
|
||||||
path.join(dir, filename),
|
path.join(dir, filename),
|
||||||
tmpl(config.variables || {})
|
tmpl(config.variables || {})
|
||||||
).return(path.join(dir, filename));
|
).return(path.join(dir, filename));
|
||||||
};
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Get the last batch of migrations, by name, ordered by insert id
|
// Get the last batch of migrations, by name, ordered by insert id
|
||||||
// in reverse order.
|
// in reverse order.
|
||||||
Migrator.prototype._getLastBatch = function() {
|
_getLastBatch() {
|
||||||
var tableName = this.config.tableName;
|
var tableName = this.config.tableName;
|
||||||
return this.knex(tableName)
|
return this.knex(tableName)
|
||||||
.where('batch', function(qb) {
|
.where('batch', function(qb) {
|
||||||
qb.max('batch').from(tableName)
|
qb.max('batch').from(tableName)
|
||||||
})
|
})
|
||||||
.orderBy('id', 'desc');
|
.orderBy('id', 'desc');
|
||||||
};
|
}
|
||||||
|
|
||||||
// Returns the latest batch number.
|
// Returns the latest batch number.
|
||||||
Migrator.prototype._latestBatchNumber = function() {
|
_latestBatchNumber() {
|
||||||
return this.knex(this.config.tableName)
|
return this.knex(this.config.tableName)
|
||||||
.max('batch as max_batch').then(function(obj) {
|
.max('batch as max_batch').then(function(obj) {
|
||||||
return (obj[0].max_batch || 0);
|
return (obj[0].max_batch || 0);
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Runs a batch of `migrations` in a specified `direction`,
|
|
||||||
// saving the appropriate database information as the migrations are run.
|
|
||||||
Migrator.prototype._waterfallBatch = function(batchNo, migrations, direction) {
|
|
||||||
var knex = this.knex;
|
|
||||||
var tableName = this.config.tableName;
|
|
||||||
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() {
|
|
||||||
return knex.transaction(function(trx) {
|
|
||||||
return warnPromise(migration[direction](trx, Promise), 'migration ' + name + ' did not return a promise');
|
|
||||||
});
|
});
|
||||||
}).then(function() {
|
}
|
||||||
log.push(path.join(directory, name));
|
|
||||||
if (direction === 'up') {
|
// Runs a batch of `migrations` in a specified `direction`,
|
||||||
return knex(tableName).insert({
|
// saving the appropriate database information as the migrations are run.
|
||||||
name: name,
|
_waterfallBatch(batchNo, migrations, direction) {
|
||||||
batch: batchNo,
|
var knex = this.knex;
|
||||||
migration_time: new Date()
|
var {tableName, disableTransactions} = this.config
|
||||||
|
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 (disableTransactions) {
|
||||||
|
return warnPromise(migration[direction](knex, Promise), 'migration ' + name + ' did not return a promise')
|
||||||
|
}
|
||||||
|
return knex.transaction(function(trx) {
|
||||||
|
return warnPromise(migration[direction](trx, Promise), 'migration ' + name + ' did not return a promise');
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
if (direction === 'down') {
|
.then(() => {
|
||||||
return knex(tableName).where({name: name}).del();
|
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]);
|
return current.thenReturn([batchNo, log]);
|
||||||
};
|
}
|
||||||
|
|
||||||
Migrator.prototype._absoluteConfigDir = function() {
|
_absoluteConfigDir() {
|
||||||
return path.resolve(process.cwd(), this.config.directory);
|
return path.resolve(process.cwd(), this.config.directory);
|
||||||
};
|
}
|
||||||
|
|
||||||
Migrator.prototype.setConfig = function(config) {
|
setConfig(config) {
|
||||||
return _.extend({
|
return assign({
|
||||||
extension: 'js',
|
extension: 'js',
|
||||||
tableName: 'knex_migrations',
|
tableName: 'knex_migrations',
|
||||||
directory: './migrations'
|
directory: './migrations'
|
||||||
}, this.config || {}, config);
|
}, this.config || {}, config);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Validates that migrations are present in the appropriate directories.
|
// Validates that migrations are present in the appropriate directories.
|
||||||
function validateMigrationList(migrations) {
|
function validateMigrationList(migrations) {
|
||||||
@ -270,14 +265,14 @@ function warnPromise(value, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that we have 2 places for each of the date segments
|
// Ensure that we have 2 places for each of the date segments
|
||||||
var padDate = function(segment) {
|
function padDate(segment) {
|
||||||
segment = segment.toString();
|
segment = segment.toString();
|
||||||
return segment[1] ? segment : '0' + segment;
|
return segment[1] ? segment : '0' + segment;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Get a date object in the correct format, without requiring
|
// Get a date object in the correct format, without requiring
|
||||||
// a full out library like "moment.js".
|
// a full out library like "moment.js".
|
||||||
var yyyymmddhhmmss = function() {
|
function yyyymmddhhmmss() {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
return d.getFullYear().toString() +
|
return d.getFullYear().toString() +
|
||||||
padDate(d.getMonth() + 1) +
|
padDate(d.getMonth() + 1) +
|
||||||
@ -285,7 +280,4 @@ var yyyymmddhhmmss = function() {
|
|||||||
padDate(d.getHours()) +
|
padDate(d.getHours()) +
|
||||||
padDate(d.getMinutes()) +
|
padDate(d.getMinutes()) +
|
||||||
padDate(d.getSeconds());
|
padDate(d.getSeconds());
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = Migrator;
|
|
||||||
|
@ -31,7 +31,7 @@ module.exports = function(knex) {
|
|||||||
describe('knex.migrate.latest', function() {
|
describe('knex.migrate.latest', function() {
|
||||||
|
|
||||||
before(function() {
|
before(function() {
|
||||||
return knex.migrate.latest({directory: 'test/integration/migrate/test'});
|
return knex.migrate.latest({directory: 'test/integration/migrate/test'}).catch(function() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run all migration files in the specified directory', function() {
|
it('should run all migration files in the specified directory', function() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user