diff --git a/lib/include.js b/lib/include.js index a72824b0..5fe6548c 100644 --- a/lib/include.js +++ b/lib/include.js @@ -133,7 +133,7 @@ Inclusion.include = function (objects, include, cb) { obj.__cachedRelations[relationName] = result; if(obj === inst) { obj.__data[relationName] = result; - obj.__strict = false; + obj.setStrict(false); } else { obj[relationName] = result; } diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 59e8425a..fc2bd48c 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -65,6 +65,7 @@ function RelationDefinition(definition) { } definition = definition || {}; this.name = definition.name; + this.accessor = definition.accessor || this.name; assert(this.name, 'Relation name is missing'); this.type = normalizeType(definition.type); assert(this.type, 'Invalid relation type: ' + definition.type); @@ -1444,6 +1445,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; var definition = modelFrom.relations[accessorName] = new RelationDefinition({ + accessor: accessorName, name: relationName, type: RelationTypes.embedsMany, modelFrom: modelFrom, @@ -1507,8 +1509,11 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) destroy: scopeMethod(definition, 'destroyById'), updateById: scopeMethod(definition, 'updateById'), exists: scopeMethod(definition, 'exists'), + add: scopeMethod(definition, 'add'), + remove: scopeMethod(definition, 'remove'), get: scopeMethod(definition, 'get'), set: scopeMethod(definition, 'set'), + unset: scopeMethod(definition, 'unset'), at: scopeMethod(definition, 'at') }; @@ -1520,6 +1525,12 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) var updateByIdFunc = scopeMethods.updateById; modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc; + + var addFunc = scopeMethods.add; + modelFrom.prototype['__link__' + relationName] = addFunc; + + var removeFunc = scopeMethods.remove; + modelFrom.prototype['__unlink__' + relationName] = removeFunc; scopeMethods.create = scopeMethod(definition, 'create'); scopeMethods.build = scopeMethod(definition, 'build'); @@ -1660,7 +1671,7 @@ EmbedsMany.prototype.destroyById = function (fkId, cb) { var embeddedList = modelInstance[relationName] || []; - var inst = this.findById(fkId); + var inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId); if (inst instanceof modelTo) { var index = embeddedList.indexOf(inst); @@ -1681,6 +1692,7 @@ EmbedsMany.prototype.destroyById = function (fkId, cb) { EmbedsMany.prototype.get = EmbedsMany.prototype.findById; EmbedsMany.prototype.set = EmbedsMany.prototype.updateById; +EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById; EmbedsMany.prototype.at = function (index, cb) { var modelTo = this.definition.modelTo; @@ -1734,7 +1746,7 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { }); }; -EmbedsMany.prototype.build = HasOne.prototype.build = function(targetModelData) { +EmbedsMany.prototype.build = function(targetModelData) { var pk = this.definition.keyFrom; var modelTo = this.definition.modelTo; var relationName = this.definition.name; @@ -1765,6 +1777,88 @@ EmbedsMany.prototype.build = HasOne.prototype.build = function(targetModelData) return inst; }; +/** + * Add the target model instance to the 'embedsMany' relation + * @param {Object|ID} acInst The actual instance or id value + */ +EmbedsMany.prototype.add = function (acInst, cb) { + var self = this; + var definition = this.definition; + var modelTo = this.definition.modelTo; + var modelInstance = this.modelInstance; + + var options = definition.options; + var referenceDef = options.reference && modelTo.relations[options.reference]; + + if (!referenceDef) { + throw new Error('Invalid reference: ' + options.reference || '(none)'); + } + + var fk2 = referenceDef.keyTo; + var pk2 = referenceDef.modelTo.definition.idName() || 'id'; + + var query = {}; + + query[fk2] = (acInst instanceof referenceDef.modelTo) ? acInst[pk2] : acInst; + + var filter = { where: query }; + + referenceDef.applyScope(modelInstance, filter); + + referenceDef.modelTo.findOne(filter, function(err, ref) { + if (ref instanceof referenceDef.modelTo) { + var inst = self.build(); + inst[options.reference](ref); + modelInstance.save(function(err) { + cb(err, err ? null : inst); + }); + } else { + cb(null, null); + } + }); +}; + +/** + * Remove the target model instance from the 'embedsMany' relation + * @param {Object|ID) acInst The actual instance or id value + */ +EmbedsMany.prototype.remove = function (acInst, cb) { + var self = this; + var definition = this.definition; + var modelTo = this.definition.modelTo; + var modelInstance = this.modelInstance; + + var options = definition.options; + var referenceDef = options.reference && modelTo.relations[options.reference]; + + if (!referenceDef) { + throw new Error('Invalid reference: ' + options.reference || '(none)'); + } + + var fk2 = referenceDef.keyTo; + var pk2 = referenceDef.modelTo.definition.idName() || 'id'; + + var query = {}; + + query[fk2] = (acInst instanceof referenceDef.modelTo) ? acInst[pk2] : acInst; + + var filter = { where: query }; + + referenceDef.applyScope(modelInstance, filter); + + modelInstance[definition.accessor](filter, function(err, items) { + if (err) return cb(err); + + items.forEach(function(item) { + self.unset(item); + }); + + modelInstance.save(function(err) { + cb(err, err ? [] : items); + }); + }); +}; + RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, params) { }; diff --git a/test/relations.test.js b/test/relations.test.js index c37b082b..a3d04a7d 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -1545,13 +1545,13 @@ describe('relations', function () { describe('embedsMany - relations, scope and properties', function () { - var product1, product2; + var product1, product2, product3; before(function (done) { db = getSchema(); Category = db.define('Category', {name: String}); Product = db.define('Product', {name: String}); - Link = db.define('Link'); + Link = db.define('Link', {name: String}); db.automigrate(function () { Person.destroyAll(done); @@ -1561,13 +1561,16 @@ describe('relations', function () { it('can be declared', function (done) { Category.embedsMany(Link, { as: 'items', // rename - scope: { include: 'product' } // always include + scope: { include: 'product' }, // always include + options: { reference: 'product' } // optional, for add()/remove() }); Link.belongsTo(Product, { foreignKey: 'id', // re-use the actual product id properties: { id: 'id', name: 'name' }, // denormalize, transfer id }); - db.automigrate(done); + db.automigrate(function() { + Product.create({ name: 'Product 0' }, done); // offset ids for tests + }); }); it('should setup related items', function(done) { @@ -1575,7 +1578,10 @@ describe('relations', function () { product1 = p; Product.create({ name: 'Product 2' }, function(err, p) { product2 = p; - done(); + Product.create({ name: 'Product 3' }, function(err, p) { + product3 = p; + done(); + }); }); }); }); @@ -1588,6 +1594,7 @@ describe('relations', function () { link.product(product2); cat.save(function(err, cat) { var product = cat.items.at(0); + product.should.be.instanceof(Link); product.should.not.have.property('productId'); product.id.should.eql(product1.id); product.name.should.equal(product1.name); @@ -1604,6 +1611,7 @@ describe('relations', function () { cat.links.should.have.length(2); // denormalized properties: + cat.items.at(0).should.be.instanceof(Link); cat.items.at(0).id.should.eql(product1.id); cat.items.at(0).name.should.equal(product1.name); cat.items.at(1).id.should.eql(product2.id); @@ -1650,6 +1658,52 @@ describe('relations', function () { }); }); + it('should add related items to scope', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(1); + cat.items.add(product3, function(err, link) { + link.should.be.instanceof(Link); + link.id.should.equal(product3.id); + link.name.should.equal('Product 3'); + + cat.links.should.have.length(2); + done(); + }); + }); + }); + + it('should find items on scope', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(2); + + cat.items.at(0).should.be.instanceof(Link); + cat.items.at(0).id.should.eql(product2.id); + cat.items.at(0).name.should.equal(product2.name); + cat.items.at(1).id.should.eql(product3.id); + cat.items.at(1).name.should.equal(product3.name); + + done(); + }); + }); + + it('should remove embedded items by reference id', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(2); + cat.items.remove(product2.id, function(err) { + should.not.exist(err); + cat.links.should.have.length(1); + done(); + }); + }); + }); + + it('should remove embedded items by reference id', function(done) { + Category.findOne(function(err, cat) { + cat.links.should.have.length(1); + done(); + }); + }); + }); describe('embedsMany - polymorphic relations', function () {