Merge pull request #235 from anatoliychakkaev/master

Rework hooks API
This commit is contained in:
Anatoliy Chakkaev 2013-03-24 14:30:20 -07:00
commit a7bc7ff40e
14 changed files with 322 additions and 155 deletions

View File

@ -1,11 +1,17 @@
# doc: ## TESTS
# makedoc lib/abstract-class.js lib/schema.js lib/validatable.js -t "JugglingDB API docs"
TESTER = ./node_modules/.bin/mocha
OPTS = --require ./test/init.js
TESTS = test/*.test.js
test: test:
@./node_modules/.bin/mocha --require should test/*.test.js $(TESTER) $(OPTS) $(TESTS)
test-verbose: test-verbose:
@./node_modules/.bin/mocha --require should --reporter spec test/*.test.js $(TESTER) $(OPTS) --reporter spec $(TESTS)
testing:
$(TESTER) $(OPTS) --watch $(TESTS)
## DOCS
MAN_DOCS = $(shell find docs -name '*.md' \ MAN_DOCS = $(shell find docs -name '*.md' \
|sed 's|.md|.3|g' \ |sed 's|.md|.3|g' \

View File

@ -5,7 +5,7 @@ jugglingdb-changelog(3) - The History of JugglingDB
### upcoming release ### upcoming release
* Documentation in web and man * Documentation in [web][http://jugglingdb.co] and man
### 0.2.1 ### 0.2.1

View File

@ -7,6 +7,39 @@ This section describes common methods of models managed by jugglingdb and
explains some model internals, such as data representation, setters, getters and explains some model internals, such as data representation, setters, getters and
virtual attributes. virtual attributes.
## ESSENTIALS
### Default values
## DB WRITE METHODS
### Model.create([data[, callback]])
Create instance of Model with given data and save to database, invoke callback
when ready. Callback accepts two arguments: error and model instance.
### Model.updateAttributes(data[, callback]);
### Model.updateAttributes(data[, callback]);
## DB READ METHODS
### Model.all([params[, callback]])
Find all instances of Model, matched by query. Fields used for filter and sort
should be declared with `{index: true}` in model definition.
* `param`:
* where: Object `{ key: val, key2: {gt: 'val2'}}`
* include: String, Object or Array. See AbstractClass.include documentation.
* order: String
* limit: Number
* skip: Number
* `callback`:
Accepts two arguments:
* err (null or Error)
* Array of instances
## SEE ALSO ## SEE ALSO
jugglingdb-schema(3) jugglingdb-schema(3)

View File

@ -2,10 +2,10 @@
* Module dependencies * Module dependencies
*/ */
var util = require('util'); var util = require('util');
var jutil = require('./jutil'); var jutil = require('./jutil.js');
var Validatable = require('./validatable').Validatable; var Validatable = require('./validations.js').Validatable;
var List = require('./list'); var List = require('./list.js');
var Hookable = require('./hookable').Hookable; var Hookable = require('./hooks.js').Hookable;
var DEFAULT_CACHE_LIMIT = 1000; var DEFAULT_CACHE_LIMIT = 1000;
var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text']; var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];
@ -74,7 +74,7 @@ AbstractClass.prototype._initProperties = function (data, applySetters) {
ctor.forEachProperty(function (attr) { ctor.forEachProperty(function (attr) {
if (!self.__data.hasOwnProperty(attr)) { if ('undefined' === typeof self.__data[attr]) {
self.__data[attr] = self.__dataWas[attr] = getDefault(attr); self.__data[attr] = self.__dataWas[attr] = getDefault(attr);
} else { } else {
self.__dataWas[attr] = self.__data[attr]; self.__dataWas[attr] = self.__data[attr];
@ -110,11 +110,11 @@ AbstractClass.prototype._initProperties = function (data, applySetters) {
return def; return def;
} }
} else { } else {
return null; return undefined;
} }
} }
this.trigger("initialize"); this.trigger('initialize');
} }
/** /**
@ -181,17 +181,21 @@ AbstractClass.create = function (data, callback) {
obj = new this(data); obj = new this(data);
data = obj.toObject(true); data = obj.toObject(true);
// validation required obj.trigger('save', function(saveDone) {
obj.isValid(function (valid) {
if (!valid) { // validation required
callback(new Error('Validation error'), obj); obj.isValid(function (valid) {
} else { if (!valid) {
create(); callback(new Error('Validation error'), obj);
} } else {
}); create(saveDone);
}
});
}, obj);
} }
function create() { function create(saveDone) {
obj.trigger('create', function (done) { obj.trigger('create', function (done) {
var data = this.toObject(true); // Added this to fix the beforeCreate trigger not fire. 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 obj._rev = rev
} }
done.call(this, function () { done.call(this, function () {
if (callback) { if (saveDone) {
saveDone.call(obj, function () {
if (callback) {
callback(err, obj);
}
});
} else if (callback) {
callback(err, obj); callback(err, obj);
} }
}); });
}.bind(this)); }.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 inst = this;
var model = this.constructor.modelName; var model = this.constructor.modelName;
if(!data) data = {}; if (typeof data === 'function') {
cb = data;
data = null;
}
if (!data) {
data = {};
}
// update instance's properties // update instance's properties
Object.keys(data).forEach(function (key) { Object.keys(data).forEach(function (key) {
inst[key] = data[key]; inst[key] = data[key];
}); });
inst.isValid(function (valid) { inst.trigger('save', function (saveDone) {
if (!valid) { inst.trigger('update', function (done) {
if (cb) {
cb(new Error('Validation error'), inst);
}
} else {
update();
}
});
function update() { inst.isValid(function (valid) {
inst.trigger('save', function (saveDone) { if (!valid) {
inst.trigger('update', function (done) { if (cb) {
cb(new Error('Validation error'), inst);
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];
});
} }
done.call(inst, function () { } else {
saveDone.call(inst, function () { Object.keys(data).forEach(function (key) {
cb(err, inst); 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) { AbstractClass.prototype.fromObject = function (obj) {

View File

@ -130,6 +130,8 @@ function applyFilter(filter) {
if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
return value.match(example); return value.match(example);
} }
if (typeof example === 'undefined') return undefined;
if (typeof value === 'undefined') return undefined;
// not strict equality // not strict equality
return (example !== null ? example.toString() : example) == (value !== null ? value.toString() : value); return (example !== null ? example.toString() : example) == (value !== null ? value.toString() : value);
} }

View File

@ -232,7 +232,7 @@ function testOrm(schema) {
}); });
it('should be exported to JSON', function (test) { it('should be exported to JSON', function (test) {
var outString = '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"id":1,"userId":null}' var outString = '{"title":"hello, json","date":1,"published":false,"likes":[],"related":[],"id":1}'
if (schema.name === 'nano') if (schema.name === 'nano')
outString = '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"_rev":null,"id":1,"userId":null}' outString = '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"_rev":null,"id":1,"userId":null}'

View File

@ -19,7 +19,6 @@ describe('defaults', function() {
it('should apply defaults on create', function(done) { it('should apply defaults on create', function(done) {
Server.create(function(err, s) { Server.create(function(err, s) {
s.port.should.equal(80); s.port.should.equal(80);
console.log(s.__data);
done(); done();
}); });
}); });

View File

@ -1,95 +0,0 @@
juggling = require('../index')
Schema = juggling.Schema
AbstractClass = juggling.AbstractClass
Hookable = juggling.Hookable
require('./spec_helper').init module.exports
schema = new Schema 'memory'
User = schema.define 'User',
email: String
name: String
password: String
state: String
age: Number
gender: String
domain: String
pendingPeriod: Number
createdByAdmin: Boolean
it "should trigger after initialize", (test) ->
User.afterInitialize = ->
User.afterInitialize = null
test.done()
user = new User
it "should trigger before create", (test) ->
User.beforeCreate = () ->
User.beforeCreate = null
test.done()
User.create -> test.ok "saved"
it "should trigger after create", (test) ->
User.afterCreate = (next) ->
User.afterCreate = null
next()
User.create ->
test.ok "saved"
test.done()
it 'should trigger before save', (test) ->
test.expect(3)
User.beforeSave = (next) ->
User.beforeSave = null
@name = 'mr. ' + @name
next()
user = new User name: 'Jonathan'
user.save ->
test.equals User.schema.adapter.cache.User[user.id].name, user.name
test.equals user.name, 'mr. Jonathan'
test.ok 'saved'
test.done()
it 'should trigger after save', (test) ->
User.afterSave = (next) ->
User.afterSave = null
next()
user = new User
user.save ->
test.ok "saved"
test.done()
it "should trigger before update", (test) ->
User.beforeUpdate = () ->
User.beforeUpdate = null
test.done()
User.create {}, (err, user) ->
user.updateAttributes email:"1@1.com", -> test.ok "updated"
it "should trigger after update", (test) ->
User.afterUpdate = () ->
User.afterUpdate = null
test.done()
User.create (err, user) ->
user.updateAttributes email: "1@1.com", -> test.ok "updated"
it "should trigger before destroy", (test)->
User.beforeDestroy = () ->
User.beforeDestroy = null
test.done()
User.create {}, (err, user) ->
user.destroy()
it "should trigger after destroy", (test) ->
User.afterDestroy = () ->
User.afterDestroy = null
test.done()
User.create (err, user) ->
user.destroy()
it 'allows me to modify attributes before saving', (test) ->
test.done()

198
test/hooks.test.js Normal file
View File

@ -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;
};
}

11
test/init.js Normal file
View File

@ -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');
}

View File

@ -4,7 +4,7 @@ var should = require('should');
describe('JSON property', function() { describe('JSON property', function() {
var schema, Model; var schema, Model;
it('could be defined', function() { it('should be defined', function() {
schema = new Schema('memory'); schema = new Schema('memory');
Model = schema.define('Model', {propertyName: Schema.JSON}); Model = schema.define('Model', {propertyName: Schema.JSON});
var m = new Model; var m = new Model;

View File

@ -1,6 +1,6 @@
if (!process.env.TRAVIS) { if (!process.env.TRAVIS) {
var semicov = require('semicov'); var semicov = require('semicov');
semicov.init('lib'); semicov.init('lib', 'JugglingDB');
process.on('exit', semicov.report); process.on('exit', semicov.report);
} }