UNPKG

sequelize-cmd

Version:
394 lines (336 loc) 14.5 kB
'use strict'; var fs = require('fs'), p = require('path'), moment, Utils, Migration, DataTypes; module.exports = function migrator() { var ctx = this; moment = this.moment; Utils = require(p.join(this.sqPath, '/lib/utils')); DataTypes = this.DataTypes; Migration = require(p.join(this.sqPath, '/lib/migration')); /** * Sequelize Migrator class. * @class Migrator * @param {object} sequelize - the sequelize instance. * @param {object} [options] - the migration options. * @constructor */ var Migrator = function Migrator(sequelize, options) { this.sequelize = sequelize; this.options = Utils._.extend({ path: __dirname + '/../migrations', from: null, to: null, logging: console.log, filesFilter: /\.js$/ }, options || {}); if (this.options.logging === true) { console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log'); this.options.logging = console.log; } if (this.options.logging == console.log) { // using just console.log will break in node < 0.6 this.options.logging = function(s) { console.log(s); } } }; /** * @property queryInterface * @memberof Migrator */ Object.defineProperty(Migrator.prototype, "queryInterface", { get: function() { return this.sequelize.getQueryInterface(); } }); /** * Runs migration by option method specified. * @memberof Migrator * @param {object} [options] - migrate options. * @returns {*} */ Migrator.prototype.migrate = function(options) { var self = this; options = Utils._.extend({ method: 'up' }, options || {}); return new Utils.CustomEventEmitter(function(emitter) { self.getUndoneMigrations(function(err, migrations) { if (err) { emitter.emit('error', err) } else { var chainer = new Utils.QueryChainer() , from = migrations[0]; if (options.method === 'down') { migrations.reverse(); } if (migrations.length === 0) { self.options.logging("There are no pending migrations.", 'warn'); } else { self.options.logging("Running migrations..."); } migrations.forEach(function(migration) { var migrationTime; chainer.add(migration, 'execute', [options], { before: function(migration) { if (self.options.logging !== false) { self.options.logging(migration.filename) } migrationTime = process.hrtime(); }, after: function(migration) { migrationTime = process.hrtime(migrationTime); migrationTime = Math.round( (migrationTime[0] * 1000) + (migrationTime[1] / 1000000)); if (self.options.logging !== false) { self.options.logging('Completed in ' + migrationTime + 'ms'); } }, success: function(migration, callback) { if (options.method === 'down') { deleteUndoneMigration.call(self, from, migration, callback); } else { saveSuccessfulMigration.call(self, from, migration, callback); } } }); }); chainer .runSerially({ skipOnError: true }) .success(function() { emitter.emit('success', null) }) .error(function(err) { emitter.emit('error', err) }); } }) }).run() }; /** * Gets pending or undone migrations. * @memberof Migrator * @param {function} callback - callback on completion. */ Migrator.prototype.getUndoneMigrations = function(callback) { var self = this; var filterFrom = function(migrations, from, callback, options) { var result = migrations.filter(function(migration) { return migration.isAfter(from, options) }); callback && callback(null, result); }; var filterTo = function(migrations, to, callback, options) { var result = migrations.filter(function(migration) { return migration.isBefore(to, options) }); callback && callback(null, result); }; var migrationFiles = fs.readdirSync(this.options.path).filter(function(file) { return self.options.filesFilter.test(file); }); var migrations = migrationFiles.map(function(file) { return new Migration(self, self.options.path + '/' + file); }); migrations = migrations.sort(function(a,b){ return parseInt(a.filename.split('-')[0]) - parseInt(b.filename.split('-')[0]); }); if (this.options.from) { filterFrom(migrations, this.options.from, function(err, migrations) { if (self.options.to) { filterTo(migrations, self.options.to, callback); } else { callback && callback(null, migrations); } }) } else { getLastMigrationIdFromDatabase.call(this).success(function(lastMigrationId) { if (lastMigrationId) { filterFrom(migrations, lastMigrationId, function(err, migrations) { if (self.options.to) { filterTo(migrations, self.options.to, callback); } else { callback && callback(null, migrations); } }, { withoutEqual: true }) } else { if (self.options.to) { filterTo(migrations, self.options.to, callback); } else { callback && callback(null, migrations); } } }).error(function(err) { callback && callback(err, null); }) } }; /** * Creates or finds migration metadata in database. * @memberof Migrator * @param {object} syncOptions - synchronization options. * @returns {*} */ Migrator.prototype.findOrCreateSequelizeMetaDAO = function(syncOptions) { var self = this; return new Utils.CustomEventEmitter(function(emitter) { var storedDAO = self.sequelize.daoFactoryManager.getDAO('SequelizeMeta') , SequelizeMeta = storedDAO; if (!storedDAO) { SequelizeMeta = self.sequelize.define('SequelizeMeta', { from: DataTypes.STRING, to: DataTypes.STRING, data: DataTypes.BLOB }, { timestamps: false }); } // force sync when model has newly created or if syncOptions are passed if (!storedDAO || syncOptions) { SequelizeMeta .sync(syncOptions || {}) .success(function() { emitter.emit('success', SequelizeMeta) }) .error(function(err) { emitter.emit('error', err) }); } else { emitter.emit('success', SequelizeMeta); } }).run() }; /** * Explicitly executes one or multiple migrations. * @memberof Migrator * @param filename {String|Array} Absolute filename(s) of the migrations script * @param options {Object} Can contain three functions, before, after and success, which are executed before * or after each migration respectively, with one parameter, the migration. */ Migrator.prototype.exec = function(filename, options) { var self = this; return new Utils.CustomEventEmitter(function(emitter) { var chainer = new Utils.QueryChainer(); var addMigration = function(filename) { self.options.logging('Adding migration script at ' + filename); var migration = new Migration(self, filename); chainer.add(migration, 'execute', [{ method: 'up' }], { before: function(migration) { if (options && Utils._.isFunction(options.before)) { options.before.call(self, migration); } }, after: function(migration) { if (options && Utils._.isFunction(options.after)) { options.after.call(self, migration); } }, success: function(migration, callback) { if (options && Utils._.isFunction(options.success)) { options.success.call(self, migration); } callback(); } }) }; if (Utils._.isArray(filename)) { Utils._.each(filename, function(f) { addMigration(f); }) } else { addMigration(filename); } chainer .runSerially({ skipOnError: true }) .success(function() { emitter.emit('success', null) }) .error(function(err) { emitter.emit('error', err) }); }).run() }; // private /** * Gets the last migration from the database. * @private * @memberof Migrator * @type {getLastMigrationFromDatabase} */ var getLastMigrationFromDatabase = Migrator.prototype.getLastMigrationFromDatabase = function() { var self = this; return new Utils.CustomEventEmitter(function(emitter) { self .findOrCreateSequelizeMetaDAO() .success(function(SequelizeMeta) { SequelizeMeta .find({ order: 'id DESC' }) .success(function(meta) { emitter.emit('success', meta ? meta : null); }) .error(function(err) { emitter.emit('error', err); }) }) .error(function(err) { emitter.emit('error', err); }) }).run() }; /** * Gets the last migration from the database by specified id. * @private * @memberof Migrator * @type {getLastMigrationIdFromDatabase} */ var getLastMigrationIdFromDatabase = Migrator.prototype.getLastMigrationIdFromDatabase = function() { var self = this; return new Utils.CustomEventEmitter(function(emitter) { getLastMigrationFromDatabase .call(self) .success(function(meta) { emitter.emit('success', meta ? meta.to : null); }) .error(function(err) { emitter.emit('error', err); }) }).run() }; /** * Get a formated data string used for from/to in migration metadata. * @private * @memberof Migrator * @type {getFormattedDateString} */ var getFormattedDateString = Migrator.prototype.getFormattedDateString = function(s) { var result = null; try { result = s.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1, 6).join('-'); } catch(e) { return self.options.logging(s + ' is not a valid migration timestamp format! Use YYYYMMDDHHmmss!' + '\n ' + err.stack, 'error'); } return result }; /** * Convert a string to date using moment and provided format. * @private * @memberof Migrator * @type {stringToDate} */ var stringToDate = Migrator.prototype.stringToDate = function(s) { return moment(getFormattedDateString(s), "YYYYMMDDHHmmss"); }; /** * Called upon successful migration. * @private * @memberof Migrator * @type {saveSuccessfulMigration} */ var saveSuccessfulMigration = Migrator.prototype.saveSuccessfulMigration = function(from, to, callback) { var self = this; self.findOrCreateSequelizeMetaDAO().success(function(SequelizeMeta) { SequelizeMeta .create({ from: from.migrationId, to: to.migrationId, data: new Buffer(JSON.stringify(ctx.models), 'utf8') }) .success(callback); }); }; /** * Deletes undone migration * @private * @memberof Migrator * @type {deleteUndoneMigration} */ var deleteUndoneMigration = Migrator.prototype.deleteUndoneMigration = function(from, to, callback) { var self = this; self.findOrCreateSequelizeMetaDAO().success(function(SequelizeMeta) { SequelizeMeta .find({ where: { from: from.migrationId.toString(), to: to.migrationId.toString() } }) .success(function(meta) { meta.destroy().success(callback); }) }) }; return Migrator; }