From 8f88a42395f87be51f939f1c8de800ed10d203f0 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Thu, 19 Mar 2015 16:50:26 +0100 Subject: [PATCH 1/2] Enable custom methods on singular relations --- lib/relation-definition.js | 44 ++++++++++++++++++++++------- test/relations.test.js | 58 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index ed0d7fef..232f232a 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -90,6 +90,14 @@ function extendScopeMethods(definition, scopeMethods, ext) { 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. * @param {Object} definition @@ -120,6 +128,7 @@ function RelationDefinition(definition) { this.options = definition.options || {}; this.scope = definition.scope; this.embed = definition.embed === true; + this.methods = definition.methods || {}; } RelationDefinition.prototype.toJSON = function () { @@ -159,13 +168,22 @@ RelationDefinition.prototype.defineMethod = function(name, fn) { var relationName = this.name; var modelFrom = this.modelFrom; var definition = this; - var scope = this.modelFrom.scopes[this.name]; - if (!scope) throw new Error('Unknown relation scope: ' + this.name); - var method = scope.defineMethod(name, function() { - var relation = new relationClass(definition, this); - return fn.apply(relation, arguments); - }); - if (fn.shared) { + var method; + if (definition.multiple) { + var scope = this.modelFrom.scopes[this.name]; + if (!scope) throw new Error('Unknown relation scope: ' + this.name); + method = scope.defineMethod(name, function() { + var relation = new relationClass(definition, this); + 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); modelFrom.prototype['__' + name + '__' + relationName] = method; } @@ -1152,7 +1170,8 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { properties: params.properties, scope: params.scope, 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 @@ -1169,6 +1188,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { relationMethod.build = relation.build.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; } + bindRelationMethods(relation, relationMethod, definition); return relationMethod; } }); @@ -1466,7 +1486,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { properties: params.properties, scope: params.scope, options: params.options, - polymorphic: polymorphic + polymorphic: polymorphic, + methods: params.methods }); 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.destroy = relation.destroy.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; + bindRelationMethods(relation, relationMethod, definition); return relationMethod; } }); @@ -1740,7 +1762,8 @@ RelationDefinition.embedsOne = function (modelFrom, modelTo, params) { properties: params.properties, scope: params.scope, options: params.options, - embed: true + embed: true, + methods: params.methods }); var opts = { type: modelTo }; @@ -1785,6 +1808,7 @@ RelationDefinition.embedsOne = function (modelFrom, modelTo, params) { relationMethod.destroy = relation.destroy.bind(relation); relationMethod.value = relation.embeddedValue.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; + bindRelationMethods(relation, relationMethod, definition); return relationMethod; } }); diff --git a/test/relations.test.js b/test/relations.test.js index c3b81b23..638e537d 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -1617,12 +1617,30 @@ describe('relations', function () { (new Item).list.should.be.an.instanceOf(Function); // syntax 2 (new) - Fear.belongsTo('mind'); + Fear.belongsTo('mind', { + methods: { check: function() { return true; } } + }); + Object.keys((new Fear).toObject()).should.containEql('mindId'); (new Fear).mind.should.be.an.instanceOf(Function); // (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) { List.hasMany('todos', {model: Item}); db.automigrate(function () { @@ -1823,11 +1841,29 @@ describe('relations', 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'); (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) { db.automigrate(function () { Supplier.create({name: 'Supplier 1'}, function (e, supplier) { @@ -2213,7 +2249,8 @@ describe('relations', function () { it('can be declared using embedsOne method', function (done) { 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 db.automigrate(done); @@ -2228,6 +2265,21 @@ describe('relations', 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) { var p = new Person(); should.not.exist(p.address); From 54781f376ef5a4ecbb0ccc0ba6f49ad6b9435d6c Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Thu, 19 Mar 2015 16:50:26 +0100 Subject: [PATCH 2/2] Enable custom methods on singular relations --- lib/relation-definition.js | 44 ++++++++++++++++++++++------- test/relations.test.js | 58 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index ed0d7fef..232f232a 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -90,6 +90,14 @@ function extendScopeMethods(definition, scopeMethods, ext) { 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. * @param {Object} definition @@ -120,6 +128,7 @@ function RelationDefinition(definition) { this.options = definition.options || {}; this.scope = definition.scope; this.embed = definition.embed === true; + this.methods = definition.methods || {}; } RelationDefinition.prototype.toJSON = function () { @@ -159,13 +168,22 @@ RelationDefinition.prototype.defineMethod = function(name, fn) { var relationName = this.name; var modelFrom = this.modelFrom; var definition = this; - var scope = this.modelFrom.scopes[this.name]; - if (!scope) throw new Error('Unknown relation scope: ' + this.name); - var method = scope.defineMethod(name, function() { - var relation = new relationClass(definition, this); - return fn.apply(relation, arguments); - }); - if (fn.shared) { + var method; + if (definition.multiple) { + var scope = this.modelFrom.scopes[this.name]; + if (!scope) throw new Error('Unknown relation scope: ' + this.name); + method = scope.defineMethod(name, function() { + var relation = new relationClass(definition, this); + 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); modelFrom.prototype['__' + name + '__' + relationName] = method; } @@ -1152,7 +1170,8 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { properties: params.properties, scope: params.scope, 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 @@ -1169,6 +1188,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { relationMethod.build = relation.build.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; } + bindRelationMethods(relation, relationMethod, definition); return relationMethod; } }); @@ -1466,7 +1486,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { properties: params.properties, scope: params.scope, options: params.options, - polymorphic: polymorphic + polymorphic: polymorphic, + methods: params.methods }); 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.destroy = relation.destroy.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; + bindRelationMethods(relation, relationMethod, definition); return relationMethod; } }); @@ -1740,7 +1762,8 @@ RelationDefinition.embedsOne = function (modelFrom, modelTo, params) { properties: params.properties, scope: params.scope, options: params.options, - embed: true + embed: true, + methods: params.methods }); var opts = { type: modelTo }; @@ -1785,6 +1808,7 @@ RelationDefinition.embedsOne = function (modelFrom, modelTo, params) { relationMethod.destroy = relation.destroy.bind(relation); relationMethod.value = relation.embeddedValue.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; + bindRelationMethods(relation, relationMethod, definition); return relationMethod; } }); diff --git a/test/relations.test.js b/test/relations.test.js index c3b81b23..638e537d 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -1617,12 +1617,30 @@ describe('relations', function () { (new Item).list.should.be.an.instanceOf(Function); // syntax 2 (new) - Fear.belongsTo('mind'); + Fear.belongsTo('mind', { + methods: { check: function() { return true; } } + }); + Object.keys((new Fear).toObject()).should.containEql('mindId'); (new Fear).mind.should.be.an.instanceOf(Function); // (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) { List.hasMany('todos', {model: Item}); db.automigrate(function () { @@ -1823,11 +1841,29 @@ describe('relations', 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'); (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) { db.automigrate(function () { Supplier.create({name: 'Supplier 1'}, function (e, supplier) { @@ -2213,7 +2249,8 @@ describe('relations', function () { it('can be declared using embedsOne method', function (done) { 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 db.automigrate(done); @@ -2228,6 +2265,21 @@ describe('relations', 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) { var p = new Person(); should.not.exist(p.address);