sequelize-cmd
Version:
Extended cli for sequelize.
394 lines (336 loc) • 14.5 kB
JavaScript
;
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;
}