Enable custom methods on singular relations

This commit is contained in:
Fabien Franzen 2015-03-19 16:50:26 +01:00
parent 579895d1b3
commit 8f88a42395
2 changed files with 89 additions and 13 deletions

View File

@ -90,6 +90,14 @@ function extendScopeMethods(definition, scopeMethods, ext) {
return [].concat(customMethods || []); return [].concat(customMethods || []);
}; };
function bindRelationMethods(relation, relationMethod, definition) {
var methods = definition.methods || {};
Object.keys(methods).forEach(function(m) {
if (typeof methods[m] !== 'function') return;
relationMethod[m] = methods[m].bind(relation);
});
};
/** /**
* Relation definition class. Use to define relationships between models. * Relation definition class. Use to define relationships between models.
* @param {Object} definition * @param {Object} definition
@ -120,6 +128,7 @@ function RelationDefinition(definition) {
this.options = definition.options || {}; this.options = definition.options || {};
this.scope = definition.scope; this.scope = definition.scope;
this.embed = definition.embed === true; this.embed = definition.embed === true;
this.methods = definition.methods || {};
} }
RelationDefinition.prototype.toJSON = function () { RelationDefinition.prototype.toJSON = function () {
@ -159,13 +168,22 @@ RelationDefinition.prototype.defineMethod = function(name, fn) {
var relationName = this.name; var relationName = this.name;
var modelFrom = this.modelFrom; var modelFrom = this.modelFrom;
var definition = this; var definition = this;
var scope = this.modelFrom.scopes[this.name]; var method;
if (!scope) throw new Error('Unknown relation scope: ' + this.name); if (definition.multiple) {
var method = scope.defineMethod(name, function() { var scope = this.modelFrom.scopes[this.name];
var relation = new relationClass(definition, this); if (!scope) throw new Error('Unknown relation scope: ' + this.name);
return fn.apply(relation, arguments); method = scope.defineMethod(name, function() {
}); var relation = new relationClass(definition, this);
if (fn.shared) { return fn.apply(relation, arguments);
});
} else {
definition.methods[name] = fn;
method = function() {
var rel = this[relationName];
return rel[name].apply(rel, arguments);
}
}
if (method && fn.shared) {
sharedMethod(definition, name, method, fn); sharedMethod(definition, name, method, fn);
modelFrom.prototype['__' + name + '__' + relationName] = method; modelFrom.prototype['__' + name + '__' + relationName] = method;
} }
@ -1152,7 +1170,8 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
properties: params.properties, properties: params.properties,
scope: params.scope, scope: params.scope,
options: params.options, options: params.options,
polymorphic: polymorphic polymorphic: polymorphic,
methods: params.methods
}); });
// Define a property for the scope so that we have 'this' for the scoped methods // Define a property for the scope so that we have 'this' for the scoped methods
@ -1169,6 +1188,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
relationMethod.build = relation.build.bind(relation); relationMethod.build = relation.build.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName; relationMethod._targetClass = definition.modelTo.modelName;
} }
bindRelationMethods(relation, relationMethod, definition);
return relationMethod; return relationMethod;
} }
}); });
@ -1466,7 +1486,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
properties: params.properties, properties: params.properties,
scope: params.scope, scope: params.scope,
options: params.options, options: params.options,
polymorphic: polymorphic polymorphic: polymorphic,
methods: params.methods
}); });
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName); modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
@ -1483,6 +1504,7 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
relationMethod.update = relation.update.bind(relation); relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation); relationMethod.destroy = relation.destroy.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName; relationMethod._targetClass = definition.modelTo.modelName;
bindRelationMethods(relation, relationMethod, definition);
return relationMethod; return relationMethod;
} }
}); });
@ -1740,7 +1762,8 @@ RelationDefinition.embedsOne = function (modelFrom, modelTo, params) {
properties: params.properties, properties: params.properties,
scope: params.scope, scope: params.scope,
options: params.options, options: params.options,
embed: true embed: true,
methods: params.methods
}); });
var opts = { type: modelTo }; var opts = { type: modelTo };
@ -1785,6 +1808,7 @@ RelationDefinition.embedsOne = function (modelFrom, modelTo, params) {
relationMethod.destroy = relation.destroy.bind(relation); relationMethod.destroy = relation.destroy.bind(relation);
relationMethod.value = relation.embeddedValue.bind(relation); relationMethod.value = relation.embeddedValue.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName; relationMethod._targetClass = definition.modelTo.modelName;
bindRelationMethods(relation, relationMethod, definition);
return relationMethod; return relationMethod;
} }
}); });

View File

@ -1617,12 +1617,30 @@ describe('relations', function () {
(new Item).list.should.be.an.instanceOf(Function); (new Item).list.should.be.an.instanceOf(Function);
// syntax 2 (new) // syntax 2 (new)
Fear.belongsTo('mind'); Fear.belongsTo('mind', {
methods: { check: function() { return true; } }
});
Object.keys((new Fear).toObject()).should.containEql('mindId'); Object.keys((new Fear).toObject()).should.containEql('mindId');
(new Fear).mind.should.be.an.instanceOf(Function); (new Fear).mind.should.be.an.instanceOf(Function);
// (new Fear).mind.build().should.be.an.instanceOf(Mind); // (new Fear).mind.build().should.be.an.instanceOf(Mind);
}); });
it('should setup a custom method on accessor', function() {
var rel = Fear.relations['mind'];
rel.defineMethod('other', function() {
return true;
})
});
it('should have setup a custom method on accessor', function() {
var f = new Fear();
f.mind.check.should.be.a.function;
f.mind.check().should.be.true;
f.mind.other.should.be.a.function;
f.mind.other().should.be.true;
});
it('can be used to query data', function (done) { it('can be used to query data', function (done) {
List.hasMany('todos', {model: Item}); List.hasMany('todos', {model: Item});
db.automigrate(function () { db.automigrate(function () {
@ -1823,11 +1841,29 @@ describe('relations', function () {
}); });
it('can be declared using hasOne method', function () { it('can be declared using hasOne method', function () {
Supplier.hasOne(Account, { properties: { name: 'supplierName' } }); Supplier.hasOne(Account, {
properties: { name: 'supplierName' },
methods: { check: function() { return true; } }
});
Object.keys((new Account()).toObject()).should.containEql('supplierId'); Object.keys((new Account()).toObject()).should.containEql('supplierId');
(new Supplier()).account.should.be.an.instanceOf(Function); (new Supplier()).account.should.be.an.instanceOf(Function);
}); });
it('should setup a custom method on accessor', function() {
var rel = Supplier.relations['account'];
rel.defineMethod('other', function() {
return true;
})
});
it('should have setup a custom method on accessor', function() {
var s = new Supplier();
s.account.check.should.be.a.function;
s.account.check().should.be.true;
s.account.other.should.be.a.function;
s.account.other().should.be.true;
});
it('can be used to query data', function (done) { it('can be used to query data', function (done) {
db.automigrate(function () { db.automigrate(function () {
Supplier.create({name: 'Supplier 1'}, function (e, supplier) { Supplier.create({name: 'Supplier 1'}, function (e, supplier) {
@ -2213,7 +2249,8 @@ describe('relations', function () {
it('can be declared using embedsOne method', function (done) { it('can be declared using embedsOne method', function (done) {
Person.embedsOne(Passport, { Person.embedsOne(Passport, {
default: {name: 'Anonymous'} // a bit contrived default: {name: 'Anonymous'}, // a bit contrived
methods: { check: function() { return true; } }
}); });
Person.embedsOne(Address); // all by default Person.embedsOne(Address); // all by default
db.automigrate(done); db.automigrate(done);
@ -2228,6 +2265,21 @@ describe('relations', function () {
p.passportItem.destroy.should.be.a.function; p.passportItem.destroy.should.be.a.function;
}); });
it('should setup a custom method on accessor', function() {
var rel = Person.relations['passportItem'];
rel.defineMethod('other', function() {
return true;
})
});
it('should have setup a custom method on accessor', function() {
var p = new Person();
p.passportItem.check.should.be.a.function;
p.passportItem.check().should.be.true;
p.passportItem.other.should.be.a.function;
p.passportItem.other().should.be.true;
});
it('should behave properly without default or being set', function (done) { it('should behave properly without default or being set', function (done) {
var p = new Person(); var p = new Person();
should.not.exist(p.address); should.not.exist(p.address);