Merge pull request #245 from anatoliychakkaev/master

Hooks, validations, lifecycle
This commit is contained in:
Anatoliy Chakkaev 2013-03-28 04:30:58 -07:00
commit e16219eb61
9 changed files with 283 additions and 114 deletions

View File

@ -2,4 +2,4 @@ language: node_js
node_js: node_js:
- 0.6 - 0.6
- 0.8 - 0.8
- 0.9 - 0.10

View File

@ -5,8 +5,8 @@ function Hookable() {
}; };
Hookable.afterInitialize = null; Hookable.afterInitialize = null;
Hookable.beforeValidation = null; Hookable.beforeValidate = null;
Hookable.afterValidation = null; Hookable.afterValidate = null;
Hookable.beforeSave = null; Hookable.beforeSave = null;
Hookable.afterSave = null; Hookable.afterSave = null;
Hookable.beforeCreate = null; Hookable.beforeCreate = null;
@ -18,8 +18,12 @@ Hookable.afterDestroy = null;
Hookable.prototype.trigger = function trigger(actionName, work, data) { Hookable.prototype.trigger = function trigger(actionName, work, data) {
var capitalizedName = capitalize(actionName); var capitalizedName = capitalize(actionName);
var afterHook = this.constructor["after" + capitalizedName];
var beforeHook = this.constructor["before" + capitalizedName]; var beforeHook = this.constructor["before" + capitalizedName];
var afterHook = this.constructor["after" + capitalizedName];
if (actionName === 'validate') {
beforeHook = beforeHook || this.constructor.beforeValidation;
afterHook = afterHook || this.constructor.afterValidation;
}
var inst = this; var inst = this;
// we only call "before" hook when we have actual action (work) to perform // we only call "before" hook when we have actual action (work) to perform

View File

@ -171,59 +171,49 @@ AbstractClass.create = function (data, callback) {
callback = function () {}; callback = function () {};
} }
var obj = null; var obj;
// if we come from save // if we come from save
if (data instanceof this && !data.id) { if (data instanceof this && !data.id) {
obj = data; obj = data;
data = obj.toObject(true);
obj._initProperties(data, false);
create();
} else { } else {
obj = new this(data); obj = new this(data);
data = obj.toObject(true);
obj.trigger('save', function(saveDone) {
// validation required
obj.isValid(function (valid) {
if (!valid) {
callback(new Error('Validation error'), obj);
} else {
create(saveDone);
}
});
}, obj);
} }
data = obj.toObject(true);
function create(saveDone) { // validation required
obj.trigger('create', function (done) { obj.isValid(function(valid) {
if (valid) {
create();
} else {
callback(new Error('Validation error'), obj);
}
}, data);
var data = this.toObject(true); // Added this to fix the beforeCreate trigger not fire. function create() {
// The fix is per issue #72 and the fix was found by by5739. obj.trigger('create', function(createDone) {
obj.trigger('save', function(saveDone) {
this._adapter().create(modelName, this.constructor._forDB(data), function (err, id, rev) { this._adapter().create(modelName, this.constructor._forDB(data), function (err, id, rev) {
if (id) { obj._initProperties(data, false);
obj.__data.id = id; if (id) {
obj.__dataWas.id = id; obj.__data.id = id;
defineReadonlyProp(obj, 'id', id); obj.__dataWas.id = id;
} defineReadonlyProp(obj, 'id', id);
if (rev) {
obj._rev = rev
}
done.call(this, function () {
if (saveDone) {
saveDone.call(obj, function () {
if (callback) {
callback(err, obj);
}
});
} else if (callback) {
callback(err, obj);
} }
if (rev) {
obj._rev = rev
}
if (err) {
return callback(err, obj);
}
saveDone.call(obj, function () {
createDone.call(obj, function () {
callback(err, obj);
});
});
}); });
}.bind(this)); }, data);
}, obj); }, data);
} }
}; };
@ -618,52 +608,50 @@ AbstractClass.prototype.save = function (options, callback) {
options.throws = false; options.throws = false;
} }
if (options.validate) { var inst = this;
this.isValid(function (valid) { var data = inst.toObject(true);
if (valid) { var Model = this.constructor;
save.call(this); var modelName = Model.modelName;
} else {
var err = new Error('Validation error'); if (!this.id) {
// throws option is dangerous for async usage return Model.create(this, callback);
if (options.throws) {
throw err;
}
callback(err, this);
}
}.bind(this));
} else {
save.call(this);
} }
// validate first
if (!options.validate) {
return save();
}
inst.isValid(function (valid) {
if (valid) {
save();
} else {
var err = new Error('Validation error');
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, inst);
}
});
// then save
function save() { function save() {
this.trigger('save', function (saveDone) { inst.trigger('save', function (saveDone) {
var modelName = this.constructor.modelName; inst.trigger('update', function (updateDone) {
var data = this.toObject(true); inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
var inst = this; if (err) {
if (inst.id) { return callback(err, inst);
inst.trigger('update', function (updateDone) { }
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) { inst._initProperties(data, false);
if (err) { updateDone.call(inst, function () {
console.log(err); saveDone.call(inst, function () {
} else { callback(err, inst);
inst._initProperties(data, false);
}
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
});
}); });
}); });
}, data);
} else {
inst.constructor.create(inst, function (err) {
saveDone.call(inst, function () {
callback(err, inst);
});
}); });
} }, data);
}, data);
}, this);
} }
}; };
@ -784,15 +772,15 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
inst[key] = data[key]; inst[key] = data[key];
}); });
inst.trigger('save', function (saveDone) { inst.isValid(function (valid) {
inst.trigger('update', function (done) { if (!valid) {
if (cb) {
cb(new Error('Validation error'), inst);
}
} else {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (done) {
inst.isValid(function (valid) {
if (!valid) {
if (cb) {
cb(new Error('Validation error'), inst);
}
} else {
Object.keys(data).forEach(function (key) { Object.keys(data).forEach(function (key) {
inst[key] = data[key]; inst[key] = data[key];
}); });
@ -810,9 +798,9 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
}); });
}); });
}); });
} }, data);
}); }, data);
}, data); }
}, data); }, data);
}; };

View File

@ -332,14 +332,18 @@ function getConfigurator(name, opts) {
* }); * });
* ``` * ```
*/ */
Validatable.prototype.isValid = function (callback) { Validatable.prototype.isValid = function (callback, data) {
var valid = true, inst = this, wait = 0, async = false; var valid = true, inst = this, wait = 0, async = false;
// exit with success when no errors // exit with success when no errors
if (!this.constructor._validations) { if (!this.constructor._validations) {
cleanErrors(this); cleanErrors(this);
if (callback) { if (callback) {
callback(valid); this.trigger('validate', function (validationsDone) {
validationsDone.call(inst, function() {
callback(valid);
});
});
} }
return valid; return valid;
} }
@ -350,7 +354,7 @@ Validatable.prototype.isValid = function (callback) {
value: new Errors value: new Errors
}); });
this.trigger('validation', function (validationsDone) { this.trigger('validate', function (validationsDone) {
var inst = this, var inst = this,
asyncFail = false; asyncFail = false;
@ -370,20 +374,20 @@ Validatable.prototype.isValid = function (callback) {
}); });
if (!async) { if (!async) {
validationsDone(); validationsDone.call(inst, callback);
} }
function done(fail) { function done(fail) {
asyncFail = asyncFail || fail; asyncFail = asyncFail || fail;
if (--wait === 0 && callback) { if (--wait === 0 && callback) {
validationsDone.call(inst, function () { validationsDone.call(inst, function () {
if( valid && !asyncFail ) cleanErrors(inst); if (valid && !asyncFail) cleanErrors(inst);
callback(valid && !asyncFail); callback(valid && !asyncFail);
}); });
} }
} }
}); }, data);
if (!async) { if (!async) {
if (valid) cleanErrors(this); if (valid) cleanErrors(this);

View File

@ -146,7 +146,7 @@ describe('basic-querying', function() {
User.findOne(function(e, u) { User.findOne(function(e, u) {
should.not.exist(e); should.not.exist(e);
should.exist(u); should.exist(u);
u.id.should.equal(users[0].id); u.id.toString().should.equal(users[0].id.toString());
done(); done();
}); });
}); });
@ -185,6 +185,16 @@ describe('basic-querying', function() {
}); });
}); });
it('should work even when find by id', function(done) {
User.findOne(function(e, u) {
User.findOne({where: {id: u.id}}, function(err, user) {
should.not.exist(err);
should.exist(user);
done();
});
});
});
}); });
describe('exists', function() { describe('exists', function() {

View File

@ -1,2 +1,3 @@
require('./basic-querying.test.js'); require('./basic-querying.test.js');
require('./hooks.test.js'); require('./hooks.test.js');
require('./relations.test.js');

View File

@ -42,7 +42,7 @@ describe('hooks', function() {
} }
}; };
User.create({name: 'Nickolay'}, function(err, u) { User.create({name: 'Nickolay'}, function(err, u) {
u.id.should.be.a('number'); u.id.should.be.ok;
u.name.should.equal('Nickolay Rozental'); u.name.should.equal('Nickolay Rozental');
done(); done();
}); });
@ -105,7 +105,7 @@ describe('hooks', function() {
it('should save full object', function(done) { it('should save full object', function(done) {
User.create(function(err, user) { User.create(function(err, user) {
User.beforeSave = function(next, data) { User.beforeSave = function(next, data) {
data.toObject().should.have.keys('id', 'name', 'email', data.should.have.keys('id', 'name', 'email',
'password', 'state') 'password', 'state')
done(); done();
}; };
@ -207,10 +207,11 @@ describe('hooks', function() {
describe('destroy', function() { describe('destroy', function() {
afterEach(removeHooks('Destroy')); afterEach(removeHooks('Destroy'));
it('should be triggered on destroy', function() { it('should be triggered on destroy', function(done) {
var hook = 'not called'; var hook = 'not called';
User.beforeDestroy = function() { User.beforeDestroy = function(next) {
hook = 'called'; hook = 'called';
next();
}; };
User.afterDestroy = function() { User.afterDestroy = function() {
hook.should.eql('called'); hook.should.eql('called');
@ -221,6 +222,96 @@ describe('hooks', function() {
}); });
}); });
}); });
describe('lifecycle', function() {
var life = [], user;
before(function(done) {
User.beforeSave = function(d){life.push('beforeSave'); d();};
User.beforeCreate = function(d){life.push('beforeCreate'); d();};
User.beforeUpdate = function(d){life.push('beforeUpdate'); d();};
User.beforeDestroy = function(d){life.push('beforeDestroy');d();};
User.beforeValidate = function(d){life.push('beforeValidate');d();};
User.afterInitialize= function( ){life.push('afterInitialize'); };
User.afterSave = function(d){life.push('afterSave'); d();};
User.afterCreate = function(d){life.push('afterCreate'); d();};
User.afterUpdate = function(d){life.push('afterUpdate'); d();};
User.afterDestroy = function(d){life.push('afterDestroy'); d();};
User.afterValidate = function(d){life.push('afterValidate');d();};
User.create(function(e, u) {
user = u;
life = [];
done();
});
});
beforeEach(function() {
life = [];
});
it('should describe create sequence', function(done) {
User.create(function() {
life.should.eql([
'afterInitialize',
'beforeValidate',
'afterValidate',
'beforeCreate',
'beforeSave',
'afterInitialize',
'afterSave',
'afterCreate'
]);
done();
});
});
it('should describe new+save sequence', function(done) {
var u = new User;
u.save(function() {
life.should.eql([
'afterInitialize',
'beforeValidate',
'afterValidate',
'beforeCreate',
'beforeSave',
'afterInitialize',
'afterSave',
'afterCreate'
]);
done();
});
});
it('should describe new+save sequence', function(done) {
var u = new User;
u.save(function() {
life.should.eql([
'afterInitialize',
'beforeValidate',
'afterValidate',
'beforeCreate',
'beforeSave',
'afterInitialize',
'afterSave',
'afterCreate'
]);
done();
});
});
it('should describe updateAttributes sequence', function(done) {
user.updateAttributes({name: 'Antony'}, function() {
life.should.eql([
'beforeValidate',
'afterValidate',
'beforeSave',
'beforeUpdate',
'afterUpdate',
'afterSave',
]);
done();
});
});
});
}); });
function addHooks(name, done) { function addHooks(name, done) {

View File

@ -76,10 +76,68 @@ describe('manipulation', function() {
}); });
describe('save', function() { describe('save', function() {
it('should save new object');
it('should save existing object'); it('should save new object', function(done) {
it('should save invalid object (skipping validation)'); var p = new Person;
it('should save throw error on validation'); p.save(function(err) {
should.not.exist(err);
should.exist(p.id);
done();
});
});
it('should save existing object', function(done) {
Person.findOne(function(err, p) {
should.not.exist(err);
p.name = 'Hans';
p.propertyChanged('name').should.be.true;
p.save(function(err) {
should.not.exist(err);
p.propertyChanged('name').should.be.false;
Person.findOne(function(err, p) {
should.not.exist(err);
p.name.should.equal('Hans');
p.propertyChanged('name').should.be.false;
done();
});
});
});
});
it('should save invalid object (skipping validation)', function(done) {
Person.findOne(function(err, p) {
should.not.exist(err);
p.isValid = function(done) {
process.nextTick(done);
return false;
};
p.name = 'Nana';
p.save(function(err) {
should.exist(err);
p.propertyChanged('name').should.be.true;
p.save({validate: false}, function(err) {
should.not.exist(err);
p.propertyChanged('name').should.be.false;
done();
});
});
});
});
it('should save throw error on validation', function() {
Person.findOne(function(err, p) {
should.not.exist(err);
p.isValid = function(cb) {
cb(false);
return false;
};
(function() {
p.save({
'throws': true
});
}).should.throw('Validation error');
});
});
}); });
describe('destroy', function() { describe('destroy', function() {

View File

@ -43,7 +43,7 @@ describe('relations', function() {
(new Author).readers.should.be.an.instanceOf(Function); (new Author).readers.should.be.an.instanceOf(Function);
Object.keys((new Reader).toObject()).should.include('authorId'); Object.keys((new Reader).toObject()).should.include('authorId');
db.automigrate(done); db.autoupdate(done);
}); });
it('should build record on scope', function(done) { it('should build record on scope', function(done) {
@ -115,7 +115,20 @@ describe('relations', function() {
// (new Fear).mind.build().should.be.an.instanceOf(Mind); // (new Fear).mind.build().should.be.an.instanceOf(Mind);
}); });
it('can be declared in short form'); it('can be used to query data', function(done) {
List.hasMany('todos', {model: Item});
List.create(function(e, list) {
should.not.exist(e);
should.exist(list);
list.todos.create(function(err, todo) {
todo.list(function(e, l) {
should.not.exist(e);
should.exist(l);
done();
});
});
});
});
}); });
describe('hasAndBelongsToMany', function() { describe('hasAndBelongsToMany', function() {