2014-04-08 16:25:57 -04:00
|
|
|
// Migrator
|
2013-09-13 16:58:38 -04:00
|
|
|
// -------
|
2013-10-24 21:54:35 -04:00
|
|
|
"use strict";
|
2013-09-13 16:58:38 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
var fs = require('fs');
|
|
|
|
var path = require('path');
|
|
|
|
var _ = require('lodash');
|
|
|
|
var mkdirp = require('mkdirp');
|
2013-11-02 13:14:38 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
var Promise = require('../promise');
|
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.
|
2014-04-08 16:25:57 -04:00
|
|
|
function Migrator(config) {
|
|
|
|
this.config = _.defaults(config || {}, {
|
|
|
|
extension: 'js',
|
|
|
|
tableName: 'knex_migrations',
|
|
|
|
directory: process.cwd() + '/migrations'
|
|
|
|
});
|
2014-04-27 12:00:51 +03:00
|
|
|
this.knex = config.knex;
|
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Resolve to the correct directory when running globally.
|
|
|
|
if (this.config.directory.indexOf('./') === 0) {
|
|
|
|
this.config.directory = path.resolve(process.cwd(), this.config.directory);
|
|
|
|
}
|
|
|
|
}
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Ensures that the proper table has been created,
|
|
|
|
// dependent on the migration config settings.
|
|
|
|
Migrator.prototype.ensureTable = function(config) {
|
|
|
|
var migration = this;
|
2014-04-27 12:00:51 +03:00
|
|
|
return this.knex.schema.hasTable(this.config.tableName)
|
2014-04-08 16:25:57 -04:00
|
|
|
.then(function(exists) {
|
2014-04-27 12:00:51 +03:00
|
|
|
if (!exists) return migration.createMigrationTable(migration.config.tableName);
|
2014-04-08 16:25:57 -04:00
|
|
|
});
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Ensures a folder for the migrations exist, dependent on the
|
|
|
|
// migration config settings.
|
|
|
|
Migrator.prototype.ensureFolder = function(config) {
|
|
|
|
return Promise.promisify(fs.stat, fs)(config.directory)
|
|
|
|
.catch(function() {
|
|
|
|
return Promise.promisify(mkdirp)(config.directory);
|
2013-09-04 20:36:56 -04:00
|
|
|
});
|
2014-04-08 16:25:57 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
// 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.dateTime('migration_time');
|
|
|
|
});
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Migrators to the latest configuration.
|
|
|
|
Migrator.prototype.latest = function(config) {
|
2014-04-27 12:00:51 +03:00
|
|
|
return this.migrationData()
|
2014-04-08 16:25:57 -04:00
|
|
|
.tap(validateMigrationList)
|
2014-04-27 12:00:51 +03:00
|
|
|
.bind(this)
|
2014-04-08 16:25:57 -04:00
|
|
|
.spread(function(all, completed) {
|
|
|
|
return this.runBatch(_.difference(all, completed), 'up');
|
|
|
|
})
|
|
|
|
.bind();
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Rollback the last "batch" of migrations that were run.
|
|
|
|
Migrator.prototype.rollback = function(config) {
|
2014-04-27 12:00:51 +03:00
|
|
|
return this.migrationData()
|
2014-04-08 16:25:57 -04:00
|
|
|
.tap(validateMigrationList)
|
2014-04-27 12:00:51 +03:00
|
|
|
.bind(this)
|
2014-04-08 16:25:57 -04:00
|
|
|
.then(this.getLastBatch)
|
|
|
|
.then(function(migrations) {
|
|
|
|
return this.runBatch(_.pluck(migrations, 'name'), 'down');
|
|
|
|
})
|
|
|
|
.bind();
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Run a batch of current migrations, in sequence.
|
|
|
|
Migrator.prototype.runBatch = function(migrations, direction) {
|
|
|
|
return Promise.all(_.map(migrations, this.validateMigrationStructure, this))
|
2013-11-02 17:37:30 -04:00
|
|
|
.bind(this)
|
|
|
|
.then(function(migrations) {
|
2014-04-27 20:03:10 -04:00
|
|
|
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);
|
|
|
|
});
|
2013-11-02 13:14:38 -04:00
|
|
|
});
|
2014-04-08 16:25:57 -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.
|
|
|
|
Migrator.prototype.validateMigrationStructure = function(name) {
|
|
|
|
var migration = require(this.config.directory + '/' + name);
|
|
|
|
if (!_.isFunction(migration.up) || !_.isFunction(migration.down)) {
|
|
|
|
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
|
|
|
|
}
|
|
|
|
return name;
|
|
|
|
};
|
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// 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) {
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Creates a new migration, with a given name.
|
2014-04-27 12:00:51 +03:00
|
|
|
Migrator.prototype.make = function(config) {
|
|
|
|
if (!config.name) Promise.rejected(new Error('A name must be specified for the generated migration'));
|
|
|
|
return this.ensureFolder(config)
|
|
|
|
.bind(this)
|
2014-04-08 16:25:57 -04:00
|
|
|
.then(this.generateStubTemplate)
|
2014-04-27 12:00:51 +03:00
|
|
|
.then(this.writeNewMigration(config.name));
|
2014-04-08 16:25:57 -04:00
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Lists all available migration versions, as a sorted array.
|
|
|
|
Migrator.prototype.listAll = function(config) {
|
2014-04-27 12:00:51 +03:00
|
|
|
return Promise.promisify(fs.readdir, fs)(this.config.directory)
|
|
|
|
.bind(this)
|
|
|
|
.then(function(migrations) {
|
|
|
|
var ext = this.config.extension;
|
|
|
|
return _.filter(migrations, function (value) {
|
|
|
|
return value.indexOf(ext, value.length - ext.length) !== -1;
|
|
|
|
}).sort();
|
|
|
|
});
|
2014-04-08 16:25:57 -04:00
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Lists all migrations that have been completed for the current db, as an array.
|
2014-04-16 03:22:47 -04:00
|
|
|
Migrator.prototype.listCompleted = function(config) {
|
2014-04-27 12:00:51 +03:00
|
|
|
return this.ensureTable(this.config.tableName)
|
|
|
|
.bind(this)
|
|
|
|
.then(function () {
|
2014-04-08 16:25:57 -04:00
|
|
|
return this.knex(this.config.tableName).orderBy('id').select('name');
|
|
|
|
})
|
|
|
|
.then(function(migrations) {
|
|
|
|
return _.pluck(migrations, 'name');
|
|
|
|
})
|
|
|
|
.bind();
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// 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() {
|
|
|
|
return Promise.all([
|
|
|
|
this.listAll(),
|
|
|
|
this.listCompleted()
|
|
|
|
]);
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Generates the stub template for the current migration, returning a compiled template.
|
|
|
|
Migrator.prototype.generateStubTemplate = function() {
|
|
|
|
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'});
|
|
|
|
});
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// 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(name) {
|
|
|
|
var config = this.config;
|
|
|
|
return function(tmpl) {
|
|
|
|
if (name[0] === '-') name = name.slice(1);
|
|
|
|
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
|
|
|
|
return Promise.promisify(fs.writeFile, fs)(
|
|
|
|
path.join(config.directory, filename),
|
|
|
|
tmpl(config.variables || {})
|
|
|
|
).yield(filename);
|
|
|
|
};
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Get the last batch of migrations, by name, ordered by insert id
|
|
|
|
// in reverse order.
|
|
|
|
Migrator.prototype.getLastBatch = function() {
|
|
|
|
var knex = this.knex;
|
|
|
|
var tableName = this.config.tableName;
|
|
|
|
return this.knex(tableName)
|
|
|
|
.where('batch', function() {
|
|
|
|
this.select(knex.raw('MAX(batch)')).from(tableName);
|
|
|
|
})
|
|
|
|
.orderBy('id', 'desc');
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Returns the latest batch number.
|
|
|
|
Migrator.prototype.latestBatchNumber = function() {
|
|
|
|
return this.knex(this.config.tableName)
|
|
|
|
.max('batch as batchNo').then(function(obj) {
|
|
|
|
return (obj[0].batchNo || 0);
|
|
|
|
});
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
// Runs a batch of `migrations` in a specified `direction`,
|
|
|
|
// saving the appropriate database information as the migrations are run.
|
2014-04-27 20:03:10 -04:00
|
|
|
Migrator.prototype.waterfallBatch = function(batchNo, migrations, direction) {
|
2014-04-08 16:25:57 -04:00
|
|
|
var knex = this.knex;
|
|
|
|
var tableName = this.config.tableName;
|
2014-04-27 20:03:10 -04:00
|
|
|
var current = Promise.bind({failed: false, failedOn: 0});
|
2014-04-08 16:25:57 -04:00
|
|
|
var log = [];
|
2014-04-27 12:00:51 +03:00
|
|
|
var directory = this.config.directory;
|
|
|
|
|
2014-04-27 20:03:10 -04:00
|
|
|
_.each(migrations, function(migration, i) {
|
|
|
|
var name = migration;
|
|
|
|
migration = require(directory + '/' + name);
|
2014-04-08 16:25:57 -04:00
|
|
|
|
2014-04-27 20:03:10 -04:00
|
|
|
// We're going to run each of the migrations in the current "up"
|
|
|
|
current = current.then(function() {
|
|
|
|
return migration[direction](knex, Promise);
|
|
|
|
}).then(function() {
|
|
|
|
log.push(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();
|
|
|
|
}
|
2014-04-08 16:25:57 -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]);
|
|
|
|
};
|
2013-11-02 17:37:30 -04:00
|
|
|
|
|
|
|
// Validates that migrations are present in the appropriate directories.
|
|
|
|
function validateMigrationList(all, completed) {
|
|
|
|
var diff = _.difference(completed, all);
|
|
|
|
if (!_.isEmpty(diff)) {
|
|
|
|
throw new Error(
|
|
|
|
'The migration directory is corrupt, the following files are missing: ' + diff.join(', ')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Get a date object in the correct format, without requiring
|
|
|
|
// a full out library like "moment.js".
|
2013-09-04 20:36:56 -04:00
|
|
|
var yyyymmddhhmmss = function() {
|
|
|
|
var d = new Date();
|
2013-10-24 21:54:35 -04:00
|
|
|
return d.getFullYear().toString() +
|
|
|
|
padDate(d.getMonth() + 1) +
|
|
|
|
padDate(d.getDate()) +
|
|
|
|
padDate(d.getHours()) +
|
|
|
|
padDate(d.getMinutes()) +
|
|
|
|
padDate(d.getSeconds());
|
2013-09-04 20:36:56 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
// Ensure that we have 2 places for each of the date segments
|
|
|
|
var padDate = function(segment) {
|
|
|
|
segment = segment.toString();
|
|
|
|
return segment[1] ? segment : '0' + segment;
|
|
|
|
};
|
|
|
|
|
2014-04-08 16:25:57 -04:00
|
|
|
module.exports = Migrator;
|