Sequelize

    const fs     = require("fs")
        , path   = require("path")
        , moment = require("moment")
    
    var Utils          = require("./utils")
      , Migration      = require("./migration")
      , DataTypes      = require("./data-types")
    
    module.exports = (function() {
      var Migrator = function(sequelize, options) {
        this.sequelize = sequelize
        this.options   = Utils._.extend({
          path: __dirname + '/../migrations',
          from: null,
          to: null,
          logging: console.log
        }, 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) }
        }
      }
    
      Object.defineProperty(Migrator.prototype, "queryInterface", {
        get: function() {
          return this.sequelize.getQueryInterface()
        }
      })
    
      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()
                from = migrations[0]
              }
    
              migrations.forEach(function(migration) {
                chainer.add(migration, 'execute', [options], {
                  before: function(migration) {
                    if(self.options.logging !== false)
                      self.options.logging('Executing migration: ' + migration.filename)
                  },
                  after: function(migration) {
                    if(self.options.logging !== false)
                      self.options.logging('Executed migration: ' + migration.filename)
                  },
                  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()
      }
    
      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)
    
        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)
          })
        }
      }
    
      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
            }, {
              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()
      }
    
      // private
    
      var 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(err) })
        }).run()
      }
    
      var 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()
      }
    
      var 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) {
          throw new Error(s + ' is no valid migration timestamp format! Use YYYYMMDDHHmmss!')
        }
    
        return result
      }
    
      var stringToDate = function(s) {
        return moment(getFormattedDateString(s), "YYYYMMDDHHmmss")
      }
    
      var saveSuccessfulMigration = function(from, to, callback) {
        var self = this
    
        self.findOrCreateSequelizeMetaDAO().success(function(SequelizeMeta) {
          SequelizeMeta
            .create({ from: from.migrationId, to: to.migrationId })
            .success(callback)
        })
      }
    
      var deleteUndoneMigration = function(from, to, callback) {
        var self = this
    
        self.findOrCreateSequelizeMetaDAO().success(function(SequelizeMeta) {
          SequelizeMeta
            .find({ from: from.migrationId, to: to.migrationId })
            .success(function(meta) {
              meta.destroy().success(callback)
            })
        })
      }
    
      return Migrator
    })()