lib/migrate.js

Migrate (WIP)

var fs = require('fs'); var path = require('path'); var _ = require('underscore');

var _str = require('underscore.string'); var mkdirp = require('mkdirp');

var when = require('when'); var nodefn = require('when/node/function'); var sequence = require('when/sequence');

The new migration we're performing. Takes a config object, which has the name of the current migration (main if not otherwise specified)

var Migrate = function(instance) { this.knex = instance; _.bindAll(this, 'currentVersion', 'createMigrationTable', '_migrationData'); }; Migrate.prototype = {

Initializes the migration, by creating the proper migration file or database table, depending on the migration config settings.

initialize: function(config) { config = _.defaults(config || {}, { extension: 'js', tableName: 'knex_migrations', namespace: 'main', directory: process.cwd() + '/migrations' }); var Knex = this.Knex; var directory = config.directory; var tableName = config.tableName; return nodefn.call(fs.stat, directory).then(null, function() { return nodefn.call(mkdirp, directory); }) .then(function() { return Knex.Schema.hasTable(tableName); }) .tap(function(exists) { if (exists) this.createMigrationTable(tableName); }) .yield(config); },

Create the migration table, if it doesn't already exist.

createMigrationTable: function(tableName) { return Knex.Schema.createTable(tableName, function(t) { t.increments(); t.string('name'); t.integer('batch'); t.dateTime('migration_time'); }); },

Runs a specific migration, based on the migration version number.

run: function(version, config) { version = parseVersion(version); var migration = this; return this.initialize(config) .then(this._migrationData(config)) .spread(function(all, completed) { if (!hasRun(completed, version)) { return migration.runBatch([getMigration(all, version, config)]); } else { throw new Error('Migration ' + version + ' already exists'); } }) .then(function() { return 'Migration ' + version + ' successfully run'; }); },

Migrate "up" to a specific migration id otherwise, migrates all migrations which have not been run yet.

up: function(version, config) { return this._direction('up', version, config); },

Migrate "down" to a specific migration id, otherwise rolls back the last migration "batch".

down: function(version, config) { return this._direction('down', version, config); },

Run a batch of current migrations, in sequence.

runBatch: function(migrations, direction) { migrations = _.map(migrations, function(item) { var migration = require(item); if (!_.isFunction(migration.up) || !_.isFunction(migration.down)) { throw new Error('Invalid migration: ' + item + ' must have both an up and down function'); } return migration[direction]; }); return sequence(migrations); },

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: 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); }); },

Creates a new migration, with a given name.

create: function(name, config) { return this.initialize(config).then(function(config) { return when.all([nodefn.call(fs.readFile, config.stub || path.join(__dirname, 'stub', config.extension + '.stub')), config]); }).spread(function(stub, config) { name = _str.dasherize(name); if (name[0] === '-') name = name.slice(1); var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension; return nodefn.call(fs.writeFile, path.join(config.directory, filename), stub).then(function() { return filename; }); }); },

Lists all available migration versions, as an array.

listAll: function(config) { return this.initialize(config) .then(function(config) { return nodefn.call(fs.readdir, config.directory); }) .then(function(files) { return _.reduce(files, function(memo, value) { memo.push(value); return memo; }, []); }); },

Lists all migrations that have been completed for the current db, as an array.

listCompleted: function(config) { var Knex = this.Knex; return this.initialize(config).then(function(config) { return Knex(config.tableName).orderBy('id').select('name'); }).then(function(values) { return values; }); },

Gets the migration list, and the list of completed migrations to check what should be run.

_migrationData: function(config) { var migration = this; return function() { return when.all([migration.listAll(), migration.listCompleted()]); }; },

Shared between the up and down migrations, this helps to create the batch of migrations that need to be run.

_direction: function(direction, version, config) { version = parseVersion(version || 'latest'); var migration = this; return this.initialize(config).then(this._migrationData(config)).spread(function(all, completed, version) { }); },

Gets the current migration.

getMigration: function(all, version, config) { var found = _.find(all, function(item) { item.indexOf(version) === 0; }); if (!found) throw new Error('Unable to locate the specified migration ' + version); return path.join(config.directory, found); },

Get all of the migrations that need to be run in the current batch.

getMigrations: function(all, version, direction, config) { return _.reduce(all, function() { }); },

Check if the current version of the query has run.

hasRun: function(versions, check) { return _.some(versions, function(version) { return (version.indexOf(check) === 0); }); },

Parse the version, which really only needs to be the timestamp of the migration we wish to migrate to.

parseVersion: function(version) { if (version !== 'latest') { version = version.slice(0, 14); if (version.length !== 14) { throw new Error('Invalid version number provided'); } } return version; } };

Get a date object in this form

var yyyymmddhhmmss = function() { var d = new Date(); return d.getFullYear().toString() + padDate(d.getMonth() + 1) + padDate(d.getDate()) + padDate(d.getHours()) + padDate(d.getMinutes()) + padDate(d.getSeconds()); };

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; }; module.exports = Migrate;