knex/lib/migrate/index.js

248 lines
9.1 KiB
JavaScript
Raw Normal View History

// Migrator
2013-09-13 16:58:38 -04:00
// -------
2015-05-09 13:58:18 -04:00
'use strict';
2013-09-13 16:58:38 -04:00
2015-05-09 13:58:18 -04:00
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var mkdirp = require('mkdirp');
var Promise = require('../promise');
var helpers = require('../helpers');
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
// The new migration we're performing, typically called from the `knex.migrate`
// interface on the main `knex` object. Passes the `knex` instance performing
// the migration.
function Migrator(knex) {
2015-05-09 13:58:18 -04:00
this.knex = knex;
this.config = this.setConfig(knex.client.config.migrations);
}
2013-09-04 20:36:56 -04:00
// Migrators to the latest configuration.
2015-05-09 13:58:18 -04:00
Migrator.prototype.latest = Promise.method(function (config) {
this.config = this.setConfig(config);
2015-05-09 13:58:18 -04:00
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype.rollback = Promise.method(function (config) {
this.config = this.setConfig(config);
2015-05-09 13:58:18 -04:00
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`.
2015-05-09 13:58:18 -04:00
Migrator.prototype.currentVersion = function (config) {
this.config = this.setConfig(config);
2015-05-09 13:58:18 -04:00
return this._listCompleted(config).then(function (completed) {
var val = _.chain(completed).map(function (value) {
return value.split('_')[0];
}).max().value();
2015-05-09 13:58:18 -04:00
return val === -Infinity ? 'none' : val;
});
};
// Creates a new migration, with a given name.
2015-05-09 13:58:18 -04:00
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'));
2015-05-09 13:58:18 -04:00
return this._ensureFolder(config).bind(this).then(this._generateStubTemplate).then(this._writeNewMigration(name));
};
2013-09-04 20:36:56 -04:00
// Lists all available migration versions, as a sorted array.
2015-05-09 13:58:18 -04:00
Migrator.prototype._listAll = Promise.method(function (config) {
this.config = this.setConfig(config);
2015-05-09 13:58:18 -04:00
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._ensureFolder = function () {
var dir = this._absoluteConfigDir();
2015-05-09 13:58:18 -04:00
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._ensureTable = Promise.method(function () {
var table = this.config.tableName;
2015-05-09 13:58:18 -04:00
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.
2015-05-09 13:58:18 -04:00
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');
});
};
2013-09-04 20:36:56 -04:00
// Run a batch of current migrations, in sequence.
2015-05-09 13:58:18 -04:00
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);
});
2015-05-09 13:58:18 -04:00
});
};
2013-09-04 20:36:56 -04:00
2014-04-27 20:03:10 -04:00
// Validates some migrations by requiring and checking for an `up` and `down` function.
2015-05-09 13:58:18 -04:00
Migrator.prototype._validateMigrationStructure = function (name) {
var migration = require(path.join(this._absoluteConfigDir(), name));
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
2014-04-27 20:03:10 -04:00
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._listCompleted = Promise.method(function () {
var tableName = this.config.tableName;
2015-05-09 13:58:18 -04:00
return this._ensureTable(tableName).bind(this).then(function () {
return this.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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._migrationData = function () {
return Promise.all([this._listAll(), this._listCompleted()]);
};
// Generates the stub template for the current migration, returning a compiled template.
2015-05-09 13:58:18 -04:00
Migrator.prototype._generateStubTemplate = function () {
var stubPath = this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub');
2015-05-09 13:58:18 -04:00
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._writeNewMigration = function (name) {
var config = this.config;
var dir = this._absoluteConfigDir();
2015-05-09 13:58:18 -04:00
return function (tmpl) {
if (name[0] === '-') name = name.slice(1);
2015-05-09 13:58:18 -04:00
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._getLastBatch = function () {
var tableName = this.config.tableName;
2015-05-09 13:58:18 -04:00
return this.knex(tableName).where('batch', function (qb) {
qb.max('batch').from(tableName);
}).orderBy('id', 'desc');
};
// Returns the latest batch number.
2015-05-09 13:58:18 -04:00
Migrator.prototype._latestBatchNumber = function () {
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
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.
2015-05-09 13:58:18 -04:00
Migrator.prototype._waterfallBatch = function (batchNo, migrations, direction) {
var knex = this.knex;
var tableName = this.config.tableName;
var directory = this._absoluteConfigDir();
2015-05-09 13:58:18 -04:00
var current = Promise.bind({ failed: false, failedOn: 0 });
var log = [];
_.each(migrations, function (migration) {
var name = migration;
2014-04-27 20:03:10 -04:00
migration = require(directory + '/' + name);
2014-04-27 20:03:10 -04:00
// We're going to run each of the migrations in the current "up"
2015-05-09 13:58:18 -04:00
current = current.then(function () {
return knex.transaction(function (trx) {
return warnPromise(migration[direction](trx, Promise), 'migration ' + name + ' did not return a promise');
2015-01-08 23:53:11 +01:00
});
2015-05-09 13:58:18 -04:00
}).then(function () {
log.push(path.join(directory, name));
2014-04-27 20:03:10 -04:00
if (direction === 'up') {
return knex(tableName).insert({
name: name,
batch: batchNo,
migration_time: new Date()
});
}
if (direction === 'down') {
2015-05-09 13:58:18 -04:00
return knex(tableName).where({ name: name }).del();
2014-04-27 20:03:10 -04:00
}
});
2014-04-27 20:03:10 -04:00
});
2013-09-04 20:36:56 -04:00
2014-04-27 20:03:10 -04:00
return current.thenReturn([batchNo, log]);
};
2015-05-09 13:58:18 -04:00
Migrator.prototype._absoluteConfigDir = function () {
return path.resolve(process.cwd(), this.config.directory);
};
2015-05-09 13:58:18 -04:00
Migrator.prototype.setConfig = function (config) {
return _.extend({
extension: 'js',
tableName: 'knex_migrations',
directory: './migrations'
}, this.config || {}, config);
};
// Validates that migrations are present in the appropriate directories.
function validateMigrationList(migrations) {
var all = migrations[0];
var completed = migrations[1];
var diff = _.difference(completed, all);
if (!_.isEmpty(diff)) {
2015-05-09 13:58:18 -04:00
throw new Error('The migration directory is corrupt, the following files are missing: ' + diff.join(', '));
}
}
function warnPromise(value, message) {
if (!value || typeof value.then !== 'function') {
helpers.warn(message);
}
return value;
}
// Ensure that we have 2 places for each of the date segments
2015-05-09 13:58:18 -04:00
var padDate = function padDate(segment) {
segment = segment.toString();
return segment[1] ? segment : '0' + segment;
};
// Get a date object in the correct format, without requiring
// a full out library like "moment.js".
2015-05-09 13:58:18 -04:00
var yyyymmddhhmmss = function yyyymmddhhmmss() {
var d = new Date();
2015-05-09 13:58:18 -04:00
return d.getFullYear().toString() + padDate(d.getMonth() + 1) + padDate(d.getDate()) + padDate(d.getHours()) + padDate(d.getMinutes()) + padDate(d.getSeconds());
};
2015-05-09 13:58:18 -04:00
module.exports = Migrator;