Skip to content

Commit

Permalink
Always lookup relation object on model instance.
Browse files Browse the repository at this point in the history
All methods and properties that are added by a relation are now
required to look up the relation object when they're invoked.
Previously, the relation object used to define the method was assumed
to be constant, but the lookup allows the relation object to be
overridden & redefine aspects of the relation object.

This is required for supporting relations in azul-transaction.
  • Loading branch information
wbyoung committed May 27, 2015
1 parent f2ea065 commit 9563009
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 62 deletions.
38 changes: 27 additions & 11 deletions lib/relations/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,8 @@ BaseRelation.reopen(/** @lends BaseRelation# */ {

/**
* @function BaseRelation~MethodCallable
* @param {BaseRelation} relation [description]
* @return {Object|Function} A value that should be added
* @param {String} relationKey Key to find the relation on the instance.
* @return {Object|Function} A value that should be added.
*/

/**
Expand All @@ -341,13 +341,15 @@ BaseRelation.reopen(/** @lends BaseRelation# */ {
*
* CustomRelation.reopenClass({
* methods: {
* 'get<Singular>': function(relation) {
* 'get<Singular>': function(relationKey) {
* return function() {
* var relation = this[relationKey];
* return this['_' + relation._name];
* };
* },
* 'set<Singular>': function(relation) {
* 'set<Singular>': function(relationKey) {
* return function(value) {
* var relation = this[relationKey];
* this['_' + relation._name] = value;
* };
* }
Expand Down Expand Up @@ -378,15 +380,23 @@ BaseRelation.reopen(/** @lends BaseRelation# */ {
* @return {Object} A set of attributes.
*/
methods: function(existing) {
// all relations define a `<name>Relation` method that is used to
// access the relation from model instances & also to dispatch individual
// method calls. this simply returns the relation object (though it can be
// overridden by subclasses or individual instances).
var self = this;
var relationKey = this._name + 'Relation';
var base = _.object([
[relationKey, property(function() { return self; })]
]);
var methods = this.__identity__.methods;
return _.transform(methods, function(result, method, key) {
var name = self.template(key);
var include = !method.optional || !existing[name];
if (include) {
result[name] = method(self);
result[name] = method(relationKey);
}
}, {});
}, base);
}

});
Expand Down Expand Up @@ -420,12 +430,12 @@ BaseRelation.reopenClass(/** @lends BaseRelation */ {
* @return {BaseRelation~MethodCallable}
*/
property: function(getterName, setterName) {
return function(relation) {
return function(relationKey) {
var getter = getterName && function() {
return relation[getterName](this);
return this[relationKey][getterName](this);
};
var setter = setterName && function(value) {
return relation[setterName](this, value);
return this[relationKey][setterName](this, value);
};
return property(getter, setter);
};
Expand All @@ -450,6 +460,11 @@ BaseRelation.reopenClass(/** @lends BaseRelation */ {
* model instance on which the method is operating, and the remaining
* arguments will be any that were passed to the method.
*
* The resulting method looks up the relationship currently defined on the
* instance rather than assuming that it has remained unchanged since the
* method was added to the model class. This allows overriding of the
* relation object (used by azul-transaction).
*
* The example given in {@link BaseRelation#methods} could be re-implemented
* using this helper as follows:
*
Expand All @@ -475,14 +490,15 @@ BaseRelation.reopenClass(/** @lends BaseRelation */ {
* @return {BaseRelation~MethodCallable}
*/
method: function(name) {
return function(relation) {
return function(relationKey) {
return function() {
var relation = this[relationKey];
var fn = relation[name];
var args = _.toArray(arguments);
return fn.apply(relation, [this].concat(args));
};
};
}
},

});

Expand Down
27 changes: 1 addition & 26 deletions lib/relations/belongs_to.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,30 +96,6 @@ var invalidatesQuery = function(fn) {

BelongsTo.reopen(/** @lends BelongsTo# */ {

/**
* The identity property for this relation.
*
* This property allows access to the underlying relation object, allowing
* you to access details about how the relation was created/configured. This
* method is currently only intended for internal use, but if you have a
* reason for it to be documented publicly, please create an issue on GitHub
* and let us know.
*
* It is accessible on an individual model via `<singular>Relation`. For
* instance, an article that belongs to an author would cause this method to
* get triggered via `article.authorRelation`.
*
* The naming conventions are set forth in {@link BelongsTo.methods}.
*
* @method
* @protected
* @param {Model} instance The model instance on which to operate.
* @see {@link BaseRelation#methods}
*/
identity: function(/*instance*/) {
return this;
},

/**
* The item property for this relation.
*
Expand Down Expand Up @@ -357,12 +333,11 @@ BelongsTo.reopenClass(/** @lends BelongsTo */ {
*/
methods: {
'<singular>': BaseRelation.property('item', 'set'),
'<singular>Relation': BaseRelation.property('identity'),
'get<Singular>': BaseRelation.method('item'),
'set<Singular>': BaseRelation.method('set'),
'create<Singular>': BaseRelation.method('create'),
'fetch<Singular>': BaseRelation.method('fetch'),
'<foreignKey>': _.extend(function(/*relation*/) {
'<foreignKey>': _.extend(function(/*relationKey*/) {
return attr(undefined, { writable: false });
}, { optional: true }),
'save': BaseRelation.method('save')
Expand Down
25 changes: 0 additions & 25 deletions lib/relations/has_many.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,30 +84,6 @@ var notify = function(name) {

HasMany.reopen(/** @lends HasMany# */ {

/**
* The identity property for this relation.
*
* This property allows access to the underlying relation object, allowing
* you to access details about how the relation was created/configured. This
* method is currently only intended for internal use, but if you have a
* reason for it to be documented publicly, please create an issue on GitHub
* and let us know.
*
* It is accessible on an individual model via `<plural>Relation`. For
* instance, a user that has many articles would cause this method to get
* triggered via `user.articlesRelation`.
*
* The naming conventions are set forth in {@link HasMany.methods}.
*
* @method
* @protected
* @param {Model} instance The model instance on which to operate.
* @see {@link BaseRelation#methods}
*/
identity: function(/*instance*/) {
return this;
},

/**
* Determine if this relation is a many-to-many relationship, that is if the
* inverse relationship is a many relationship as well. Has many
Expand Down Expand Up @@ -419,7 +395,6 @@ HasMany.reopenClass(/** @lends HasMany */ {
*/
methods: {
'<plural>': BaseRelation.property('collection'),
'<plural>Relation': BaseRelation.property('identity'),
'<singular>Objects': BaseRelation.property('objectsQuery'),
'create<Singular>': BaseRelation.method('createObject'),
'add<Singular>': BaseRelation.method('addObjects'),
Expand Down
53 changes: 53 additions & 0 deletions test/relations/base_tests.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use strict';

var chai = require('chai');
var sinon = require('sinon'); chai.use(require('sinon-chai'));
var expect = chai.expect;

var Database = require('../../lib/database');
var FakeAdapter = require('../fakes/adapter');
var BaseRelation = require('../../lib/relations/base');
var property = require('../../lib/util/property').fn;

require('../helpers/model');

Expand Down Expand Up @@ -57,4 +59,55 @@ describe('BaseRelation', function() {
}).to.throw(/associatePrefetchResults.*must.*implemented.*subclass/i);
});

it('dispatches methods & properties to instance relation', function() {
var method = sinon.spy();
var getter = sinon.spy();
var setter = sinon.spy();

var Relation = BaseRelation.extend({
method: method,
get: getter,
set: setter
});
Relation.reopenClass({
methods: {
'<singular>Method': BaseRelation.method('method'),
'<singular>Property': BaseRelation.property('get', 'set'),
}
});

// this is a strategy that azul-transaction uses, so we need to support it
var Base = db.Model.extend({ custom: Relation.attr()() });
var relation = Object.create(Base.__class__.prototype.customRelation);
var Override = Base.extend({
customRelation: property(function() { return relation; })
});

var base = Base.create();
var override = Override.create();

base.customMethod();
override.customMethod(); // should be called with custom object
base.customProperty;
override.customProperty; // should be called with custom object
base.customProperty = undefined;
override.customProperty = undefined; // should be called with custom object

expect(method.getCall(0).thisValue)
.to.equal(Base.__class__.prototype.customRelation);
expect(method.getCall(1).thisValue)
.to.equal(relation);
expect(method).to.have.been.calledTwice;
expect(getter.getCall(0).thisValue)
.to.equal(Base.__class__.prototype.customRelation);
expect(getter.getCall(1).thisValue)
.to.equal(relation);
expect(getter).to.have.been.calledTwice;
expect(setter.getCall(0).thisValue)
.to.equal(Base.__class__.prototype.customRelation);
expect(setter.getCall(1).thisValue)
.to.equal(relation);
expect(setter).to.have.been.calledTwice;
});

});

0 comments on commit 9563009

Please sign in to comment.