Merge pull request #245 from anatoliychakkaev/master
Hooks, validations, lifecycle
This commit is contained in:
commit
e16219eb61
|
@ -2,4 +2,4 @@ language: node_js
|
|||
node_js:
|
||||
- 0.6
|
||||
- 0.8
|
||||
- 0.9
|
||||
- 0.10
|
||||
|
|
10
lib/hooks.js
10
lib/hooks.js
|
@ -5,8 +5,8 @@ function Hookable() {
|
|||
};
|
||||
|
||||
Hookable.afterInitialize = null;
|
||||
Hookable.beforeValidation = null;
|
||||
Hookable.afterValidation = null;
|
||||
Hookable.beforeValidate = null;
|
||||
Hookable.afterValidate = null;
|
||||
Hookable.beforeSave = null;
|
||||
Hookable.afterSave = null;
|
||||
Hookable.beforeCreate = null;
|
||||
|
@ -18,8 +18,12 @@ Hookable.afterDestroy = null;
|
|||
|
||||
Hookable.prototype.trigger = function trigger(actionName, work, data) {
|
||||
var capitalizedName = capitalize(actionName);
|
||||
var afterHook = this.constructor["after" + 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;
|
||||
|
||||
// we only call "before" hook when we have actual action (work) to perform
|
||||
|
|
102
lib/model.js
102
lib/model.js
|
@ -171,38 +171,30 @@ AbstractClass.create = function (data, callback) {
|
|||
callback = function () {};
|
||||
}
|
||||
|
||||
var obj = null;
|
||||
var obj;
|
||||
// if we come from save
|
||||
if (data instanceof this && !data.id) {
|
||||
obj = data;
|
||||
data = obj.toObject(true);
|
||||
obj._initProperties(data, false);
|
||||
create();
|
||||
} else {
|
||||
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);
|
||||
if (valid) {
|
||||
create();
|
||||
} else {
|
||||
create(saveDone);
|
||||
callback(new Error('Validation error'), obj);
|
||||
}
|
||||
});
|
||||
}, data);
|
||||
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function create(saveDone) {
|
||||
obj.trigger('create', function (done) {
|
||||
|
||||
var data = this.toObject(true); // Added this to fix the beforeCreate trigger not fire.
|
||||
// The fix is per issue #72 and the fix was found by by5739.
|
||||
function create() {
|
||||
obj.trigger('create', function(createDone) {
|
||||
obj.trigger('save', function(saveDone) {
|
||||
|
||||
this._adapter().create(modelName, this.constructor._forDB(data), function (err, id, rev) {
|
||||
obj._initProperties(data, false);
|
||||
if (id) {
|
||||
obj.__data.id = id;
|
||||
obj.__dataWas.id = id;
|
||||
|
@ -211,19 +203,17 @@ AbstractClass.create = function (data, callback) {
|
|||
if (rev) {
|
||||
obj._rev = rev
|
||||
}
|
||||
done.call(this, function () {
|
||||
if (saveDone) {
|
||||
if (err) {
|
||||
return callback(err, obj);
|
||||
}
|
||||
saveDone.call(obj, function () {
|
||||
if (callback) {
|
||||
createDone.call(obj, function () {
|
||||
callback(err, obj);
|
||||
}
|
||||
});
|
||||
} else if (callback) {
|
||||
callback(err, obj);
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}, obj);
|
||||
});
|
||||
}, data);
|
||||
}, data);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -618,36 +608,42 @@ AbstractClass.prototype.save = function (options, callback) {
|
|||
options.throws = false;
|
||||
}
|
||||
|
||||
if (options.validate) {
|
||||
this.isValid(function (valid) {
|
||||
var inst = this;
|
||||
var data = inst.toObject(true);
|
||||
var Model = this.constructor;
|
||||
var modelName = Model.modelName;
|
||||
|
||||
if (!this.id) {
|
||||
return Model.create(this, callback);
|
||||
}
|
||||
|
||||
// validate first
|
||||
if (!options.validate) {
|
||||
return save();
|
||||
}
|
||||
|
||||
inst.isValid(function (valid) {
|
||||
if (valid) {
|
||||
save.call(this);
|
||||
save();
|
||||
} else {
|
||||
var err = new Error('Validation error');
|
||||
// throws option is dangerous for async usage
|
||||
if (options.throws) {
|
||||
throw err;
|
||||
}
|
||||
callback(err, this);
|
||||
}
|
||||
}.bind(this));
|
||||
} else {
|
||||
save.call(this);
|
||||
callback(err, inst);
|
||||
}
|
||||
});
|
||||
|
||||
// then save
|
||||
function save() {
|
||||
this.trigger('save', function (saveDone) {
|
||||
var modelName = this.constructor.modelName;
|
||||
var data = this.toObject(true);
|
||||
var inst = this;
|
||||
if (inst.id) {
|
||||
inst.trigger('save', function (saveDone) {
|
||||
inst.trigger('update', function (updateDone) {
|
||||
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
inst._initProperties(data, false);
|
||||
return callback(err, inst);
|
||||
}
|
||||
inst._initProperties(data, false);
|
||||
updateDone.call(inst, function () {
|
||||
saveDone.call(inst, function () {
|
||||
callback(err, inst);
|
||||
|
@ -655,15 +651,7 @@ AbstractClass.prototype.save = function (options, callback) {
|
|||
});
|
||||
});
|
||||
}, data);
|
||||
} else {
|
||||
inst.constructor.create(inst, function (err) {
|
||||
saveDone.call(inst, function () {
|
||||
callback(err, inst);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}, this);
|
||||
}, data);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -784,15 +772,15 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
|
|||
inst[key] = data[key];
|
||||
});
|
||||
|
||||
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 {
|
||||
inst.trigger('save', function (saveDone) {
|
||||
inst.trigger('update', function (done) {
|
||||
|
||||
Object.keys(data).forEach(function (key) {
|
||||
inst[key] = data[key];
|
||||
});
|
||||
|
@ -810,10 +798,10 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
|
|||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}, data);
|
||||
}, data);
|
||||
}
|
||||
}, data);
|
||||
};
|
||||
|
||||
AbstractClass.prototype.fromObject = function (obj) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
// exit with success when no errors
|
||||
if (!this.constructor._validations) {
|
||||
cleanErrors(this);
|
||||
if (callback) {
|
||||
this.trigger('validate', function (validationsDone) {
|
||||
validationsDone.call(inst, function() {
|
||||
callback(valid);
|
||||
});
|
||||
});
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
@ -350,7 +354,7 @@ Validatable.prototype.isValid = function (callback) {
|
|||
value: new Errors
|
||||
});
|
||||
|
||||
this.trigger('validation', function (validationsDone) {
|
||||
this.trigger('validate', function (validationsDone) {
|
||||
var inst = this,
|
||||
asyncFail = false;
|
||||
|
||||
|
@ -370,7 +374,7 @@ Validatable.prototype.isValid = function (callback) {
|
|||
});
|
||||
|
||||
if (!async) {
|
||||
validationsDone();
|
||||
validationsDone.call(inst, callback);
|
||||
}
|
||||
|
||||
function done(fail) {
|
||||
|
@ -383,7 +387,7 @@ Validatable.prototype.isValid = function (callback) {
|
|||
}
|
||||
}
|
||||
|
||||
});
|
||||
}, data);
|
||||
|
||||
if (!async) {
|
||||
if (valid) cleanErrors(this);
|
||||
|
|
|
@ -146,7 +146,7 @@ describe('basic-querying', function() {
|
|||
User.findOne(function(e, u) {
|
||||
should.not.exist(e);
|
||||
should.exist(u);
|
||||
u.id.should.equal(users[0].id);
|
||||
u.id.toString().should.equal(users[0].id.toString());
|
||||
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() {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
require('./basic-querying.test.js');
|
||||
require('./hooks.test.js');
|
||||
require('./relations.test.js');
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('hooks', function() {
|
|||
}
|
||||
};
|
||||
User.create({name: 'Nickolay'}, function(err, u) {
|
||||
u.id.should.be.a('number');
|
||||
u.id.should.be.ok;
|
||||
u.name.should.equal('Nickolay Rozental');
|
||||
done();
|
||||
});
|
||||
|
@ -105,7 +105,7 @@ describe('hooks', function() {
|
|||
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',
|
||||
data.should.have.keys('id', 'name', 'email',
|
||||
'password', 'state')
|
||||
done();
|
||||
};
|
||||
|
@ -207,10 +207,11 @@ describe('hooks', function() {
|
|||
describe('destroy', function() {
|
||||
afterEach(removeHooks('Destroy'));
|
||||
|
||||
it('should be triggered on destroy', function() {
|
||||
it('should be triggered on destroy', function(done) {
|
||||
var hook = 'not called';
|
||||
User.beforeDestroy = function() {
|
||||
User.beforeDestroy = function(next) {
|
||||
hook = 'called';
|
||||
next();
|
||||
};
|
||||
User.afterDestroy = function() {
|
||||
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) {
|
||||
|
|
|
@ -76,10 +76,68 @@ describe('manipulation', function() {
|
|||
});
|
||||
|
||||
describe('save', function() {
|
||||
it('should save new object');
|
||||
it('should save existing object');
|
||||
it('should save invalid object (skipping validation)');
|
||||
it('should save throw error on validation');
|
||||
|
||||
it('should save new object', function(done) {
|
||||
var p = new Person;
|
||||
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() {
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('relations', function() {
|
|||
(new Author).readers.should.be.an.instanceOf(Function);
|
||||
Object.keys((new Reader).toObject()).should.include('authorId');
|
||||
|
||||
db.automigrate(done);
|
||||
db.autoupdate(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);
|
||||
});
|
||||
|
||||
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() {
|
||||
|
|
Loading…
Reference in New Issue