lebretr/sequelize-oracle

View on GitHub
lib/dao-factory.js

Summary

Maintainability
F
3 wks
Test Coverage
var Utils       = require("./utils")
  , DAO         = require("./dao")
  , DataTypes   = require("./data-types")
  , Util        = require('util')
  , sql         = require('sql')
  , SqlString   = require('./sql-string')
  , Transaction = require('./transaction')
  , QueryTypes  = require('./query-types')

module.exports = (function() {
  var DAOFactory = function(name, attributes, options) {
    this.options = Utils._.extend({
      timestamps: true,
      createdAt: 'createdAt',
      updatedAt: 'updatedAt',
      deletedAt: 'deletedAt',
      instanceMethods: {},
      classMethods: {},
      validate: {},
      freezeTableName: false,
      freezeAssociations: false,
      underscored: false,
      syncOnAssociation: true,
      paranoid: false,
      whereCollection: null,
      schema: null,
      schemaDelimiter: '',
      language: 'en',
      defaultScope: null,
      scopes: null,
      hooks: {
        beforeCreate: [],
        afterCreate: []
      }
    }, options || {})

    // error check options
    Utils._.each(options.validate, function(validator, validatorType) {
      if (Utils._.contains(Utils._.keys(attributes), validatorType)) {
        throw new Error("A model validator function must not have the same name as a field. Model: " + name + ", field/validation name: " + validatorType)
      }

      if (!Utils._.isFunction(validator)) {
        throw new Error("Members of the validate option must be functions. Model: " + name + ", error with validate member " + validatorType)
      }
    })

    this.name = name

    if (!this.options.tableName) {
      this.tableName = this.options.freezeTableName ? name : Utils.pluralize(name, this.options.language)
    } else {
      this.tableName = this.options.tableName
    }

    attributes = replaceReferencesWithTableNames(attributes)
    this.options.hooks = this.replaceHookAliases(this.options.hooks)

    this.rawAttributes     = attributes
    this.daoFactoryManager = null // defined in init function
    this.associations      = {}
    this.scopeObj          = {}
  }

  Object.defineProperty(DAOFactory.prototype, 'attributes', {
    get: function() {
      return this.QueryGenerator.attributesToSQL(this.rawAttributes)
    }
  })

  Object.defineProperty(DAOFactory.prototype, 'sequelize', {
    get: function() { return this.daoFactoryManager.sequelize }
  })

  Object.defineProperty(DAOFactory.prototype, 'QueryInterface', {
    get: function() { return this.daoFactoryManager.sequelize.getQueryInterface() }
  })

  Object.defineProperty(DAOFactory.prototype, 'QueryGenerator', {
    get: function() { return this.QueryInterface.QueryGenerator }
  })

  // inject the node-sql methods to the dao factory in order to
  // receive the syntax sugar ...

 ;(function() {
    var instance = sql.define({ name: "dummy", columns: [] })

    for (var methodName in instance) {
     ;(function(methodName) {
        DAOFactory.prototype[methodName] = function() {
          var dataset = this.dataset()
            , result  = dataset[methodName].apply(dataset, arguments)
            , dialect = this.daoFactoryManager.sequelize.options.dialect
            , self    = this

          result.toSql = function() {
            var query = result.toQuery()
            return SqlString.format(query.text.replace(/(\$\d)/g, '?'), query.values, null, dialect) + ';'
          }

          result.exec = function(options) {
            options = Utils._.extend({
              transaction: null,
              type:        QueryTypes.SELECT
            }, options || {})

            return self.QueryInterface.queryAndEmit([result.toSql(), self, options], 'snafu')
          }

          return result
        }
      })(methodName)
    }
  })()

  DAOFactory.prototype.init = function(daoFactoryManager) {
    var self = this

    this.daoFactoryManager  = daoFactoryManager
    this.primaryKeys        = {}
    self.options.uniqueKeys = {}

    Utils._.each(this.rawAttributes, function(columnValues, columnName) {
      if (columnValues.hasOwnProperty('unique') && columnValues.unique !== true && columnValues.unique !== false) {
        var idxName = columnValues.unique
        if (typeof columnValues.unique === "object") {
          idxName = columnValues.unique.name
        }

        self.options.uniqueKeys[idxName] = self.options.uniqueKeys[idxName] || {fields: [], msg: null}
        self.options.uniqueKeys[idxName].fields.push(columnName)
        self.options.uniqueKeys[idxName].msg = self.options.uniqueKeys[idxName].msg || columnValues.unique.msg || null
      }
    })

    Utils._.each(this.attributes, function(dataTypeString, attributeName) {
      if (dataTypeString.indexOf('PRIMARY KEY') !== -1) {
        self.primaryKeys[attributeName] = dataTypeString
      }
    })

    this.primaryKeyAttributes = Object.keys(this.primaryKeys)
    this.primaryKeyCount = this.primaryKeyAttributes.length
    this.options.hasPrimaryKeys = this.hasPrimaryKeys = this.primaryKeyCount > 0

    if (typeof this.options.defaultScope === "object") {
      Utils.injectScope.call(this, this.options.defaultScope)
    }

    // DAO prototype
    // WTF ... ?
    this.DAO = function() {
      DAO.apply(this, arguments);
    }

    Util.inherits(this.DAO, DAO);

    this._timestampAttributes = {}
    if (this.options.timestamps) {
      if (this.options.createdAt) {
        this._timestampAttributes.createdAt = Utils._.underscoredIf(this.options.createdAt, this.options.underscored)
      }
      if (this.options.updatedAt) {
        this._timestampAttributes.updatedAt = Utils._.underscoredIf(this.options.updatedAt, this.options.underscored)
      }
      if (this.options.paranoid && this.options.deletedAt) {
        this._timestampAttributes.deletedAt = Utils._.underscoredIf(this.options.deletedAt, this.options.underscored)
      }

      this.DAO.prototype._readOnlyAttributes = Object.keys(this._timestampAttributes)
    }

    this.DAO.prototype._hasReadOnlyAttributes = this.DAO.prototype._readOnlyAttributes && this.DAO.prototype._readOnlyAttributes.length
    this.DAO.prototype._isReadOnlyAttribute = Utils._.memoize(function (key) {
      return self.DAO.prototype._hasReadOnlyAttributes && self.DAO.prototype._readOnlyAttributes.indexOf(key) !== -1
    })

    addDefaultAttributes.call(this)
    addOptionalClassMethods.call(this)
    findAutoIncrementField.call(this)

    this.DAO.prototype.rawAttributes = this.rawAttributes;

    this.DAO.prototype._hasPrimaryKeys = this.options.hasPrimaryKeys
    this.DAO.prototype._isPrimaryKey = Utils._.memoize(function (key) {
      return self.primaryKeyAttributes.indexOf(key) !== -1 && key !== 'id'
    })

    if (this.options.instanceMethods) {
      Utils._.each(this.options.instanceMethods, function(fct, name) {
        self.DAO.prototype[name] = fct
      })
    }

    this.refreshAttributes();

    this.DAO.prototype.booleanValues = []
    this.DAO.prototype.dateAttributes = []
    this.DAO.prototype.defaultValues = {}
    this.DAO.prototype.validators    = {}

    Utils._.each(this.rawAttributes, function (definition, name) {
      if (((definition === DataTypes.BOOLEAN) || (definition.type === DataTypes.BOOLEAN))) {
        self.DAO.prototype.booleanValues.push(name);
      }
      if (((definition === DataTypes.DATE) || (definition.type === DataTypes.DATE) || (definition.originalType === DataTypes.DATE))) {
        self.DAO.prototype.dateAttributes.push(name);
      }
      if (definition.hasOwnProperty('defaultValue')) {
        self.DAO.prototype.defaultValues[name] = Utils._.partial(
          Utils.toDefaultValue, definition.defaultValue)
      }

      if (definition.hasOwnProperty('validate')) {
        self.DAO.prototype.validators[name] = definition.validate;
      }
    })

    this.DAO.prototype._hasBooleanAttributes = !!this.DAO.prototype.booleanValues.length
    this.DAO.prototype._isBooleanAttribute = Utils._.memoize(function (key) {
      return self.DAO.prototype.booleanValues.indexOf(key) !== -1
    })

    this.DAO.prototype._hasDateAttributes = !!this.DAO.prototype.dateAttributes.length
    this.DAO.prototype._isDateAttribute = Utils._.memoize(function (key) {
      return self.DAO.prototype.dateAttributes.indexOf(key) !== -1
    })

    this.DAO.prototype.__factory        = this
    this.DAO.prototype.daoFactory       = this
    this.DAO.prototype.Model            = this
    this.DAO.prototype.hasDefaultValues = !Utils._.isEmpty(this.DAO.prototype.defaultValues)
    this.DAO.prototype.daoFactoryName   = this.name

    return this
  }

  DAOFactory.prototype.refreshAttributes = function() {
    var self = this
      , attributeManipulation = {};

    this.DAO.prototype._customGetters = {}
    this.DAO.prototype._customSetters = {}

    Utils._.each(['get', 'set'], function(type) {
      var opt   = type + 'terMethods'
        , funcs = Utils._.clone(Utils._.isObject(self.options[opt]) ? self.options[opt] : {})
        , _custom = type === 'get' ? self.DAO.prototype._customGetters : self.DAO.prototype._customSetters

      Utils._.each(funcs, function (method, attribute) {
        _custom[attribute] = method

        if (type === 'get') {
          funcs[attribute] = function() {
            return this.get(attribute)
          }
        }
        if (type === 'set') {
          funcs[attribute] = function(value) {
            return this.set(attribute, value)
          }
        }
      })

      Utils._.each(self.rawAttributes, function(options, attribute) {
        if (options.hasOwnProperty(type)) {
          _custom[attribute] = options[type]
        }

        if (type === 'get') {
          funcs[attribute] = function() {
            return this.get(attribute)
          }
        }
        if (type === 'set') {
          funcs[attribute] = function(value) {
            return this.set(attribute, value)
          }
        }
      })

      Utils._.each(funcs, function(fct, name) {
        if (!attributeManipulation[name]) {
          attributeManipulation[name] = {
            configurable: true
          }
        }
        attributeManipulation[name][type] = fct
      })
    })

    this.DAO.prototype._hasCustomGetters = Object.keys(this.DAO.prototype._customGetters).length
    this.DAO.prototype._hasCustomSetters = Object.keys(this.DAO.prototype._customSetters).length

    Object.defineProperties(this.DAO.prototype, attributeManipulation)
    this.DAO.prototype.attributes = Object.keys(this.DAO.prototype.rawAttributes)
    this.DAO.prototype._isAttribute = Utils._.memoize(function (key) {
      return self.DAO.prototype.attributes.indexOf(key) !== -1
    })
  }

  DAOFactory.prototype.sync = function(options) {
    options = Utils._.extend({}, this.options, options || {})

    var self = this

    return new Utils.CustomEventEmitter(function(emitter) {
      var doQuery = function() {
        self
          .QueryInterface
          .createTable(self.getTableName(), self.attributes, options)
          .proxy(emitter, {events: ['error', 'sql']})
          .success(function() { emitter.emit('success', self) })
      }

      if (options.force) {
        self
          .drop(options)
          .proxy(emitter, {events: ['error', 'sql']})
          .success(doQuery)
      } else {
        doQuery()
      }
    }).run()
  }

  DAOFactory.prototype.drop = function(options) {
    // Only Postgres' QueryGenerator.dropTableQuery() will add schema manually
    var isPostgres = this.options.dialect === "postgres" || (!!this.daoFactoryManager && this.daoFactoryManager.sequelize.options.dialect === "postgres")
     , tableName = isPostgres ? this.tableName : this.getTableName()

    return this.QueryInterface.dropTable(tableName, options)
  }

  DAOFactory.prototype.dropSchema = function(schema) {
    return this.QueryInterface.dropSchema(schema)
  }

  DAOFactory.prototype.schema = function(schema, options) {
    this.options.schema = schema

    if (!!options) {
      if (typeof options === "string") {
        this.options.schemaDelimiter = options
      } else {
        if (!!options.schemaDelimiter) {
          this.options.schemaDelimiter = options.schemaDelimiter
        }
      }
    }

    return this
  }

  DAOFactory.prototype.getTableName = function() {
    return this.QueryGenerator.addSchema(this)
  }

  DAOFactory.prototype.scope = function(option) {
    var self = Object.create(this)
      , type
      , options
      , merge
      , i
      , scope
      , scopeName
      , scopeOptions
      , argLength = arguments.length
      , lastArg = arguments[argLength-1]

    // Set defaults
    scopeOptions = (typeof lastArg === "object" && !Array.isArray(lastArg) ? lastArg : {}) || {} // <-- for no arguments
    scopeOptions.silent = (scopeOptions !== null && scopeOptions.hasOwnProperty('silent') ? scopeOptions.silent : true)

    // Clear out any predefined scopes...
    self.scopeObj = {}

    // Possible formats for option:
    // String of arguments: 'hello', 'world', 'etc'
    // Array: ['hello', 'world', 'etc']
    // Object: {merge: 'hello'}, {method: ['scopeName' [, args1, args2..]]}, {merge: true, method: ...}

    if (argLength < 1 || !option) {
      return self
    }

    for (i = 0; i < argLength; i++) {
      options = Array.isArray(arguments[i]) ? arguments[i] : [arguments[i]]

      options.forEach(function(o){
        type = typeof o
        scope = null
        merge = false
        scopeName = null

        if (type === "object") {
          // Right now we only support a merge functionality for objects
          if (!!o.merge) {
            merge = true
            scopeName = o.merge[0]
            if (Array.isArray(o.merge) && !!self.options.scopes[scopeName]) {
              scope = self.options.scopes[scopeName].apply(self, o.merge.splice(1))
            }
            else if (typeof o.merge === "string") {
              scopeName = o.merge
              scope = self.options.scopes[scopeName]
            }
          }

          if (!!o.method) {
            if (Array.isArray(o.method) && !!self.options.scopes[o.method[0]]) {
              scopeName = o.method[0]
              scope = self.options.scopes[scopeName].apply(self, o.method.splice(1))
              merge = !!o.merge
            }
            else if (!!self.options.scopes[o.method]) {
              scopeName = o.method
              scope = self.options.scopes[scopeName].apply(self)
            }
          } else {
            scopeName = o
            scope = self.options.scopes[scopeName]
          }
        } else {
          scopeName = o
          scope = self.options.scopes[scopeName]
        }

        if (!!scope) {
          Utils.injectScope.call(self, scope, merge)
        }
        else if (scopeOptions.silent !== true && !!scopeName) {
          throw new Error("Invalid scope " + scopeName + " called.")
        }
      })
    }

    return self
  }

  // alias for findAll
  DAOFactory.prototype.all = function(options, queryOptions) {
    return this.findAll(options, queryOptions)
  }

  DAOFactory.prototype.findAll = function(options, queryOptions) {
    var hasJoin = false
      , tableNames  = { }

    tableNames[this.tableName] = true

    options = optClone(options)
    if (typeof options === 'object') {
      if (options.hasOwnProperty('include') && options.include) {
        hasJoin = true

        validateIncludedElements.call(this, options, tableNames)
      }

      // whereCollection is used for non-primary key updates
      this.options.whereCollection = options.where || null
    }

    options = paranoidClause.call(this, options)

    return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({
      type:    QueryTypes.SELECT,
      hasJoin: hasJoin,
      tableNames: Object.keys(tableNames)
    }, queryOptions, { transaction: (options || {}).transaction }))
  }

  //right now, the caller (has-many-double-linked) is in charge of the where clause
  DAOFactory.prototype.findAllJoin = function(joinTableName, options, queryOptions) {
    var optcpy = Utils._.clone(options)
    optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteIdentifier(this.tableName)+".*"]

    // whereCollection is used for non-primary key updates
    this.options.whereCollection = optcpy.where || null;

    return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, Utils._.defaults({
      type: QueryTypes.SELECT
    }, queryOptions, { transaction: (options || {}).transaction }))
  }

 /**
  * Search for an instance.
  *
  * @param  {Object} options Options to describe the scope of the search.
  *   @param {Array} include A list of associations which shall get eagerly loaded. Supported is either { include: [ DaoFactory1, DaoFactory2, ...] } or { include: [ { daoFactory: DaoFactory1, as: 'Alias' } ] }.
  * @param  {Object} set the query options, e.g. raw, specifying that you want raw data instead of built DAOs
  * @return {Object}         A promise which fires `success`, `error`, `complete` and `sql`.
  */
  DAOFactory.prototype.find = function(options, queryOptions) {
    var hasJoin = false

    // no options defined?
    // return an emitter which emits null
    if ([null, undefined].indexOf(options) !== -1) {
      return new Utils.CustomEventEmitter(function(emitter) {
        setTimeout(function() { emitter.emit('success', null) }, 10)
      }).run()
    }

    var primaryKeys = this.primaryKeys
      , keys        = Object.keys(primaryKeys)
      , keysLength  = keys.length
      , tableNames  = { }

    tableNames[this.tableName] = true

    // options is not a hash but an id
    if (typeof options === 'number') {
      var oldOption = options
      options = { where: {} }
      if (keysLength === 1) {
        options.where[keys[0]] = oldOption
      } else {
        options.where.id = oldOption
      }
    } else if (Utils._.size(primaryKeys) && Utils.argsArePrimaryKeys(arguments, primaryKeys)) {
      var where = {}

      Utils._.each(arguments, function(arg, i) {
        var key = keys[i]
        where[key] = arg
      })

      options = { where: where }
    } else if (typeof options === 'string' && parseInt(options, 10).toString() === options) {
      var parsedId = parseInt(options, 10)

      if (!Utils._.isFinite(parsedId)) {
        throw new Error('Invalid argument to find(). Must be an id or an options object.')
      }

      options = { where: parsedId }
    } else if (typeof options === 'object') {
      options = Utils._.clone(options, function(thing) {
        if (Buffer.isBuffer(thing)) { return thing }
        return undefined;
      })

      if (options.hasOwnProperty('include') && options.include) {
        hasJoin = true

        validateIncludedElements.call(this, options, tableNames)
      }

      // whereCollection is used for non-primary key updates
      this.options.whereCollection = options.where || null
    } else if (typeof options === "string") {
      var where = {}

      if (this.primaryKeyCount === 1) {
        where[primaryKeys[keys[0]]] = options;
        options = where;
      } else if (this.primaryKeyCount < 1) {
        // Revert to default behavior which is {where: [int]}
        options = {where: parseInt(Number(options) || 0, 0)}
      }
    }

    options = paranoidClause.call(this, options)
    if (options.limit === undefined) {
      options.limit = 1
    }

    return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({
      plain: true,
      type: QueryTypes.SELECT,
      hasJoin: hasJoin,
      tableNames: Object.keys(tableNames)
    }, queryOptions, { transaction: (options || {}).transaction }))
  }

  DAOFactory.prototype.aggregate = function(field, aggregateFunction, options) {
    var tableField;

    if (field == '*') {
      tableField = field
    } else {
      tableField = this.QueryInterface.QueryGenerator.quoteIdentifier(field)
    }

    options = Utils._.extend({ attributes: [] }, options || {})
    options.attributes.push([aggregateFunction + '(' + tableField + ')', aggregateFunction])

    if (!options.dataType) {
      if (this.rawAttributes[field]) {
        options.dataType = this.rawAttributes[field]
      } else {
        // Use FLOAT as fallback
        options.dataType = DataTypes.FLOAT
      }
    }

    options = paranoidClause.call(this, options)

    return this.QueryInterface.rawSelect(this.getTableName(), options, aggregateFunction)
  }

  DAOFactory.prototype.count = function(options) {
    options = Utils._.clone(options || {})

    return new Utils.CustomEventEmitter(function (emitter) {
      var col = this.sequelize.col('*')
      if (options.include) {
        col = this.sequelize.col(this.tableName+'.'+(this.primaryKeyAttributes[0] || 'id'))
      }

      options.attributes = [
        [this.sequelize.fn('COUNT', col), 'count']
      ]

      if(options.distinct) {
        options.attributes = [
            [this.sequelize.fn('COUNT', this.sequelize.fn('DISTINCT', col) ), 'count']
        ]
      }

      options.includeIgnoreAttributes = false
      options.limit = null

      this.find(options, {raw: true, transaction: options.transaction}).proxy(emitter, {events: ['sql', 'error']}).success(function (result) {
        var count = (result && (result.count || result.COUNT)) ? parseInt(result.count || result.COUNT, 10) : 0
        emitter.emit('success', count)
      })
    }.bind(this)).run()
  }

  DAOFactory.prototype.findAndCountAll = function(findOptions, queryOptions) {
    var self  = this
      // no limit, offset, order, attributes for the options given to count()
      , countOptions = Utils._.omit(findOptions ? Utils._.merge({}, findOptions) : {}, ['offset', 'limit', 'order', 'attributes'])

    return new Utils.CustomEventEmitter(function (emitter) {
      var emit = {
        okay : function(count, results) {     // emit success
          emitter.emit('success', {
            count: count || 0,
            rows : (results && Array.isArray(results) ? results : [])
          })
        }
      }

      self.count(countOptions)
        .proxy(emitter, {events: ['sql', 'error']})
        .success(function(count) {
          if (count === 0) {
            return emit.okay(count) // no records, no need for another query
          }

          self.findAll(findOptions, queryOptions)
            .proxy(emitter, {events: ['sql', 'error']})
            .success(function(results) {
              emit.okay(count, results)
            })
        })

    }).run()
  }

  DAOFactory.prototype.max = function(field, options) {
    return this.aggregate(field, 'max', options)
  }

  DAOFactory.prototype.min = function(field, options) {
    return this.aggregate(field, 'min', options)
  }

  DAOFactory.prototype.sum = function(field, options) {
    return this.aggregate(field, 'sum', options)
  }

  DAOFactory.prototype.build = function(values, options) {
    options = options || { isNewRecord: true, isDirty: true }

    if (options.hasOwnProperty('include') && options.include && !options.includeValidated) {
      validateIncludedElements.call(this, options)
    }

    return new this.DAO(values, options)
  }

  DAOFactory.prototype.create = function(values, fieldsOrOptions) {
    Utils.validateParameter(values, Object, { optional: true })
    Utils.validateParameter(fieldsOrOptions, Object, { deprecated: Array, optional: true, index: 2, method: 'DAOFactory#create' })

    if (fieldsOrOptions instanceof Array) {
      fieldsOrOptions = { fields: fieldsOrOptions }
    }

    fieldsOrOptions = Utils._.extend({
      transaction: null
    }, fieldsOrOptions || {})

    return this.build(values).save(fieldsOrOptions)
  }

  DAOFactory.prototype.findOrInitialize = DAOFactory.prototype.findOrBuild = function (params, defaults, options) {
    defaults = defaults || {}
    options  = options  || {}

    var self          = this
      , defaultKeys   = Object.keys(defaults)
      , defaultLength = defaultKeys.length

    if (!options.transaction && defaults.transaction && (defaults.transaction instanceof Transaction)) {
      options.transaction = defaults.transaction
      delete defaults.transaction
    }

    return new Utils.CustomEventEmitter(function (emitter) {
      self.find({
        where: params
      }, options).success(function (instance) {
        if (instance === null) {
          var i = 0

          for (i = 0; i < defaultLength; i++) {
            params[defaultKeys[i]] = defaults[defaultKeys[i]]
          }

          var build = self.build(params)

          build.hookValidate({skip: Object.keys(params)}).success(function (instance) {
            emitter.emit('success', build, true)
          })
          .error(function (error) {
            emitter.emit('error', error)
          })
        } else {
          emitter.emit('success', instance, false)
        }
      }).error(function (error) {
        emitter.emit('error', error)
      })
    }).run()
  }

  DAOFactory.prototype.findOrCreate = function (where, defaults, options) {
    var self   = this
      , values = {}

    options = Utils._.extend({
      transaction: null
    }, options || {})

    if (!(where instanceof Utils.or) && !(where instanceof Utils.and) && !Array.isArray(where)) {
      for (var attrname in where) {
        values[attrname] = where[attrname]
      }
    }

    return new Utils.CustomEventEmitter(function (emitter) {
      self.find({
        where: where
      }, {
        transaction: options.transaction
      }).success(function (instance) {
        if (instance === null) {
          for (var attrname in defaults) {
            values[attrname] = defaults[attrname]
          }

          self
            .create(values, options)
            .success(function (instance) {
              emitter.emit('success', instance, true)
            })
            .error( function (error) {
              emitter.emit('error', error)
            })
        } else {
          emitter.emit('success', instance, false)
        }
      }).error(function (error) {
        emitter.emit('error', error)
      });
    }).run()
  }

  /**
   * Create and insert multiple instances
   *
   * @param  {Array} records List of objects (key/value pairs) to create instances from
   * @param  {Array} fields Fields to insert (defaults to all fields)
   * @return {Object}       A promise which fires `success`, `error`, `complete` and `sql`.
   *
   * Note: the `success` handler is not passed any arguments. To obtain DAOs for
   * the newly created values, you will need to query for them again. This is
   * because MySQL and SQLite do not make it easy to obtain back automatically
   * generated IDs and other default values in a way that can be mapped to
   * multiple records
   */
  DAOFactory.prototype.bulkCreate = function(records, fieldsOrOptions, options) {
    Utils.validateParameter(fieldsOrOptions, Object, { deprecated: Array, optional: true, index: 2, method: 'DAOFactory#bulkCreate' })
    Utils.validateParameter(options, 'undefined', { deprecated: Object, optional: true, index: 3, method: 'DAOFactory#bulkCreate' })

    if (!records.length) {
      return new Utils.CustomEventEmitter(function(emitter) {
        emitter.emit('success', [])
      }).run();
    }

    options = Utils._.extend({
      validate: false,
      hooks: false,
      ignoreDuplicates: false
    }, options || {})

    if (fieldsOrOptions instanceof Array) {
      options.fields = fieldsOrOptions
    } else {
      options.fields = options.fields || []
      options = Utils._.extend(options, fieldsOrOptions)
    }

    if(this.daoFactoryManager.sequelize.options.dialect === 'postgres' && options.ignoreDuplicates ) {
      return new Utils.CustomEventEmitter(function(emitter) {
        emitter.emit('error', new Error('Postgres does not support the \'ignoreDuplicates\' option.'))
      }).run();
    }

    var self          = this
      , updatedAtAttr = this._timestampAttributes.updatedAt
      , createdAtAttr = this._timestampAttributes.createdAt
      , errors        = []
      , daos          = records.map(function(v) { return self.build(v) })

    return new Utils.CustomEventEmitter(function(emitter) {
      var done = function() {
        self.runHooks('afterBulkCreate', daos, options.fields, function(err, newRecords, newFields) {
          if (!!err) {
            return emitter.emit('error', err)
          }

          daos           = newRecords || daos
          options.fields = newFields  || options.fields

          emitter.emit('success', daos, options.fields)
        })
      }

      var next = function() {
        if (options.hooks === false) {
          return runQuery()
        }

        var i = 0
        var iterate = function(i) {
          self.runHooks('beforeCreate', daos[i], function(err, newValues) {
            if (!!err) {
              return emitter.emit('error', err)
            }

            daos[i] = newValues || daos[i]
            daos[i].save({ transaction: options.transaction }).error(function(err) {
              emitter.emit('error', err)
            }).success(function() {
              self.runHooks('afterCreate', daos[i], function(err, newValues) {
                if (!!err) {
                  return emitter.emit('error', err)
                }

                daos[i] = newValues || daos[i]
                i++
                if (i >= daos.length) {
                  return done()
                }

                iterate(i)
              })
            })
          })
        }

        iterate(i)
      }

      var runQuery = function() {
        // we will re-create from DAOs, which may have set up default attributes
        records = []

        daos.forEach(function(dao) {
          var values = options.fields.length > 0 ? {} : dao.dataValues

          options.fields.forEach(function(field) {
            values[field] = dao.dataValues[field]
          })

          if (createdAtAttr && !values[createdAtAttr]) {
            values[createdAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
          }

          if (updatedAtAttr && !values[updatedAtAttr]) {
            values[updatedAtAttr] = Utils.now(self.daoFactoryManager.sequelize.options.dialect)
          }

          records.push(values)
        })

        self.QueryInterface.bulkInsert(self.tableName, records, options)
        .on('sql', function(sql) {
          emitter.emit('sql', sql)
        })
        .error(function(err) {
          emitter.emit('error', err)
        }).success(function(rows) {
          done()
        })
      }

      self.runHooks('beforeBulkCreate', daos, options.fields, function(err, newRecords, newFields) {
        if (!!err) {
          return emitter.emit('error', err)
        }

        daos           = newRecords || daos
        options.fields = newFields  || options.fields

        if (options.validate === true) {
          if (options.fields.length) {
            var skippedFields = Utils._.difference(Object.keys(self.attributes), options.fields);
          }

          if (options.hooks === true) {
            var iterate = function(i) {

              daos[i].hookValidate({skip: skippedFields}).error(function(err) {
                errors[errors.length] = {record: v, errors: err}
                i++
                if (i > daos.length) {
                  if (errors.length > 0) {
                    return emitter.emit('error', errors)
                  }

                  return next()
                }
                iterate(i)
              })
            }
          } else {
            daos.forEach(function(v) {
              var valid = v.validate({skip: skippedFields})
              if (valid !== null) {
                errors[errors.length] = {record: v, errors: valid}
              }
            })

            if (errors.length > 0) {
              return emitter.emit('error', errors)
            }

            next()
          }
        } else {
          next()
        }
      })
    }).run()
  }

  /**
   * Delete multiple instances
   *
   * @param  {Object} where   Options to describe the scope of the search.
   * @param  {Object} options Possible options are:
                              - hooks: If set to true, destroy will find all records within the where parameter and will execute before/afterDestroy hooks on each row
                              - limit: How many rows to delete
                              - truncate: If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored
   * @return {Object}         A promise which fires `success`, `error`, `complete` and `sql`.
   */
  DAOFactory.prototype.destroy = function(where, options) {
    options = options || {}
    options.force = options.force === undefined ? false : Boolean(options.force)
    options.type = QueryTypes.BULKDELETE

    var self  = this
      , query = null
      , args  = []

    return new Utils.CustomEventEmitter(function(emitter) {
      self.runHooks(self.options.hooks.beforeBulkDestroy, where, function(err, newWhere) {
        if (!!err) {
          return emitter.emit('error', err)
        }

        where = newWhere || where

        if (self._timestampAttributes.deletedAt && options.force === false) {
          var attrValueHash = {}
          attrValueHash[self._timestampAttributes.deletedAt] = Utils.now()
          query = 'bulkUpdate'
          args  = [self.tableName, attrValueHash, where, options]
        } else {
          query = 'bulkDelete'
          args  = [self.tableName, where, options]
        }

        var runQuery = function(err, records) {
          if (!!err) {
            return emitter.emit('error', err)
          }

          query = self.QueryInterface[query].apply(self.QueryInterface, args)
          query.on('sql', function(sql) {
            emitter.emit('sql', sql)
          })
          .error(function(err) {
            emitter.emit('error', err)
          })
          .success(function(results) {
            var finished = function(err) {
              if (!!err) {
                return emitter.emit('error', err)
              }

              self.runHooks(self.options.hooks.afterBulkDestroy, where, function(err) {
                if (!!err) {
                  return emitter.emit('error', err)
                }

                emitter.emit('success', results)
              })
            }

            if (options && options.hooks === true) {
              var tick = 0
              var next = function(i) {
                if (i >= records.length) {
                  return finished();
                }

                self.runHooks(self.options.hooks.afterDestroy, records[i], function(err, newValues) {
                  if (!!err) {
                    return finished(err)
                  }

                  records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
                  tick++

                  next(tick)
                })
              }

              next(tick)
            } else {
              finished()
            }
          })
        }

        if (options && options.hooks === true) {
          var tick = 0
          self.all({where: where}).error(function(err) { emitter.emit('error', err) })
          .success(function(records) {
            var next = function(i) {
              if (i >= records.length) {
                return runQuery(null, records)
              }

              self.runHooks(self.options.hooks.beforeDestroy, records[i], function(err, newValues) {
                if (!!err) {
                  return runQuery(err)
                }

                records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
                tick++

                next(tick)
              })
            }

            next(tick)
          })
          //
        } else {
          runQuery()
        }
      })
    }).run()
  }

  /**
   * Update multiple instances
   *
   * @param  {Object} attrValueHash A hash of fields to change and their new values
   * @param  {Object} where         Options to describe the scope of the search.
   * @return {Object}               A promise which fires `success`, `error`, `complete` and `sql`.
   */
  DAOFactory.prototype.update = function(attrValueHash, where, options) {
    var self  = this
      , query = null
      , tick  = 0

    options = options || {}
    options.validate  = options.validate === undefined ? true : Boolean(options.validate)
    options.hooks     = options.hooks === undefined ? false : Boolean(options.hooks)
    options.type      = QueryTypes.BULKUPDATE

    if (self._timestampAttributes.updatedAt) {
      attrValueHash[self._timestampAttributes.updatedAt] = Utils.now()
    }

    return new Utils.CustomEventEmitter(function(emitter) {
      var runSave = function() {
        self.runHooks(self.options.hooks.beforeBulkUpdate, attrValueHash, where, function(err, attributes, _where) {
          if (!!err) {
            return emitter.emit('error', err)
          }

          where         = _where || where
          attrValueHash = attributes || attrValueHash

          var runQuery = function(err, records) {
            if (!!err) {
              return emitter.emit('error', err)
            }

            query = self.QueryInterface.bulkUpdate(self.tableName, attrValueHash, where, options)
            query.on('sql', function(sql) {
              emitter.emit('sql', sql)
            })
            .error(function(err) {
              emitter.emit('error', err)
            })
            .success(function(results) {
              var finished = function(err, records) {
                if (!!err) {
                  return emitter.emit('error', err)
                }

                self.runHooks(self.options.hooks.afterBulkUpdate, attrValueHash, where, function(err) {
                  if (!!err) {
                    return emitter.emit('error', err)
                  }

                  emitter.emit('success', records)
                })
              }

              if (options && options.hooks === true && !!records && records.length > 0) {
                var tick = 0
                var next = function(i) {
                  if (i >= records.length) {
                    return finished(null, records)
                  }

                  self.runHooks(self.options.hooks.afterUpdate, records[i], function(err, newValues) {
                    if (!!err) {
                      return finished(err)
                    }

                    records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
                    tick++

                    next(tick)
                  })
                }

                next(tick)
              } else {
                finished(null, results)
              }
            })
          }

          if (options.hooks === true) {
            self.all({where: where}).error(function(err) { emitter.emit('error', err) })
            .success(function(records) {
              if (records === null || records.length < 1) {
                return runQuery(null)
              }

              var next = function(i) {
                if (i >= records.length) {
                  return runQuery(null, records)
                }

                self.runHooks(self.options.hooks.beforeUpdate, records[i], function(err, newValues) {
                  if (!!err) {
                    return runQuery(err)
                  }

                  records[i].dataValues = !!newValues ? newValues.dataValues : records[i].dataValues
                  tick++

                  next(tick)
                })
              }

              next(tick)
            })
          } else {
            runQuery()
          }
        })
      }

      if (options.validate === true) {
        var build = self.build(attrValueHash)
        build.set(self._timestampAttributes.updatedAt, attrValueHash[self._timestampAttributes.updatedAt], { raw: true })

        // We want to skip validations for all other fields
        var updatedFields = Object.keys(attrValueHash)
        var skippedFields = Utils._.difference(Object.keys(self.attributes), updatedFields)

        build.hookValidate({skip: skippedFields}).error(function(err) {
          emitter.emit('error', err)
        }).success(function(attributes) {
          if (!!attributes && !!attributes.dataValues) {
            attrValueHash = Utils._.pick.apply(Utils._, [].concat(attributes.dataValues).concat(Object.keys(attrValueHash)))
          }

          runSave()
        })
      } else {
        runSave()
      }
    }).run()
  }

  DAOFactory.prototype.describe = function(schema) {
    return this.QueryInterface.describeTable(this.tableName, schema || this.options.schema || undefined)
  }

  DAOFactory.prototype.dataset = function() {
    if (!this.__sql) {
      this.__setSqlDialect()
    }

    var instance   = this.__sql.define({ name: this.tableName, columns: [] })
      , attributes = this.attributes

    Object.keys(attributes).forEach(function(key) {
      instance.addColumn(key, attributes[key])
    })

    return instance
  }

  DAOFactory.prototype.__setSqlDialect = function() {
    var dialect = this.daoFactoryManager.sequelize.options.dialect
    this.__sql = sql.setDialect(dialect === 'mariadb' ? 'mysql' : dialect)
  }

  // private

  var paranoidClause = function(options) {
    if (this.options.paranoid === true) {
      options = options || {}
      options.where = options.where || {}

      var deletedAtCol = this._timestampAttributes.deletedAt
        , quoteIdentifiedDeletedAtCol = this.QueryInterface.quoteIdentifier(deletedAtCol)

      // Don't overwrite our explicit deletedAt search value if we provide one
      if (!!options.where[deletedAtCol]) {
        return options
      }

      if(this.tableName) {
        quoteIdentifiedDeletedAtCol = this.QueryInterface.quoteIdentifier(this.tableName) + '.' + quoteIdentifiedDeletedAtCol
      }

      if (typeof options.where === "string") {
        options.where += ' AND ' + quoteIdentifiedDeletedAtCol + ' IS NULL '
      }
      else if (Array.isArray(options.where)) {

        // Don't overwrite our explicit deletedAt search value if we provide one
        if(options.where[0].indexOf(deletedAtCol) !== -1) {
          return options
        }
        options.where[0] += ' AND ' + quoteIdentifiedDeletedAtCol + ' IS NULL '
      } else {
        options.where[deletedAtCol] = null
      }
    }

    return options
  }

  var addOptionalClassMethods = function() {
    var self = this
    Utils._.each(this.options.classMethods || {}, function(fct, name) { self[name] = fct })
  }

  var addDefaultAttributes = function() {
    var self              = this
      , tail = {}
      , head = {
        id: {
          type: DataTypes.INTEGER,
          allowNull: false,
          primaryKey: true,
          autoIncrement: true,
          _autoGenerated: true
        }
      }

    if (this.hasPrimaryKeys) {
      head = {}
    }

    if (this._timestampAttributes.createdAt) {
      tail[this._timestampAttributes.createdAt] = {type: DataTypes.DATE, allowNull: false}
    }
    if (this._timestampAttributes.updatedAt) {
      tail[this._timestampAttributes.updatedAt] = {type: DataTypes.DATE, allowNull: false}
    }
    if (this._timestampAttributes.deletedAt) {
      tail[this._timestampAttributes.deletedAt] = {type: DataTypes.DATE}
    }

    var existingAttributes = Utils._.clone(self.rawAttributes)
    self.rawAttributes = {}

    Utils._.each(head, function(value, attr) {
      self.rawAttributes[attr] = value
    })

    Utils._.each(existingAttributes, function(value, attr) {
      self.rawAttributes[attr] = value
    })

    Utils._.each(tail, function(value, attr) {
      if (Utils._.isUndefined(self.rawAttributes[attr])) {
        self.rawAttributes[attr] = value
      }
    })
  }

  var findAutoIncrementField = function() {
    var fields = this.QueryGenerator.findAutoIncrementField(this)

    this.autoIncrementField = null

    fields.forEach(function(field) {
      if (this.autoIncrementField) {
        throw new Error('Invalid DAO definition. Only one autoincrement field allowed.')
      } else {
        this.autoIncrementField = field
      }
    }.bind(this))
  }

  var validateIncludedElements = function(options, tableNames) {
    tableNames = tableNames || {}
    options.includeNames = []
    options.includeMap = {}
    options.hasSingleAssociation = false
    options.hasMultiAssociation = false

    // if include is not an array, wrap in an array
    if (!Array.isArray(options.include)) {
      options.include = [options.include]
    }

    // convert all included elements to { daoFactory: Model } form
    var includes = options.include = options.include.map(function(include) {
      if (include instanceof DAOFactory) {
        return { daoFactory: include }
      } else if (typeof include !== 'object') {
        throw new Error('Include unexpected. Element has to be either an instance of DAOFactory or an object.')
      } else if (include.hasOwnProperty('model')) {
        include.daoFactory = include.model
        delete include.model
      }
      return include
    })

    // validate all included elements
    for (var index = 0; index < includes.length; index++) {
      var include = includes[index]

      if (include.all) {
        includes.splice(index, 1)
        index--

        validateIncludedAllElement.call(this, includes, include)
        continue
      }

      include = includes[index] = validateIncludedElement.call(this, include, tableNames)

      include.parent = options
      // associations that are required or have a required child as is not a ?:M association are candidates for the subquery
      include.subQuery = !include.association.isMultiAssociation && (include.hasIncludeRequired || include.required)
      include.hasParentWhere = options.hasParentWhere || !!options.where
      include.hasParentRequired = options.hasParentRequired || !!options.required

      options.includeMap[include.as] = include
      options.includeNames.push(include.as)
      options.includeNames.push(include.as.substr(0,1).toLowerCase() + include.as.substr(1))

      if (include.association.isMultiAssociation || include.hasMultiAssociation) options.hasMultiAssociation = true
      if (include.association.isSingleAssociation || include.hasSingleAssociation) options.hasSingleAssociation = true

      options.hasIncludeWhere = options.hasIncludeWhere || include.hasIncludeWhere || !!include.where
      options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required
    }
  }

  var validateIncludedElement = function(include, tableNames) {
    if (!include.hasOwnProperty('daoFactory')) {
      throw new Error('Include malformed. Expected attributes: daoFactory, as!')
    }

    tableNames[include.daoFactory.tableName] = true

    if (include.hasOwnProperty('attributes')) {
      var primaryKeys;
      if (include.daoFactory.hasPrimaryKeys) {
        primaryKeys = []
        for (var field_name in include.daoFactory.primaryKeys) {
          primaryKeys.push(field_name)
        }
      } else {
        primaryKeys = ['id']
      }
      include.attributes = include.attributes.concat(primaryKeys)
    } else {
      include.attributes = Object.keys(include.daoFactory.attributes)
    }

    // pseudo include just needed the attribute logic, return
    if (include._pseudo) return include

    // check if the current daoFactory is actually associated with the passed daoFactory - or it's a pseudo include
    var association = this.getAssociation(include.daoFactory, include.as)
    if (association) {
      include.association = association
      include.as = association.as

      // If through, we create a pseudo child include, to ease our parsing later on
      if (Object(include.association.through) === include.association.through) {
        if (!include.include) include.include = []
        var through = include.association.through

        include.through = {
          daoFactory: through,
          as: Utils.singularize(through.tableName, through.options.language),
          association: {
            isSingleAssociation: true
          },
          _pseudo: true
        }

        include.include.push(include.through)
        tableNames[through.tableName] = true
      }

      if (include.required === undefined) {
        include.required = !!include.where
      }

      // Validate child includes
      if (include.hasOwnProperty('include')) {
        validateIncludedElements.call(include.daoFactory, include, tableNames)
      }

      return include
    } else {
      var msg = include.daoFactory.name

      if (include.as) {
        msg += " (" + include.as + ")"
      }

      msg += " is not associated to " + this.name + "!"

      throw new Error(msg)
    }
  }

  var validateIncludedAllElement = function(includes, include) {
    // check 'all' attribute provided is valid
    var all = include.all
    delete include.all

    if (all !== true) {
      if (!Array.isArray(all)) {
        all = [all]
      }

      var validTypes = {
        BelongsTo: true,
        HasOne: true,
        HasMany: true,
        One: ['BelongsTo', 'HasOne'],
        Has: ['HasOne', 'HasMany'],
        Many: ['HasMany']
      }

      for (var i = 0; i < all.length; i++) {
        var type = all[i]
        if (type == 'All') {
          all = true
          break
        }

        var types = validTypes[type]
        if (!types) {
          throw new Error('include all \'' + type + '\' is not valid - must be BelongsTo, HasOne, HasMany, One, Has, Many or All')
        }

        if (types !== true) {
          // replace type placeholder e.g. 'One' with it's constituent types e.g. 'HasOne', 'BelongsTo'
          all.splice(i, 1)
          i--
          for (var j = 0; j < types.length; j++) {
            if (all.indexOf(types[j]) == -1) {
              all.unshift(types[j])
              i++
            }
          }
        }
      }
    }

    // add all associations of types specified to includes
    var nested = include.nested
    if (nested) {
      delete include.nested

      if (!include.include) {
        include.include = []
      } else if (!Array.isArray(include.include)) {
        include.include = [include.include]
      }
    }

    var used = []
    ;(function addAllIncludes(parent, includes) {
      used.push(parent)
      Utils._.forEach(parent.associations, function(association) {
        if (all !== true && all.indexOf(association.associationType) == -1) {
          return
        }

        // check if model already included, and skip if so
        var model = association.target
        var as = association.options.as
        if (Utils._.find(includes, {daoFactory: model, as: as})) {
          return
        }

        // skip if recursing over a model already nested
        if (nested && used.indexOf(model) != -1) {
          return
        }

        // include this model
        var thisInclude = optClone(include)
        thisInclude.daoFactory = model
        if (as) {
          thisInclude.as = as
        }
        includes.push(thisInclude)

        // run recursively if nested
        if (nested) {
          addAllIncludes(model, thisInclude.include)
        }
      })
      used.pop()
    })(this, includes)
  }

  var replaceReferencesWithTableNames = function(attributes) {
    Object.keys(attributes).forEach(function(attrName) {
      if (attributes[attrName].references instanceof DAOFactory) {
        attributes[attrName].references = attributes[attrName].references.tableName
      }
    })

    return attributes
  }

  var optClone = function (options) {
    return Utils._.cloneDeep(options, function (elem) {
      // The DAOFactories used for include are pass by ref, so don't clone them.
      if (elem instanceof DAOFactory || elem instanceof Utils.col || elem instanceof Utils.literal || elem instanceof Utils.cast || elem instanceof Utils.fn || elem instanceof Utils.and || elem instanceof Utils.or) {
        return elem
      }
      // Unfortunately, lodash.cloneDeep doesn't preserve Buffer.isBuffer, which we have to rely on for binary data
      if (Buffer.isBuffer(elem)) { return elem; }

      // Otherwise return undefined, meaning, 'handle this lodash'
      return undefined
    })
  }

  Utils._.extend(DAOFactory.prototype, require("./associations/mixin"))
  Utils._.extend(DAOFactory.prototype, require(__dirname + '/hooks'))

  return DAOFactory
})()