From da303b72a51d45094a9d4e6bf83a85ee1878b3fc Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Tue, 29 Jul 2014 10:54:28 +0200 Subject: [PATCH] Implemented more complex scenaro: embedsMany + relations The test case will denormalize data into the embedded object, and re-use the actual related object id as its own id. --- lib/dao.js | 37 ++++++++++-- lib/relation-definition.js | 32 ++++++++--- test/relations.test.js | 112 ++++++++++++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 12 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index a1508eb1..2a03cc4f 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -1046,6 +1046,17 @@ DataAccessObject.prototype.remove = }.bind(this)); }, null, cb); }; + +/** + * Set a single attribute. + * Equivalent to `setAttributes({name: value})` + * + * @param {String} name Name of property + * @param {Mixed} value Value of property + */ +DataAccessObject.prototype.setAttribute = function setAttribute(name, value) { + this[name] = value; +}; /** * Update a single attribute. @@ -1062,7 +1073,27 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu }; /** - * Update saet of attributes. + * Update set of attributes. + * + * @trigger `change` hook + * @param {Object} data Data to update + */ +DataAccessObject.prototype.setAttributes = function setAttributes(data) { + if (typeof data !== 'object') return; + + var Model = this.constructor; + var inst = this; + + // update instance's properties + for (var key in data) { + inst.setAttribute(key, data[key]); + } + + Model.emit('set', inst); +}; + +/** + * Update set of attributes. * Performs validation before updating. * * @trigger `validation`, `save` and `update` hooks @@ -1086,9 +1117,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb } // update instance's properties - for (var key in data) { - inst[key] = data[key]; - } + inst.setAttributes(data); inst.isValid(function (valid) { if (!valid) { diff --git a/lib/relation-definition.js b/lib/relation-definition.js index d14582b8..0d34b473 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -1057,6 +1057,11 @@ BelongsTo.prototype.related = function (refresh, params) { modelTo = params.constructor; modelInstance[fk] = params[pk]; if (discriminator) modelInstance[discriminator] = params.constructor.modelName; + + var data = {}; + this.definition.applyProperties(params, data); + modelInstance.setAttributes(data); + self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter @@ -1525,7 +1530,9 @@ RelationDefinition.embedsMany = function hasMany(modelFrom, modelTo, params) { }; EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) { - var name = this.definition.name; + var modelTo = this.definition.modelTo; + var relationName = this.definition.name; + var modelInstance = this.modelInstance; var self = receiver; var actualCond = {}; @@ -1543,14 +1550,29 @@ EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb throw new Error('Method can be only called with one or two arguments'); } - var embeddedList = self[name] || []; + var embeddedList = self[relationName] || []; + + this.definition.applyScope(modelInstance, actualCond); var params = mergeQuery(actualCond, scopeParams); + if (params.where) { embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList; } - process.nextTick(function() { cb(null, embeddedList); }); + var returnRelated = function(list) { + if (params.include) { + modelTo.include(list, params.include, cb); + } else { + process.nextTick(function() { cb(null, list); }); + } + }; + + if (actualRefresh) { + + } + + returnRelated(embeddedList); }; EmbedsMany.prototype.findById = function (fkId, cb) { @@ -1722,10 +1744,6 @@ EmbedsMany.prototype.build = HasOne.prototype.build = function(targetModelData) var embeddedList = modelInstance[relationName] || []; - if (typeof targetModelData === 'function' && !cb) { - cb = targetModelData; - targetModelData = {}; - } targetModelData = targetModelData || {}; if (typeof targetModelData[pk] !== 'number' && autoId) { diff --git a/test/relations.test.js b/test/relations.test.js index 2a9e5526..e0c9c44d 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -1221,6 +1221,9 @@ describe('relations', function () { }); describe('embedsMany', function () { + + var address1, address2; + before(function (done) { db = getSchema(); Person = db.define('Person', {name: String}); @@ -1250,7 +1253,6 @@ describe('relations', function () { p.addressList.build.should.be.a.function; }); - var address1, address2; it('should create embedded items on scope', function(done) { Person.create({ name: 'Fred' }, function(err, p) { p.addressList.create({ street: 'Street 1' }, function(err, addresses) { @@ -1540,5 +1542,113 @@ describe('relations', function () { }); }); + + describe('embedsMany - relations, scope and properties', function () { + + var product1, product2; + + before(function (done) { + db = getSchema(); + Category = db.define('Category', {name: String}); + Product = db.define('Product', {name: String}); + Link = db.define('Link'); + + db.automigrate(function () { + Person.destroyAll(done); + }); + }); + + it('can be declared', function (done) { + Category.embedsMany(Link, { + as: 'items', // rename + scope: { include: 'product' } // always include + }); + Link.belongsTo(Product, { + foreignKey: 'id', // re-use the actual product id + properties: { id: 'id', name: 'name' }, // denormalize, transfer id + }); + db.automigrate(done); + }); + + it('should setup related items', function(done) { + Product.create({ name: 'Product 1' }, function(err, p) { + product1 = p; + Product.create({ name: 'Product 2' }, function(err, p) { + product2 = p; + done(); + }); + }); + }); + + it('should create item on scope', function(done) { + Category.create({ name: 'Category A' }, function(err, cat) { + var link = cat.items.build(); + link.product(product1); + var link = cat.items.build(); + link.product(product2); + cat.save(function(err, cat) { + var product = cat.items.at(0); + product.id.should.equal(product1.id); + product.name.should.equal(product1.name); + var product = cat.items.at(1); + product.id.should.equal(product2.id); + product.name.should.equal(product2.name); + done(); + }); + }); + }); + + it('should include related items on scope', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(2); + + // denormalized properties: + cat.items.at(0).id.should.equal(product1.id); + cat.items.at(0).name.should.equal(product1.name); + cat.items.at(1).id.should.equal(product2.id); + cat.items.at(1).name.should.equal(product2.name); + + // lazy-loaded relations + should.not.exist(cat.items.at(0).product()); + should.not.exist(cat.items.at(1).product()); + + cat.items(function(err, items) { + cat.items.at(0).product().should.be.instanceof(Product); + cat.items.at(1).product().should.be.instanceof(Product); + cat.items.at(1).product().name.should.equal('Product 2'); + done(); + }); + }); + }); + + it('should remove embedded items by id', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(2); + cat.items.destroy(product1.id, function(err) { + should.not.exist(err); + cat.links.should.have.length(1); + done(); + }); + }); + }); + + it('should find items on scope', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(1); + cat.items.at(0).id.should.equal(product2.id); + cat.items.at(0).name.should.equal(product2.name); + + // lazy-loaded relations + should.not.exist(cat.items.at(0).product()); + + cat.items(function(err, items) { + cat.items.at(0).product().should.be.instanceof(Product); + cat.items.at(0).product().name.should.equal('Product 2'); + done(); + }); + }); + }); + + }); });