diff --git a/lib/relation-definition.js b/lib/relation-definition.js index ed0d7fef8..a8192b667 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -1856,27 +1856,35 @@ EmbedsOne.prototype.create = function (targetModelData, cb) { var inst = this.callScopeMethod('build', targetModelData); - var updateEmbedded = function() { + var updateEmbedded = function(callback) { modelInstance.updateAttribute(propertyName, inst, function(err) { - cb(err, err ? null : inst); + callback(err, err ? null : inst); }); }; if (this.definition.options.persistent) { inst.save(function(err) { // will validate if (err) return cb(err, inst); - updateEmbedded(); + updateEmbedded(cb); }); } else { - var err = inst.isValid() ? null : new ValidationError(inst); - if (err) { - process.nextTick(function() { - cb(err); - }); - } else { - updateEmbedded(); - } + var context = { Model: modelTo, instance: inst, hookState: {} }; + modelTo.notifyObserversOf('before save', context, function(err) { + err = err || (inst.isValid() ? null : new ValidationError(inst)); + if (err) { + process.nextTick(function() { + cb(err); + }); + } else { + updateEmbedded(function(err, inst) { + if (err) return cb(err, null); + modelTo.notifyObserversOf('after save', context, function(err) { + cb(err, err ? null : inst); + }); + }); + } + }); } }; @@ -1916,14 +1924,40 @@ EmbedsOne.prototype.update = function (targetModelData, cb) { var isInst = targetModelData instanceof ModelBaseClass; var data = isInst ? targetModelData.toObject() : targetModelData; + data = typeof data === 'object' ? data : {}; var embeddedInstance = modelInstance[propertyName]; if (embeddedInstance instanceof modelTo) { - embeddedInstance.setAttributes(data); if (typeof cb === 'function') { - modelInstance.save(function(err, inst) { - cb(err, inst ? inst[propertyName] : embeddedInstance); + var hookState = {}; + var context = { + Model: modelTo, + currentInstance: embeddedInstance, + data: data, + hookState: hookState + }; + modelTo.notifyObserversOf('before save', context, function(err) { + if (err) return cb(err); + + embeddedInstance.setAttributes(data); + + var err = embeddedInstance.isValid() ? null : new ValidationError(embeddedInstance); + if (err && typeof cb === 'function') { + return process.nextTick(function() { + cb(err, embeddedInstance); + }); + } + + modelInstance.save(function(err, inst) { + if (err) return cb(err); + context = { Model: modelTo, instance: embeddedInstance, hookState: hookState }; + modelTo.notifyObserversOf('after save', context, function(err) { + cb(err, inst ? inst[propertyName] : embeddedInstance); + }); + }); }); + } else { + embeddedInstance.setAttributes(data); } } else if (!embeddedInstance && cb) { this.callScopeMethod('create', data, cb); @@ -1933,11 +1967,23 @@ EmbedsOne.prototype.update = function (targetModelData, cb) { }; EmbedsOne.prototype.destroy = function (cb) { + var modelTo = this.definition.modelTo; var modelInstance = this.modelInstance; var propertyName = this.definition.keyFrom; + var embeddedInstance = modelInstance[propertyName]; + + if (!embeddedInstance) return cb && cb(null, 0); + modelInstance.unsetAttribute(propertyName, true); - modelInstance.save(function (err, result) { - cb && cb(err, result); + + var context = { Model: modelTo, instance: embeddedInstance, hookState: {} }; + modelTo.notifyObserversOf('before delete', context, function(err) { + if (err) return cb && cb(err); + modelInstance.save(function(err, result) { + modelTo.notifyObserversOf('after delete', context, function(err) { + cb && cb(err, result); + }); + }); }); }; @@ -2223,24 +2269,42 @@ EmbedsMany.prototype.updateById = function (fkId, data, cb) { var embeddedList = this.embeddedList(); var inst = this.findById(fkId); + var data = typeof data === 'object' ? data : {}; if (inst instanceof modelTo) { - if (typeof data === 'object') { - inst.setAttributes(data); - } - var err = inst.isValid() ? null : new ValidationError(inst); - if (err && typeof cb === 'function') { - return process.nextTick(function() { - cb(err, inst); - }); - } + var hookState = {}; + var context = { + Model: modelTo, + currentInstance: inst, + data: data, + hookState: hookState + }; + modelTo.notifyObserversOf('before save', context, function(err) { + if (err) return cb && cb(err); - if (typeof cb === 'function') { - modelInstance.updateAttribute(propertyName, - embeddedList, function(err) { - cb(err, inst); - }); - } + inst.setAttributes(data); + + var err = inst.isValid() ? null : new ValidationError(inst); + if (err && typeof cb === 'function') { + return process.nextTick(function() { + cb(err, inst); + }); + } + + context = { Model: modelTo, instance: inst, hookState: hookState }; + + if (typeof cb === 'function') { + modelInstance.updateAttribute(propertyName, + embeddedList, function(err) { + if (err) return cb(err, inst); + modelTo.notifyObserversOf('after save', context, function(err) { + cb(err, inst); + }); + }); + } else { + modelTo.notifyObserversOf('after save', context); + } + }); } else if (typeof cb === 'function') { process.nextTick(function() { cb(null, null); // not found @@ -2259,15 +2323,23 @@ EmbedsMany.prototype.destroyById = function (fkId, cb) { var inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId); if (inst instanceof modelTo) { - var index = embeddedList.indexOf(inst); - if (index > -1) embeddedList.splice(index, 1); - if (typeof cb === 'function') { - modelInstance.updateAttribute(propertyName, - embeddedList, function(err) { - cb(err); - modelTo.emit('deleted', inst.id, inst.toJSON()); - }); - } + var context = { Model: modelTo, instance: inst, hookState: {} }; + modelTo.notifyObserversOf('before delete', context, function(err) { + var index = embeddedList.indexOf(inst); + if (index > -1) embeddedList.splice(index, 1); + if (typeof cb === 'function') { + modelInstance.updateAttribute(propertyName, + embeddedList, function(err) { + if (err) return cb(err); + modelTo.notifyObserversOf('after delete', context, function(err) { + cb(err); + if (!err) modelTo.emit('deleted', inst.id, inst.toJSON()); + }); + }); + } else { + modelTo.notifyObserversOf('after delete', context); + } + }); } else if (typeof cb === 'function') { process.nextTick(cb); // not found } @@ -2312,17 +2384,17 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { var inst = this.callScopeMethod('build', targetModelData); - var updateEmbedded = function() { + var updateEmbedded = function(callback) { modelInstance.updateAttribute(propertyName, embeddedList, function(err, modelInst) { - cb(err, err ? null : inst); + callback(err, err ? null : inst); }); }; if (this.definition.options.persistent) { inst.save(function(err) { // will validate if (err) return cb(err, inst); - updateEmbedded(); + updateEmbedded(cb); }); } else { var err = inst.isValid() ? null : new ValidationError(inst); @@ -2331,9 +2403,19 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { cb(err); }); } else { - updateEmbedded(); + var context = { Model: modelTo, instance: inst, hookState: {} }; + modelTo.notifyObserversOf('before save', context, function(err) { + if (err) return cb(err); + updateEmbedded(function(err, inst) { + if (err) return cb(err, null); + modelTo.notifyObserversOf('after save', context, function(err) { + cb(err, err ? null : inst); + }); + }); + }); } } + }; EmbedsMany.prototype.build = function(targetModelData) { diff --git a/test/relations.test.js b/test/relations.test.js index c3b81b230..01d07a43c 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -2199,6 +2199,9 @@ describe('relations', function () { var Passport; var Other; + var hooks = []; + var combinedHooks = []; + before(function () { tmp = getTransientDataSource(); db = getSchema(); @@ -2211,11 +2214,31 @@ describe('relations', function () { Other = db.define('Other', {name: String}); }); + beforeEach(function() { + combinedHooks = []; + }); + it('can be declared using embedsOne method', function (done) { Person.embedsOne(Passport, { default: {name: 'Anonymous'} // a bit contrived }); Person.embedsOne(Address); // all by default + + observeHook(Person, 'before save'); + observeHook(Person, 'after save'); + observeHook(Person, 'before delete'); + observeHook(Person, 'after delete'); + + observeHook(Passport, 'before save'); + observeHook(Passport, 'after save'); + observeHook(Passport, 'before delete'); + observeHook(Passport, 'after delete'); + + observeEmbedded(Passport, 'before save'); + observeEmbedded(Passport, 'after save'); + observeEmbedded(Passport, 'before delete'); + observeEmbedded(Passport, 'after delete'); + db.automigrate(done); }); @@ -2274,6 +2297,17 @@ describe('relations', function () { should.not.exist(err); p.passport.toObject().should.eql({name: 'Fredric'}); p.passport.should.be.an.instanceOf(Passport); + + var expected = [ + 'Person: before save', + 'Person: after save', + 'Passport: before save', + 'Person: before save', + 'Person: after save', + 'Passport: after save' + ]; + combinedHooks.should.eql(expected); + done(); }); }); @@ -2323,6 +2357,15 @@ describe('relations', function () { passport.toObject().should.eql({name: 'Freddy'}); passport.should.be.an.instanceOf(Passport); passport.should.equal(p.passport); + + var expected = [ + 'Passport: before save', + 'Person: before save', + 'Person: after save', + 'Passport: after save' + ]; + combinedHooks.should.eql(expected); + done(); }); }); @@ -2342,6 +2385,15 @@ describe('relations', function () { p.passportItem.destroy(function(err) { should.not.exist(err); should.equal(p.passport, null); + + var expected = [ + 'Passport: before delete', + 'Person: before save', + 'Person: after save', + 'Passport: after delete' + ]; + combinedHooks.should.eql(expected); + done(); }); }); @@ -2355,6 +2407,44 @@ describe('relations', function () { }); }); + it('should have notified hook observers', function() { + // the un-paired list of events stems from a ValidationError + var expected = [ + { hook: 'before save', instance: { name: 'Fredric' } }, + { hook: 'after save', instance: { name: 'Fredric' } }, + { hook: 'before save', instance: { name: undefined } }, + { hook: 'before save', + currentInstance: { name: 'Fredric' }, + data: { name: 'Freddy' } + }, + { hook: 'after save', instance: { name: 'Freddy' } }, + { hook: 'before delete', instance: { name: 'Freddy' } }, + { hook: 'after delete', instance: { name: 'Freddy' } } + ]; + hooks.should.eql(expected); + }); + + function observeHook(Model, type) { + Model.observe(type, function(ctx, next) { + combinedHooks.push(ctx.Model.modelName + ': ' + type); + next(); + }); + }; + + function observeEmbedded(Model, type) { + Model.observe(type, function(ctx, next) { + var info = { hook: type }; + if (ctx.currentInstance) { + info.currentInstance = ctx.currentInstance.toObject(); + } else if (ctx.instance) { + info.instance = ctx.instance.toObject(); + } + if (ctx.data) info.data = ctx.data; + hooks.push(info); + next(); + }); + }; + }); describe('embedsOne - persisted model', function () { @@ -2435,6 +2525,9 @@ describe('relations', function () { describe('embedsMany', function () { var address1, address2; + + var hooks = []; + var combinedHooks = []; before(function (done) { tmp = getTransientDataSource({defaultIdType: Number}); @@ -2448,8 +2541,28 @@ describe('relations', function () { }); }); + beforeEach(function() { + combinedHooks = []; + }); + it('can be declared', function (done) { Person.embedsMany(Address); + + observeHook(Person, 'before save'); + observeHook(Person, 'after save'); + observeHook(Person, 'before delete'); + observeHook(Person, 'after delete'); + + observeHook(Address, 'before save'); + observeHook(Address, 'after save'); + observeHook(Address, 'before delete'); + observeHook(Address, 'after delete'); + + observeEmbedded(Address, 'before save'); + observeEmbedded(Address, 'after save'); + observeEmbedded(Address, 'before delete'); + observeEmbedded(Address, 'after delete'); + db.automigrate(done); }); @@ -2473,6 +2586,17 @@ describe('relations', function () { address1 = address; should.exist(address1.id); address1.street.should.equal('Street 1'); + + var expected = [ + 'Person: before save', + 'Person: after save', + 'Address: before save', + 'Person: before save', + 'Person: after save', + 'Address: after save' + ]; + combinedHooks.should.eql(expected); + done(); }); }); @@ -2485,6 +2609,15 @@ describe('relations', function () { address2 = address; should.exist(address2.id); address2.street.should.equal('Street 2'); + + var expected = [ + 'Address: before save', + 'Person: before save', + 'Person: after save', + 'Address: after save' + ]; + combinedHooks.should.eql(expected); + done(); }); }); @@ -2608,6 +2741,15 @@ describe('relations', function () { p.addressList.destroy(address1.id, function(err) { should.not.exist(err); p.addresses.should.have.length(1); + + var expected = [ + 'Address: before delete', + 'Person: before save', + 'Person: after save', + 'Address: after delete' + ]; + combinedHooks.should.eql(expected); + done(); }); }); @@ -2619,6 +2761,58 @@ describe('relations', function () { done(); }); }); + + it('should have notified hook observers', function() { + // the un-paired list of events stems from a ValidationError + var expected = [ + { hook: 'before save', instance: { street: 'Street 1', id: 1 } }, + { hook: 'after save', instance: { street: 'Street 1', id: 1 } }, + { hook: 'before save', instance: { street: 'Street 2', id: 2 } }, + { hook: 'after save', instance: { street: 'Street 2', id: 2 } }, + { hook: 'before save', + currentInstance: { street: 'Street 2', id: 2 }, + data: { street: 'New Street' } }, + { hook: 'after save', + instance: { street: 'New Street', id: 2 } }, + { hook: 'before save', + currentInstance: { street: 'New Street', id: 2 }, + data: { street: null } }, + { hook: 'before save', + currentInstance: { street: 'Street 1', id: 1 }, + data: { street: 'Changed 1' } }, + { hook: 'after save', instance: { street: 'Changed 1', id: 1 } }, + { hook: 'before save', + currentInstance: { street: 'New Street', id: 2 }, + data: { street: 'Changed 2' } }, + { hook: 'after save', instance: { street: 'Changed 2', id: 2 } }, + { hook: 'before delete', + instance: { street: 'Street 1', id: 1 } }, + { hook: 'after delete', + instance: { street: 'Street 1', id: 1 } } + ]; + hooks.should.eql(expected); + }); + + function observeHook(Model, type) { + Model.observe(type, function(ctx, next) { + combinedHooks.push(ctx.Model.modelName + ': ' + type); + next(); + }); + }; + + function observeEmbedded(Model, type) { + Model.observe(type, function(ctx, next) { + var info = { hook: type }; + if (ctx.currentInstance) { + info.currentInstance = ctx.currentInstance.toObject(); + } else if (ctx.instance) { + info.instance = ctx.instance.toObject(); + } + if (ctx.data) info.data = ctx.data; + hooks.push(info); + next(); + }); + }; });