2013-11-02 13:14:38 -04:00
|
|
|
// Migrate
|
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
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
var fs = require('fs');
|
|
|
|
var path = require('path');
|
2013-09-04 20:36:56 -04:00
|
|
|
var _ = require('underscore');
|
2013-10-24 21:54:35 -04:00
|
|
|
var mkdirp = require('mkdirp');
|
2013-11-02 13:14:38 -04:00
|
|
|
|
|
|
|
var Promise = require('./promise').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.
|
2013-09-05 16:36:49 -04:00
|
|
|
var Migrate = function(instance) {
|
|
|
|
this.knex = instance;
|
2013-09-04 20:36:56 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
Migrate.prototype = {
|
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Initializes the migration, taking an optional `config` object,
|
|
|
|
// for things like the `tableName`.
|
|
|
|
init: function(config) {
|
2013-11-02 13:14:38 -04:00
|
|
|
this.config = _.defaults(config || {}, {
|
2013-09-04 20:36:56 -04:00
|
|
|
extension: 'js',
|
|
|
|
tableName: 'knex_migrations',
|
|
|
|
directory: process.cwd() + '/migrations'
|
|
|
|
});
|
2013-11-02 13:40:34 -04:00
|
|
|
return Promise.all([
|
2013-10-24 21:54:35 -04:00
|
|
|
this.ensureFolder(config),
|
|
|
|
this.ensureTable(config)
|
2013-11-02 13:40:34 -04:00
|
|
|
]).bind(this);
|
2013-10-24 21:54:35 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Ensures that the proper table has been created,
|
|
|
|
// dependent on the migration config settings.
|
|
|
|
ensureTable: function(config) {
|
|
|
|
var migration = this;
|
|
|
|
return this.knex.schema.hasTable(config.tableName)
|
|
|
|
.then(function(exists) {
|
|
|
|
if (!exists) return migration.createMigrationTable(config.tableName);
|
|
|
|
});
|
|
|
|
},
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Ensures a folder for the migrations exist, dependent on the
|
|
|
|
// migration config settings.
|
|
|
|
ensureFolder: function(config) {
|
2013-11-02 13:14:38 -04:00
|
|
|
return Promise.promisify(fs.stat, fs)(config.directory)
|
|
|
|
.catch(function() {
|
|
|
|
return Promise.promisify(mkdirp)(config.directory);
|
2013-10-24 21:54:35 -04:00
|
|
|
});
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Create the migration table, if it doesn't already exist.
|
|
|
|
createMigrationTable: function(tableName) {
|
2013-10-24 21:54:35 -04:00
|
|
|
return this.knex.schema.createTable(tableName, function(t) {
|
2013-09-04 20:36:56 -04:00
|
|
|
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;
|
2013-10-24 21:54:35 -04:00
|
|
|
return this.init(config)
|
2013-11-02 13:14:38 -04:00
|
|
|
.then(this.migrationData)
|
2013-09-04 20:36:56 -04:00
|
|
|
.spread(function(all, completed) {
|
2013-10-24 21:54:35 -04:00
|
|
|
if (!migration.hasRun(completed, version)) {
|
2013-09-04 20:36:56 -04:00
|
|
|
return migration.runBatch([getMigration(all, version, config)]);
|
|
|
|
} else {
|
|
|
|
throw new Error('Migration ' + version + ' already exists');
|
|
|
|
}
|
|
|
|
})
|
2013-11-02 13:14:38 -04:00
|
|
|
.bind()
|
2013-10-24 21:54:35 -04:00
|
|
|
.yield('Migration ' + version + ' successfully run');
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Migrate "up" to a specific migration id
|
|
|
|
// otherwise, migrates all migrations which have
|
|
|
|
// not been run yet.
|
|
|
|
up: function(version, config) {
|
2013-11-02 13:14:38 -04:00
|
|
|
return this.direction('up', version, config);
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Migrate "down" to a specific migration id,
|
|
|
|
// otherwise rolls back the last migration "batch".
|
|
|
|
down: function(version, config) {
|
2013-11-02 13:14:38 -04:00
|
|
|
return this.direction('down', version, config);
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Run a batch of current migrations, in sequence.
|
|
|
|
runBatch: function(migrations, direction) {
|
2013-10-24 21:54:35 -04:00
|
|
|
var knex = this.knex;
|
2013-11-02 13:14:38 -04:00
|
|
|
return Promise.then(function() {
|
|
|
|
return Promise.map(migrations, function(item) {
|
2013-10-24 21:54:35 -04:00
|
|
|
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');
|
|
|
|
}
|
2013-11-02 13:14:38 -04:00
|
|
|
return migration;
|
|
|
|
});
|
|
|
|
}).then(function(migrations) {
|
|
|
|
var current = Promise.fulfilled();
|
|
|
|
_.each(migrations, function(migration) {
|
|
|
|
current = current.then(function() {
|
|
|
|
migration[direction](knex, Promise.fulfilled());
|
|
|
|
});
|
2013-10-24 21:54:35 -04:00
|
|
|
});
|
2013-11-02 13:14:38 -04:00
|
|
|
return current;
|
|
|
|
});
|
2013-09-04 20:36:56 -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`.
|
|
|
|
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.
|
2013-10-24 21:54:35 -04:00
|
|
|
generate: function(name, config) {
|
2013-11-02 13:14:38 -04:00
|
|
|
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
|
|
|
|
return this.init(config).then(function() {
|
|
|
|
var readFile = Promise.promisify(fs.readFile, fs);
|
|
|
|
return readFile(this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub'));
|
|
|
|
}).then(function(stub) {
|
2013-10-24 21:54:35 -04:00
|
|
|
name = dasherize(name);
|
2013-09-04 20:36:56 -04:00
|
|
|
if (name[0] === '-') name = name.slice(1);
|
2013-11-02 13:14:38 -04:00
|
|
|
var filename = yyyymmddhhmmss() + '_' + name + '.' + this.config.extension;
|
|
|
|
var writeFile = Promise.promisify(fs.writeFile, fs);
|
|
|
|
return writeFile(path.join(this.config.directory, filename), stub).yield(filename);
|
|
|
|
}).bind();
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Lists all available migration versions, as an array.
|
|
|
|
listAll: function(config) {
|
2013-10-24 21:54:35 -04:00
|
|
|
return this.init(config)
|
2013-11-02 13:14:38 -04:00
|
|
|
.then(function() {
|
|
|
|
return Promise.promisify(fs.readdir, fs)(this.config.directory);
|
2013-09-04 20:36:56 -04:00
|
|
|
})
|
|
|
|
.then(function(files) {
|
|
|
|
return _.reduce(files, function(memo, value) {
|
|
|
|
memo.push(value);
|
|
|
|
return memo;
|
|
|
|
}, []);
|
2013-11-02 13:14:38 -04:00
|
|
|
})
|
|
|
|
.bind();
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Lists all migrations that have been completed for the current db, as an array.
|
|
|
|
listCompleted: function(config) {
|
2013-10-24 21:54:35 -04:00
|
|
|
return this.init(config)
|
2013-11-02 13:14:38 -04:00
|
|
|
.then(function() {
|
|
|
|
return this.knex(this.config.tableName).orderBy('id').select('name');
|
|
|
|
})
|
|
|
|
.bind();
|
2013-09-04 20:36:56 -04:00
|
|
|
},
|
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Check if the current migration has run.
|
|
|
|
hasRun: function(versions, check) {
|
|
|
|
return _.some(versions, function(version) {
|
|
|
|
return (version.indexOf(check) === 0);
|
2013-09-04 20:36:56 -04:00
|
|
|
});
|
2013-11-02 13:14:38 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
return this.ensureTable(config)
|
|
|
|
.then(this.migrationData)
|
|
|
|
.spread(function(all, completed, version) {
|
|
|
|
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Gets the migration list from the specified migration directory,
|
|
|
|
// as well as the list of completed migrations to check what
|
|
|
|
// should be run.
|
|
|
|
migrationData: function() {
|
|
|
|
return Promise.all([
|
|
|
|
this.listAll(),
|
|
|
|
this.listCompleted()
|
|
|
|
]);
|
2013-10-24 21:54:35 -04:00
|
|
|
}
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Gets the current migration.
|
|
|
|
var 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);
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Parse the version, which really only needs to be the
|
|
|
|
// timestamp of the migration we wish to migrate to.
|
|
|
|
var parseVersion = function(version) {
|
|
|
|
if (version !== 'latest') {
|
|
|
|
version = version.slice(0, 14);
|
|
|
|
if (version.length !== 14) {
|
|
|
|
throw new Error('Invalid migration provided');
|
2013-09-04 20:36:56 -04:00
|
|
|
}
|
|
|
|
}
|
2013-10-24 21:54:35 -04:00
|
|
|
return version;
|
2013-09-04 20:36:56 -04:00
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
// Dasherize the string.
|
|
|
|
var dasherize = function(str) {
|
|
|
|
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
|
|
|
|
};
|
2013-09-04 20:36:56 -04:00
|
|
|
|
2013-10-24 21:54:35 -04:00
|
|
|
module.exports = Migrate;
|