ModelBaseClass: implement async observe/notify

Implement infrastructure for intent-based hooks.
This commit is contained in:
Miroslav Bajtoš 2015-01-19 13:39:31 +01:00
parent f9b0ac482c
commit b3d07ebbe8
3 changed files with 162 additions and 10 deletions

View File

@ -199,6 +199,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
hiddenProperty(ModelClass, 'relations', {});
hiddenProperty(ModelClass, 'http', { path: '/' + pathName });
hiddenProperty(ModelClass, 'base', ModelBaseClass);
hiddenProperty(ModelClass, '_observers', {});
// inherit ModelBaseClass static methods
for (var i in ModelBaseClass) {

View File

@ -7,6 +7,7 @@ module.exports = ModelBaseClass;
* Module dependencies
*/
var async = require('async');
var util = require('util');
var jutil = require('./jutil');
var List = require('./list');
@ -503,5 +504,52 @@ ModelBaseClass.prototype.setStrict = function (strict) {
this.__strict = strict;
};
/**
* Register an asynchronous observer for the given operation (event).
* @param {String} operation The operation name.
* @callback {function} listener The listener function. It will be invoked with
* `this` set to the model constructor, e.g. `User`.
* @param {Object} context Operation-specific context.
* @param {function(Error=)} next The callback to call when the observer
* has finished.
* @end
*/
ModelBaseClass.observe = function(operation, listener) {
if (!this._observers[operation]) {
this._observers[operation] = [];
}
this._observers[operation].push(listener);
};
/**
* Invoke all async observers for the given operation.
* @param {String} operation The operation name.
* @param {Object} context Operation-specific context.
* @param {function(Error=)} callback The callback to call when all observers
* has finished.
*/
ModelBaseClass.notifyObserversOf = function(operation, context, callback) {
var observers = this._observers && this._observers[operation];
this._notifyBaseObservers(operation, context, function doNotify(err) {
if (err) return callback(err, context);
if (!observers || !observers.length) return callback(null, context);
async.eachSeries(
observers,
function(fn, next) { fn(context, next); },
function(err) { callback(err, context) }
);
});
}
ModelBaseClass._notifyBaseObservers = function(operation, context, callback) {
if (this.base && this.base.notifyObserversOf)
this.base.notifyObserversOf(operation, context, callback);
else
callback();
}
jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable);

103
test/async-observer.test.js Normal file
View File

@ -0,0 +1,103 @@
var ModelBuilder = require('../').ModelBuilder;
var should = require('./init');
describe('async observer', function() {
var TestModel;
beforeEach(function defineTestModel() {
var modelBuilder = new ModelBuilder();
TestModel = modelBuilder.define('TestModel', { name: String });
});
it('calls registered async observers', function(done) {
var notifications = [];
TestModel.observe('before', pushAndNext(notifications, 'before'));
TestModel.observe('after', pushAndNext(notifications, 'after'));
TestModel.notifyObserversOf('before', {}, function(err) {
if (err) return done(err);
notifications.push('call');
TestModel.notifyObserversOf('after', {}, function(err) {
if (err) return done(err);
notifications.should.eql(['before', 'call', 'after']);
done();
});
});
});
it('allows multiple observers for the same operation', function(done) {
var notifications = [];
TestModel.observe('event', pushAndNext(notifications, 'one'));
TestModel.observe('event', pushAndNext(notifications, 'two'));
TestModel.notifyObserversOf('event', {}, function(err) {
if (err) return done(err);
notifications.should.eql(['one', 'two']);
done();
});
});
it('inherits observers from base model', function(done) {
var notifications = [];
TestModel.observe('event', pushAndNext(notifications, 'base'));
var Child = TestModel.extend('Child');
Child.observe('event', pushAndNext(notifications, 'child'));
Child.notifyObserversOf('event', {}, function(err) {
if (err) return done(err);
notifications.should.eql(['base', 'child']);
done();
});
});
it('does not modify observers in the base model', function(done) {
var notifications = [];
TestModel.observe('event', pushAndNext(notifications, 'base'));
var Child = TestModel.extend('Child');
Child.observe('event', pushAndNext(notifications, 'child'));
TestModel.notifyObserversOf('event', {}, function(err) {
if (err) return done(err);
notifications.should.eql(['base']);
done();
});
});
it('always calls inherited observers', function(done) {
var notifications = [];
TestModel.observe('event', pushAndNext(notifications, 'base'));
var Child = TestModel.extend('Child');
// Important: there are no observers on the Child model
Child.notifyObserversOf('event', {}, function(err) {
if (err) return done(err);
notifications.should.eql(['base']);
done();
});
});
it('handles no observers', function(done) {
TestModel.notifyObserversOf('no-observers', {}, function(err) {
// the test passes when no error was raised
done(err);
});
});
it('passes context to final callback', function(done) {
var context = {};
TestModel.notifyObserversOf('event', context, function(err, ctx) {
(ctx || "null").should.equal(context);
done();
});
});
});
function pushAndNext(array, value) {
return function(ctx, next) {
array.push(value);
process.nextTick(next);
};
}