lebretr/sequelize-oracle

View on GitHub
lib/associations/has-many.js

Summary

Maintainability
F
6 days
Test Coverage
var Utils      = require("./../utils")
  , DataTypes  = require('./../data-types')
  , Helpers    = require('./helpers')
  , _          = require('lodash')
  , Transaction = require('../transaction')

var HasManySingleLinked = require("./has-many-single-linked")
  , HasManyDoubleLinked  = require("./has-many-double-linked")

module.exports = (function() {
  var HasMany = function(source, target, options) {
    var self = this

    this.associationType = 'HasMany'
    this.source = source
    this.target = target
    this.targetAssociation = null
    this.options = options
    this.sequelize = source.daoFactoryManager.sequelize
    this.through = options.through
    this.isMultiAssociation = true
    this.isSelfAssociation = this.source === this.target
    this.doubleLinked = false
    this.as = this.options.as
    this.combinedTableName = Utils.combineTableNames(
      this.source.tableName,
      this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
    )

    /*
     * Map joinTableModel/Name to through for BC
     */
    if (this.through === undefined) {
      this.through = this.options.joinTableModel || this.options.joinTableName;

      /*
       * If both are undefined, see if useJunctionTable was false (for self associations) - else assume through to be true
       */
      if (this.through === undefined) {
        if (this.options.useJunctionTable === false) {
          this.through = null;
        } else {
          this.through = true;
        }
      }
    }

    /*
     * Determine associationAccessor, especially for include options to identify the correct model
     */

    this.associationAccessor = this.as
    if (!this.associationAccessor && (typeof this.through === "string" || Object(this.through) === this.through)) {
      this.associationAccessor = this.through.tableName || this.through
    }
    else if (!this.associationAccessor) {
      this.associationAccessor = this.combinedTableName
    }

    /*
     * If self association, this association is target association
     */
    if (this.isSelfAssociation) {
      this.targetAssociation = this
    }

    /*
     * Else find partner DAOFactory if present, to identify double linked association
     */
    else if (this.through) {
      _.each(this.target.associations, function (association, accessor) {
        if (self.source === association.target && association.associationType === 'HasMany') {
          var paired = false

          // If through is default, we determine pairing by the accesor value (i.e. DAOFactory's using as won't pair, but regular ones will)
          if (self.through === true && accessor === self.associationAccessor) {
            paired = true
          }
          // If through is not default, determine pairing by through value (model/string)
          if (self.through !== true && self.options.through === association.options.through) {
            paired = true
          }
          // If paired, set properties identifying both associations as double linked, and allow them to each eachtoerh
          if (paired) {
            self.doubleLinked = true
            association.doubleLinked = true

            self.targetAssociation = association
            association.targetAssociation = self
          }
        }
      })
    }

    /*
     * If we are double linked, and through is either default or a string, we create the through model and set it on both associations
     */
    if (this.doubleLinked) {
      if (this.through === true) {
        this.through = this.combinedTableName
      }
    }

    if (typeof this.through === "string") {
      this.through = this.sequelize.define(this.through, {}, _.extend(this.options, {
        tableName: this.through
      }))

      if (this.targetAssociation) {
        this.targetAssociation.through = this.through
      }
    }

    this.options.tableName = this.combinedName = (this.through === Object(this.through) ? this.through.tableName : this.through)

    if (this.as) {
      this.isAliased = true
    } else {
      this.as = (this.options.freezeAssociations ? this.target.tableName : Utils.pluralize(this.target.tableName, this.target.options.language));
    }

    this.accessors = {
      get: Utils._.camelize('get_' + this.as),
      set: Utils._.camelize('set_' + this.as),
      add: Utils._.camelize(Utils.singularize('add_' + this.as, this.target.options.language)),
      create: Utils._.camelize(Utils.singularize('create_' + this.as, this.target.options.language)),
      remove: Utils._.camelize(Utils.singularize('remove_' + this.as, this.target.options.language)),
      hasSingle: Utils._.camelize(Utils.singularize('has_' + this.as, this.target.options.language)),
      hasAll: Utils._.camelize('has_' + this.as)
    }
  }

  // the id is in the target table
  // or in an extra table which connects two tables
  HasMany.prototype.injectAttributes = function() {
    var doubleLinked      = this.doubleLinked
      , self              = this
      , primaryKeyDeleted = false

    this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored)

    // is there already a single sided association between the source and the target?
    // or is the association on the model itself?
    if ((this.isSelfAssociation && Object(this.through) === this.through) || doubleLinked) {
      // remove the obsolete association identifier from the source
      if (this.isSelfAssociation) {
        this.foreignIdentifier = Utils._.underscoredIf((this.options.as || this.target.tableName) + 'Id', this.options.underscored)
      } else {
        this.foreignIdentifier = this.targetAssociation.identifier
        this.targetAssociation.foreignIdentifier = this.identifier

        if (isForeignKeyDeletionAllowedFor.call(this, this.source, this.foreignIdentifier)) {
          delete this.source.rawAttributes[this.foreignIdentifier]
        }

        if (isForeignKeyDeletionAllowedFor.call(this, this.target, this.identifier)) {
          delete this.targetAssociation.source.rawAttributes[this.identifier]
        }
      }

      // remove any PKs previously defined by sequelize
      Utils._.each(this.through.attributes, function(dataTypeString, attributeName) {
        if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1 && self.through.rawAttributes[attributeName]._autoGenerated === true) {
          delete self.through.rawAttributes[attributeName]
          primaryKeyDeleted = true
        }
      })

      // define a new model, which connects the models
      var combinedTableAttributes = {}
      var sourceKeys = Object.keys(this.source.primaryKeys);
      var sourceKeyType = ((!this.source.hasPrimaryKeys || sourceKeys.length !== 1) ? DataTypes.INTEGER : this.source.rawAttributes[sourceKeys[0]].type)
      var targetKeys = Object.keys(this.target.primaryKeys);
      var targetKeyType = ((!this.target.hasPrimaryKeys || targetKeys.length !== 1) ? DataTypes.INTEGER : this.target.rawAttributes[targetKeys[0]].type)

      if (primaryKeyDeleted) {
        combinedTableAttributes[this.identifier] = {type: sourceKeyType, primaryKey: true}
        combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, primaryKey: true}
      } else {
        var uniqueKey = [this.through.tableName, this.identifier, this.foreignIdentifier, 'unique'].join('_')
        combinedTableAttributes[this.identifier] = {type: sourceKeyType, unique: uniqueKey}
        combinedTableAttributes[this.foreignIdentifier] = {type: targetKeyType, unique: uniqueKey}
      }

      this.through.rawAttributes = Utils._.merge(this.through.rawAttributes, combinedTableAttributes)
      this.through.init(this.through.daoFactoryManager)

      if (this.options.syncOnAssociation) {
        this.through.sync()
      }
    } else {
      var newAttributes = {}
        , sourceKeys = Object.keys(this.source.primaryKeys)
        , keyType = ((this.source.hasPrimaryKeys && sourceKeys.length === 1) ? this.source.rawAttributes[sourceKeys[0]].type : DataTypes.INTEGER)

      newAttributes[this.identifier] = { type: this.options.keyType || keyType}
      Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
      Utils._.defaults(this.target.rawAttributes, newAttributes)
    }

    // Sync attributes and setters/getters to DAO prototype
    this.target.refreshAttributes()
    this.source.refreshAttributes()

    return this
  }

  HasMany.prototype.injectGetter = function(obj) {
    var self = this

    obj[this.accessors.get] = function(options) {
      var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
      return new Class(self, this).injectGetter(options)
    }

    obj[this.accessors.hasAll] = function(objects, options) {
      var instance = this;
      var customEventEmitter = new Utils.CustomEventEmitter(function() {
        instance[self.accessors.get](options)
          .error(function(err) { customEventEmitter.emit('error', err) })
          .success(function(associatedObjects) {
            customEventEmitter.emit('success',
              Utils._.all(objects, function(o) {
                return Utils._.any(associatedObjects, function(associatedObject) {
                  return Utils._.all(associatedObject.identifiers, function(key, identifier) {
                    return o[identifier] == associatedObject[identifier];
                  });
                })
              })
            )
          })
      })
      return customEventEmitter.run()
    }

    obj[this.accessors.hasSingle] = function(o, options) {
      var instance           = this
      var customEventEmitter = new Utils.CustomEventEmitter(function() {
        instance[self.accessors.get](options)
        .error(function(err){ customEventEmitter.emit('error', err)})
        .success(function(associatedObjects) {
          customEventEmitter.emit('success',
            Utils._.any(associatedObjects, function(associatedObject) {
              return Utils._.all(associatedObject.identifiers, function(key, identifier) {
                return o[identifier] == associatedObject[identifier];
              });
            })
          )
        })
      })
      return customEventEmitter.run()
    }
    return this
  }

  HasMany.prototype.injectSetter = function(obj) {
    var self = this

    obj[this.accessors.set] = function(newAssociatedObjects, defaultAttributes) {
      if (newAssociatedObjects === null) {
        newAssociatedObjects = []
      }

      var instance = this

      // define the returned customEventEmitter, which will emit the success event once everything is done
      return new Utils.CustomEventEmitter(function(emitter) {
        instance[self.accessors.get]({
          transaction: (defaultAttributes || {}).transaction
        })
          .success(function(oldAssociatedObjects) {
            var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
            new Class(self, instance).injectSetter(emitter, oldAssociatedObjects, newAssociatedObjects, defaultAttributes)
          })
          .proxy(emitter, {events: ['error', 'sql']})
      }).run()
    }

    obj[this.accessors.add] = function(newAssociatedObject, additionalAttributes) {
      var instance = this
        , primaryKeys = Object.keys(newAssociatedObject.daoFactory.primaryKeys || {})
        , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
        , where = {}


      where[newAssociatedObject.daoFactory.tableName+'.'+primaryKey] = newAssociatedObject[primaryKey]
      return new Utils.CustomEventEmitter(function(emitter) {
        instance[self.accessors.get]({
          where: where,
          transaction: (additionalAttributes || {}).transaction
        })
          .proxy(emitter, {events: ['error', 'sql']})
          .success(function(currentAssociatedObjects) {
            if (currentAssociatedObjects.length === 0 || Object(self.through) === self.through) {
              var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
              new Class(self, instance).injectAdder(emitter, newAssociatedObject, additionalAttributes, !!currentAssociatedObjects.length)
            } else {
              emitter.emit('success', newAssociatedObject);
            }
          })
      }).run()
    }

    obj[this.accessors.remove] = function(oldAssociatedObject, options) {
      var instance = this
      return new Utils.CustomEventEmitter(function(emitter) {
        instance[self.accessors.get]({
          transaction: (options || {}).transaction
        }).success(function(currentAssociatedObjects) {
          var newAssociations = []
            , oldAssociations = []

          currentAssociatedObjects.forEach(function(association) {
            if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
              newAssociations.push(association)
            }
          })

          var tick = 0
          var next = function(err, i) {
            if (!!err || i >= oldAssociations.length) {
              return run(err)
            }

            oldAssociations[i].destroy().error(function(err) {
              next(err)
            })
            .success(function() {
              tick++
              next(null, tick)
            })
          }

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

            instance[self.accessors.set](newAssociations).proxy(emitter)
          }

          if (oldAssociations.length > 0) {
            next(null, tick)
          } else {
            run()
          }
        })
      }).run()
    }

    return this
  }

  HasMany.prototype.injectCreator = function(obj) {
    var self = this

    obj[this.accessors.create] = function(values, fieldsOrOptions) {
      var instance = this
        , options = {}

      if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
        options.transaction = fieldsOrOptions.transaction
        delete fieldsOrOptions.transaction
      }

      return new Utils.CustomEventEmitter(function(emitter) {
        // Create the related model instance
        self.target
          .create(values, fieldsOrOptions)
          .proxy(emitter, { events: ['error', 'sql'] })
          .success(function(newAssociatedObject) {
            instance[self.accessors.add](newAssociatedObject, options)
              .proxy(emitter)
          })
      }).run()
    }

    return this
  };

  /**
   * The method checks if it is ok to delete the previously defined foreign key.
   * This is done because we need to keep the foreign key if another association
   * is depending on it.
   *
   * @param  {DaoFactory}  daoFactory The source or target DaoFactory of this assocation
   * @param  {[type]}  identifier     The name of the foreign key identifier
   * @return {Boolean}                Whether or not the deletion of the foreign key is ok.
   */
  var isForeignKeyDeletionAllowedFor = function(daoFactory, identifier) {
    var isAllowed        = true
      , associationNames = Utils._.without(Object.keys(daoFactory.associations), this.associationAccessor)

    associationNames.forEach(function(associationName) {
      if (daoFactory.associations[associationName].identifier === identifier) {
        isAllowed = false
      }
    })

    return isAllowed
  }

  return HasMany
})()