From 5ea5da0153fb94fe6069670b71137a685b2ca4bf Mon Sep 17 00:00:00 2001 From: Anatoliy Chakkaev Date: Mon, 25 Mar 2013 01:25:10 +0400 Subject: [PATCH] Rewrite hooks API This commit breaks compatibility, but adds consistent API and allow modify data of update --- lib/abstract-class.js | 105 ++++++++++++---------- test/hooks.test.js | 198 ++++++++++++++++++++++++++++++++++++++++++ test/init.js | 11 +++ 3 files changed, 268 insertions(+), 46 deletions(-) create mode 100644 test/hooks.test.js create mode 100644 test/init.js diff --git a/lib/abstract-class.js b/lib/abstract-class.js index d885ed83..138f0ae1 100644 --- a/lib/abstract-class.js +++ b/lib/abstract-class.js @@ -74,7 +74,7 @@ AbstractClass.prototype._initProperties = function (data, applySetters) { ctor.forEachProperty(function (attr) { - if (!self.__data.hasOwnProperty(attr)) { + if ('undefined' === typeof self.__data[attr]) { self.__data[attr] = self.__dataWas[attr] = getDefault(attr); } else { self.__dataWas[attr] = self.__data[attr]; @@ -110,11 +110,11 @@ AbstractClass.prototype._initProperties = function (data, applySetters) { return def; } } else { - return null; + return undefined; } } - this.trigger("initialize"); + this.trigger('initialize'); } /** @@ -181,17 +181,21 @@ AbstractClass.create = function (data, callback) { obj = new this(data); data = obj.toObject(true); - // validation required - obj.isValid(function (valid) { - if (!valid) { - callback(new Error('Validation error'), obj); - } else { - create(); - } - }); + obj.trigger('save', function(saveDone) { + + // validation required + obj.isValid(function (valid) { + if (!valid) { + callback(new Error('Validation error'), obj); + } else { + create(saveDone); + } + }); + + }, obj); } - function create() { + function create(saveDone) { obj.trigger('create', function (done) { var data = this.toObject(true); // Added this to fix the beforeCreate trigger not fire. @@ -207,12 +211,18 @@ AbstractClass.create = function (data, callback) { obj._rev = rev } done.call(this, function () { - if (callback) { + if (saveDone) { + saveDone.call(obj, function () { + if (callback) { + callback(err, obj); + } + }); + } else if (callback) { callback(err, obj); } }); }.bind(this)); - }); + }, obj); } }; @@ -646,7 +656,7 @@ AbstractClass.prototype.save = function (options, callback) { }); } - }); + }, this); } }; @@ -753,47 +763,50 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) { var inst = this; var model = this.constructor.modelName; - if(!data) data = {}; + if (typeof data === 'function') { + cb = data; + data = null; + } + + if (!data) { + data = {}; + } // update instance's properties Object.keys(data).forEach(function (key) { inst[key] = data[key]; }); - inst.isValid(function (valid) { - if (!valid) { - if (cb) { - cb(new Error('Validation error'), inst); - } - } else { - update(); - } - }); + inst.trigger('save', function (saveDone) { + inst.trigger('update', function (done) { - function update() { - inst.trigger('save', function (saveDone) { - inst.trigger('update', function (done) { - - Object.keys(data).forEach(function (key) { - data[key] = inst[key]; - }); - - inst._adapter().updateAttributes(model, inst.id, inst.constructor._forDB(data), function (err) { - if (!err) { - // update _was attrs - Object.keys(data).forEach(function (key) { - inst.__dataWas[key] = inst.__data[key]; - }); + inst.isValid(function (valid) { + if (!valid) { + if (cb) { + cb(new Error('Validation error'), inst); } - done.call(inst, function () { - saveDone.call(inst, function () { - cb(err, inst); + } else { + Object.keys(data).forEach(function (key) { + inst[key] = data[key]; + }); + + inst._adapter().updateAttributes(model, inst.id, inst.constructor._forDB(data), function (err) { + if (!err) { + // update _was attrs + Object.keys(data).forEach(function (key) { + inst.__dataWas[key] = inst.__data[key]; + }); + } + done.call(inst, function () { + saveDone.call(inst, function () { + cb(err, inst); + }); }); }); - }); - }, data); - }); - } + } + }); + }, data); + }, data); }; AbstractClass.prototype.fromObject = function (obj) { diff --git a/test/hooks.test.js b/test/hooks.test.js new file mode 100644 index 00000000..9e334e28 --- /dev/null +++ b/test/hooks.test.js @@ -0,0 +1,198 @@ +var j = require('../'), + should = require('should'), + Schema = j.Schema, + AbstractClass = j.AbstractClass, + Hookable = j.Hookable, + + db, User; + +describe('hooks', function() { + + before(function() { + db = new Schema('memory'); + + User = db.define('User', { + email: String, + name: String, + password: String, + state: String + }); + }); + + describe('initialize', function() { + + afterEach(function() { + User.afterInitialize = null; + }); + + it('should be triggered on new', function(done) { + User.afterInitialize = function() { + done(); + }; + new User; + }); + + it('should be triggered on create', function(done) { + var user; + User.afterInitialize = function() { + if (this.name === 'Nickolay') { + this.name += ' Rozental'; + } + }; + User.create({name: 'Nickolay'}, function(err, u) { + u.id.should.be.a('number'); + u.name.should.equal('Nickolay Rozental'); + done(); + }); + }); + + }); + + describe('create', function() { + + afterEach(removeHooks('Create')); + + it('should be triggered on create', function(done) { + addHooks('Create', done); + User.create(); + }); + + it('should not be triggered on new', function() { + User.beforeCreate = function(next) { + should.fail('This should not be called'); + next(); + }; + var u = new User; + }); + + it('should be triggered on new+save', function(done) { + addHooks('Create', done); + (new User).save(); + }); + + }); + + describe('save', function() { + afterEach(removeHooks('Save')); + + it('should be triggered on create', function(done) { + addHooks('Save', done); + User.create(); + }); + + it('should be triggered on new+save', function(done) { + addHooks('Save', done); + (new User).save(); + }); + + it('should be triggered on updateAttributes', function(done) { + User.create(function(err, user) { + addHooks('Save', done); + user.updateAttributes({name: 'Anatoliy'}); + }); + }); + + it('should be triggered on save', function(done) { + User.create(function(err, user) { + addHooks('Save', done); + user.name = 'Hamburger'; + user.save(); + }); + }); + + it('should save full object', function(done) { + User.create(function(err, user) { + User.beforeSave = function(next, data) { + data.toObject().should.have.keys('id', 'name', 'email', + 'password', 'state') + done(); + }; + user.save(); + }); + }); + }); + + describe('update', function() { + afterEach(removeHooks('Update')); + + it('should not be triggered on create', function() { + User.beforeUpdate = function(next) { + should.fail('This should not be called'); + next(); + }; + User.create(); + }); + + it('should not be triggered on new+save', function() { + User.beforeUpdate = function(next) { + should.fail('This should not be called'); + next(); + }; + (new User).save(); + }); + + it('should be triggered on updateAttributes', function(done) { + User.create(function (err, user) { + addHooks('Update', done); + user.updateAttributes({name: 'Anatoliy'}); + }); + }); + + it('should be triggered on save', function(done) { + User.create(function (err, user) { + addHooks('Update', done); + user.name = 'Hamburger'; + user.save(); + }); + }); + + it('should update limited set of fields', function(done) { + User.create(function (err, user) { + User.beforeUpdate = function(next, data) { + data.should.have.keys('name', 'email'); + done(); + }; + user.updateAttributes({name: 1, email: 2}); + }); + }); + }); + + describe('destroy', function() { + afterEach(removeHooks('Destroy')); + + it('should be triggered on destroy', function() { + var hook = 'not called'; + User.beforeDestroy = function() { + hook = 'called'; + }; + User.afterDestroy = function() { + hook.should.eql('called'); + done(); + }; + User.create(function (err, user) { + user.destroy(); + }); + }); + }); +}); + +function addHooks(name, done) { + var called = false, random = Math.floor(Math.random() * 1000); + User['before' + name] = function(next, data) { + called = true; + data.email = random; + next(); + }; + User['after' + name] = function(next) { + (new Boolean(called)).should.equal(true); + this.email.should.equal(random); + done(); + }; +} + +function removeHooks(name) { + return function() { + User['after' + name] = null; + User['before' + name] = null; + }; +} diff --git a/test/init.js b/test/init.js new file mode 100644 index 00000000..6d13dbec --- /dev/null +++ b/test/init.js @@ -0,0 +1,11 @@ +require('should'); + +if (!process.env.TRAVIS) { + if (typeof __cov === 'undefined') { + process.on('exit', function () { + require('semicov').report(); + }); + } + + require('semicov').init('lib'); +}