wbyoung/azul

View on GitHub
lib/relations/belongs_to_config.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict';

var _ = require('lodash');
var util = require('util');
var property = require('corazon/property');
var inflection = require('../util/inflection');
var Mixin = require('corazon/mixin');

/**
 * A helper function for creating config properties that either reads from a
 * cached value or calls a function to get a value to cache.
 *
 * @function BelongsTo~config
 * @param {String} name The name of the option/property.
 * @param {Function} calculate A function to calculate the default value.
 * @return {Property} A config property
 */
var config = function(name, calculate) {
  var attr = '$' + name;
  return property(function() {
    if (this[attr] === undefined) {
      this[attr] = calculate.call(this);
    }
    return this[attr];
  });
};

/**
 * BelongsTo mixin for options/configuration.
 *
 * This mixin separates some of the logic of {@link BelongsTo} and is only
 * intended to be mixed into that one class.
 */
module.exports = Mixin.create(/** @lends BelongsTo# */ {

  /**
   * Override of {@link BaseRelation#configure}.
   *
   * @protected
   * @method
   * @see {@link BaseRelation#configure}
   */
  configure: function() {
    /* jshint expr: true */

    // configure each of the properties that are calculated on a delay by
    // simply invoking the property once. configure the inverse first as some
    // other calculations may rely on it.
    this.inverse;
    this.primaryKey;
    this.primaryKeyAttr;
    this.foreignKey;
    this.foreignKeyAttr;

    this.inverseRelation(); // ensure the inverse is configured
    this._super();
  },

  /**
   * Get the inverse of this relation. Access the option that was given or
   * calculate the value based on the current model class name.
   *
   * The resulting value will be locked in after the first call to avoid any
   * possible changes due to changing state outside of the relation.
   *
   * @private
   * @type {String}
   */
  inverse: config('inverse', function() {
    var inverse = this._options.inverse || this._inverse();

    // add the inverse if it's missing
    if (inverse && !this._relatedModel[inverse + 'Relation']) {
      var db = this._modelClass.db;
      var attr = db.hasMany(this._modelClass, {
        inverse: this._name,
        primaryKey: this._options.primaryKey,
        foreignKey: this._options.foreignKey,
        implicit: true,
      });
      this._relatedModel.reopen(_.object([[inverse, attr]]));
    }

    return inverse;
  }),

  /**
   * Calculate the default value of the inverse for when an option was not
   * provided for this relation.
   *
   * Mixins installed after this one can override {@link HasMany#_inverse} if
   * they need to change the default.
   *
   * @method
   * @private
   * @return {String} The default value.
   */
  _inverse: function() {
    var name = _.camelCase(this._modelClass.__name__);
    var singularized = inflection.singularize(name);
    var pluralized = inflection.pluralize(name);
    var related = this._relatedModel;
    var inverse;

    if (related[pluralized + 'Relation']) { inverse = pluralized; }
    else if (related[singularized + 'Relation']) { inverse = singularized; }
    else {
      // find a relation w/ an inverse that points back to this one
      var match = _.find(related.relations, function(relation) {
        return relation._options.inverse === this._name;
      }.bind(this));
      inverse = _.get(match, '_name');
    }

    if (!inverse) {
      inverse = pluralized;
    }

    return inverse;
  },

  /**
   * Get the primary key for this relation. This will access the primary key
   * value specified on the inverse relation, if an inverse relation exists. If
   * the inverse exists and the user also provided the `primaryKey` option when
   * creating this relation, it will ensure that the given value matches the
   * value from the inverse.
   *
   * If an inverse does not exist, this will return the value given for the
   * `primaryKey` option or `pk` as a default.
   *
   * The resulting value will be locked in after the first call to avoid any
   * possible changes due to changing state outside of the relation.
   *
   * @private
   * @type {String}
   */
  primaryKey: config('primaryKey', function() {
    var primaryKey = this._options.primaryKey;
    var inverseRelation = this.inverseRelation();
    var inversePrimaryKey = inverseRelation && inverseRelation.primaryKey;
    if (inverseRelation && primaryKey && primaryKey !== inversePrimaryKey) {
      throw new Error(util.format('%s.%s primary key must equal %j ' +
        'specified by %s.%s relation',
        this._modelClass.__identity__.__name__, this._name,
        inverseRelation.primaryKey,
        inverseRelation._modelClass.__identity__.__name__,
        inverseRelation._name));
    }
    if (inverseRelation) {
      primaryKey = inverseRelation.primaryKey;
    }
    if (!primaryKey) { // default in case the inverse does not exist
      primaryKey = 'pk';
    }
    return primaryKey;
  }),

  /**
   * Get the primary key attribute value for this relation. This looks up the
   * attribute value on the related class. If that value was not defied, it
   * falls back to the underscored version of {@link BelongsTo#primaryKey}.
   *
   * @private
   * @type {String}
   */
  primaryKeyAttr: config('primaryKeyAttr', function() {
    // since the related model may not have defined the attribute being used as
    // the primary key attribute, we need a fallback here, so we snake case the
    // property name.
    var relatedClass = this._relatedModel.__class__;
    var prototype = relatedClass.prototype;
    var primaryKeyAttr = prototype[this.primaryKey + 'Attr'];
    return primaryKeyAttr || _.snakeCase(this.primaryKey);
  }),

  /**
   * Get the foreign key for this relation. Access the option that was given or
   * calculate the value based on the relation name.
   *
   * @private
   * @type {String}
   */
  foreignKey: config('foreignKey', function() {
    return this._options.foreignKey || _.camelCase(this._name + 'Id');
  }),

  /**
   * Get the foreign key attribute value for this relation. This looks up the
   * attribute value on the model class.
   *
   * @private
   * @type {String}
   */
  foreignKeyAttr: config('foreignKeyAttr', function() {
    // we always try to find the foreign key attribute by looking at the model
    // class. it's possible that it won't be there, though. belongs-to
    // relationships that were created implicitly from a has-many won't add the
    // attribute, so we need to fall back to snake casing the foreign key.
    var modelClass = this._modelClass;
    var prototype = modelClass.__class__.prototype;
    var foreignKeyAttr = prototype[this.foreignKey + 'Attr'];
    if (!foreignKeyAttr) {
      foreignKeyAttr = _.snakeCase(this.foreignKey);
    }
    return foreignKeyAttr;
  }),

});