ModelBaseClass: implement async observe/notify
Implement infrastructure for intent-based hooks.
This commit is contained in:
parent
f9b0ac482c
commit
b3d07ebbe8
|
@ -29,7 +29,7 @@ var slice = Array.prototype.slice;
|
|||
|
||||
/**
|
||||
* ModelBuilder - A builder to define data models.
|
||||
*
|
||||
*
|
||||
* @property {Object} definitions Definitions of the models.
|
||||
* @property {Object} models Model constructors
|
||||
* @class
|
||||
|
@ -57,7 +57,7 @@ function isModelClass(cls) {
|
|||
|
||||
/**
|
||||
* Get a model by name.
|
||||
*
|
||||
*
|
||||
* @param {String} name The model name
|
||||
* @param {Boolean} forceCreate Whether the create a stub for the given name if a model doesn't exist.
|
||||
* @returns {*} The model class
|
||||
|
@ -101,7 +101,7 @@ ModelBuilder.prototype.getModelDefinition = function (name) {
|
|||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {String} className Name of class
|
||||
* @param {String} className Name of class
|
||||
* @param {Object} properties Hash of class properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
|
||||
* @param {Object} settings Other configuration of class
|
||||
* @return newly created class
|
||||
|
@ -112,10 +112,10 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
var args = slice.call(arguments);
|
||||
var pluralName = (settings && settings.plural) ||
|
||||
inflection.pluralize(className);
|
||||
|
||||
|
||||
var httpOptions = (settings && settings.http) || {};
|
||||
var pathName = httpOptions.path || pluralName;
|
||||
|
||||
|
||||
if (!className) {
|
||||
throw new Error('Class name required');
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -304,12 +305,12 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
* ```js
|
||||
* var user = loopback.Model.extend('user', properties, options);
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param {String} className Name of the new model being defined.
|
||||
* @options {Object} properties Properties to define for the model, added to properties of model being extended.
|
||||
* @options {Object} settings Model settings, such as relations and acls.
|
||||
*
|
||||
*/
|
||||
*/
|
||||
ModelClass.extend = function (className, subclassProperties, subclassSettings) {
|
||||
var properties = ModelClass.definition.properties;
|
||||
var settings = ModelClass.definition.settings;
|
||||
|
@ -461,7 +462,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
modelBuilder.mixins.applyMixin(ModelClass, name, mixin);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ModelClass.emit('defined', ModelClass);
|
||||
|
||||
return ModelClass;
|
||||
|
@ -499,7 +500,7 @@ ModelBuilder.prototype.defineValueType = function(type, aliases) {
|
|||
*
|
||||
* Example:
|
||||
* Instead of extending a model with attributes like this (for example):
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
* db.defineProperty('Content', 'competitionType',
|
||||
* { type: String });
|
||||
|
@ -518,7 +519,7 @@ ModelBuilder.prototype.defineValueType = function(type, aliases) {
|
|||
*```
|
||||
*
|
||||
* @param {String} model Name of model
|
||||
* @options {Object} properties JSON object specifying properties. Each property is a key whos value is
|
||||
* @options {Object} properties JSON object specifying properties. Each property is a key whos value is
|
||||
* either the [type](http://docs.strongloop.com/display/LB/LoopBack+types) or `propertyName: {options}`
|
||||
* where the options are described below.
|
||||
* @property {String} type Datatype of property: Must be an [LDL type](http://docs.strongloop.com/display/LB/LoopBack+types).
|
||||
|
|
48
lib/model.js
48
lib/model.js
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue