mig
Version:
MongoDB migration framework
174 lines (142 loc) • 4.94 kB
JavaScript
var mongodb = require('mongodb')
, _ = require('underscore')
, async = require('async')
, path = require('path')
, fs = require('fs');
var Migration = module.exports.Migration = function Migration (options) {
this.options = _.extend({
path: 'migrations',
run: false,
console: {
log: function (s) {
console.log(':: ' + s);
},
error: function (s) {
console.error('Error: ' + s);
}
}
}, options);
this.err = this.options.console.error;
this.log = this.options.console.log;
};
Migration.prototype.migrate = function (callback) {
var self = this;
var c = {err: this.err, log: this.log};
var migrationsPath = path.resolve(self.options.path)
, configPath = path.join(migrationsPath, 'config');
// Load MongoDB configuration, can either be a JSON or Javascript file (with
// access to environment, as set by foreman for example).
var config
try {
config = require(configPath);
} catch (err) {
c.err('Could not load configuration');
throw err
}
// Connect to MongoDB using the provided configuration.
mongodb.MongoClient.connect(config.url, function (err, db) {
if (err) {
c.err('Could not connect to MongoDB');
throw err;
}
// Grab the `migrations` collection, used to store schema version.
db.collection('migrations', {strict: true}, function (err, migrations) {
if (err) {
c.err('Could not obtain `migrations` collection');
throw err;
}
c.log('Connected to MongoDB');
// Figure out the current version.
migrations.findOne({_id: 'version'}, function (err, doc) {
if (err) {
c.err('Could not obtain version');
throw err;
}
var version = doc ? doc.val : 0;
c.log('Current version: ' + version);
var scripts;
if (typeof self.options.script === 'undefined') {
// Prepare a list of migration scripts to execute.
scripts = _.chain(fs.readdirSync(migrationsPath))
// Extract version from filename.
.map(function (item) {
var m = item.match(/^([\d]+)-/);
return {
filename: item,
version: m ? parseInt(m[1]) : null
};
})
// Filter out non-scripts (ie. files without version numbers).
.filter(function (item) {
return item.version !== null;
})
// Filter out elements below current version.
.filter(function (item) {
return item.version > version;
})
// Sort by version.
.sortBy(function (item) {
return item.version;
})
// Get the values.
.value();
} else {
if (_.isArray(self.options.script))
scripts = _.map(self.options.script, function (script) {
return {
filename: script,
version: null
}
});
else
scripts = [
{
filename: self.options.script,
version: null
}
];
}
if (!self.options.run) {
c.log('Plan:' + _.map(scripts, function (item) {
return '\n ' + item.filename;
}));
callback(0);
}
// Run migration scripts sequentially
async.mapSeries(scripts, function (script, next) {
c.log('Migrating to version ' + script.version + ' (' + script.filename + ')');
// Prepare a callback function passed to migration scripts; it's
// responsible for bumping setting the database schema version number.
var cb = function (err) {
if (err) throw err;
if (script.version === null)
return next();
// Upsert the current version
migrations.update({ _id: 'version' }, {
$set: { val: script.version }
}, { upsert: true }, function (err, doc) {
if (err) throw err;
if (!doc)
throw new Error('Setting the current version failed');
version = script.version;
next();
});
};
// Require the migration script and actually run it, passing along the
// database connection, configuration and callback.
require(path.join(migrationsPath, script.filename))
.migrate(db, config, cb, mongodb);
}, function (err) {
if (err) {
c.log('Migration failed');
callback(1);
}
c.log('Migration complete, now at version ' + version);
// Force-quit in case a fancy program spawned the current script (eg.
// foreman).
callback(0);
});
});
});
});
};