Merge pull request #403 from strongloop/feature/intent-hooks

Intent-based hooks for persistent models
This commit is contained in:
Miroslav Bajtoš 2015-01-29 08:59:28 +01:00
commit ce39f8ab01
9 changed files with 1774 additions and 210 deletions

View File

@ -594,6 +594,9 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
}
}
// Do not modify the data object passed in arguments
data = Object.create(data);
this.setIdValue(model, data, id);
var cachedModels = this.collection(model);

View File

@ -7,6 +7,7 @@ module.exports = DataAccessObject;
/*!
* Module dependencies
*/
var async = require('async');
var jutil = require('./jutil');
var ValidationError = require('./validations').ValidationError;
var Relation = require('./relations.js');
@ -62,6 +63,16 @@ function byIdQuery(m, id) {
return query;
}
function isWhereByGivenId(Model, where, idValue) {
var keys = Object.keys(where);
if (keys.length != 1) return false;
var pk = idName(Model);
if (keys[0] !== pk) return false;
return where[pk] === idValue;
}
DataAccessObject._forDB = function (data) {
if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
return data;
@ -133,7 +144,7 @@ DataAccessObject.create = function (data, callback) {
var Model = this;
var self = this;
if (typeof data === 'function') {
callback = data;
data = {};
@ -183,39 +194,42 @@ DataAccessObject.create = function (data, callback) {
}
}
}
var enforced = {};
var obj;
var idValue = getIdValue(this, data);
// if we come from save
if (data instanceof Model && !idValue) {
obj = data;
} else {
obj = new Model(data);
}
this.applyProperties(enforced, obj);
obj.setAttributes(enforced);
Model = this.lookupModel(data); // data-specific
if (Model !== obj.constructor) obj = new Model(data);
data = obj.toObject(true);
// validation required
obj.isValid(function (valid) {
if (valid) {
create();
} else {
callback(new ValidationError(obj), obj);
}
}, data);
Model.notifyObserversOf('before save', { Model: Model, instance: obj }, function(err) {
if (err) return callback(err);
data = obj.toObject(true);
// validation required
obj.isValid(function (valid) {
if (valid) {
create();
} else {
callback(new ValidationError(obj), obj);
}
}, data);
});
function create() {
obj.trigger('create', function (createDone) {
obj.trigger('save', function (saveDone) {
var _idName = idName(Model);
var modelName = Model.modelName;
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
@ -232,8 +246,13 @@ DataAccessObject.create = function (data, callback) {
obj.__persisted = true;
saveDone.call(obj, function () {
createDone.call(obj, function () {
callback(err, obj);
if(!err) Model.emit('changed', obj);
if (err) {
return callback(err, obj);
}
Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) {
callback(err, obj);
if(!err) Model.emit('changed', obj);
});
});
});
}, obj);
@ -267,45 +286,83 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
}
var self = this;
var Model = this;
if (!getIdValue(this, data)) {
var id = getIdValue(this, data);
if (!id) {
return this.create(data, callback);
}
if (this.getDataSource().connector.updateOrCreate) {
var update = data;
var inst = data;
if(!(data instanceof Model)) {
inst = new Model(data);
Model.notifyObserversOf('query', { Model: Model, query: byIdQuery(Model, id) }, doUpdateOrCreate);
function doUpdateOrCreate(err, ctx) {
if (err) return callback(err);
var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id)
if (Model.getDataSource().connector.updateOrCreate && isOriginalQuery) {
var context = { Model: Model, where: ctx.query.where, data: data };
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return callback(err);
data = ctx.data;
var update = data;
var inst = data;
if(!(data instanceof Model)) {
inst = new Model(data);
}
update = inst.toObject(false);
Model.applyProperties(update, inst);
Model = Model.lookupModel(update);
// FIXME(bajtos) validate the model!
// https://github.com/strongloop/loopback-datasource-juggler/issues/262
update = inst.toObject(true);
update = removeUndefined(update);
self.getDataSource().connector
.updateOrCreate(Model.modelName, update, done);
function done(err, data) {
var obj;
if (data && !(data instanceof Model)) {
inst._initProperties(data);
obj = inst;
} else {
obj = data;
}
if (err) {
callback(err, obj);
if(!err) {
Model.emit('changed', inst);
}
} else {
Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) {
callback(err, obj);
if(!err) {
Model.emit('changed', inst);
}
});
}
}
});
} else {
Model.findOne({ where: ctx.query.where }, { notify: false }, function (err, inst) {
if (err) {
return callback(err);
}
if (!isOriginalQuery) {
// The custom query returned from a hook may hide the fact that
// there is already a model with `id` value `data[idName(Model)]`
delete data[idName(Model)];
}
if (inst) {
inst.updateAttributes(data, callback);
} else {
Model = self.lookupModel(data);
var obj = new Model(data);
obj.save(data, callback);
}
});
}
update = inst.toObject(false);
this.applyProperties(update, inst);
update = removeUndefined(update);
Model = this.lookupModel(update);
this.getDataSource().connector.updateOrCreate(Model.modelName, update, function (err, data) {
var obj;
if (data && !(data instanceof Model)) {
inst._initProperties(data);
obj = inst;
} else {
obj = data;
}
callback(err, obj);
if(!err) {
Model.emit('changed', inst);
}
});
} else {
this.findById(getIdValue(this, data), function (err, inst) {
if (err) {
return callback(err);
}
if (inst) {
inst.updateAttributes(data, callback);
} else {
Model = self.lookupModel(data);
var obj = new Model(data);
obj.save(data, callback);
}
});
}
};
@ -332,11 +389,11 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) {
};
}
var t = this;
this.findOne(query, function (err, record) {
var Model = this;
Model.findOne(query, function (err, record) {
if (err) return callback(err);
if (record) return callback(null, record, false);
t.create(data, function (err, record) {
Model.create(data, function (err, record) {
callback(err, record, record != null);
});
});
@ -383,13 +440,13 @@ DataAccessObject.findByIds = function(ids, cond, cb) {
cb = cond;
cond = {};
}
var pk = idName(this);
if (ids.length === 0) {
process.nextTick(function() { cb(null, []); });
return;
}
var filter = { where: {} };
filter.where[pk] = { inq: [].concat(ids) };
mergeQuery(filter, cond || {});
@ -731,17 +788,26 @@ DataAccessObject._coerce = function (where) {
* @param {Function} callback Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances.
*/
DataAccessObject.find = function find(query, cb) {
DataAccessObject.find = function find(query, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (arguments.length === 1) {
cb = query;
query = null;
options = {};
}
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
if (!options) options = {};
var self = this;
query = query || {};
try {
this._normalize(query);
} catch (err) {
@ -751,7 +817,7 @@ DataAccessObject.find = function find(query, cb) {
}
this.applyScope(query);
var near = query && geo.nearFilter(query.where);
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
@ -763,42 +829,48 @@ DataAccessObject.find = function find(query, cb) {
// do in memory query
// using all documents
// TODO [fabien] use default scope here?
this.getDataSource().connector.all(this.modelName, {}, function (err, data) {
var memory = new Memory();
var modelName = self.modelName;
if (err) {
cb(err);
} else if (Array.isArray(data)) {
memory.define({
properties: self.dataSource.definitions[self.modelName].properties,
settings: self.dataSource.definitions[self.modelName].settings,
model: self
});
self.notifyObserversOf('query', { Model: self, query: query }, function(err, ctx) {
if (err) return cb(err);
data.forEach(function (obj) {
memory.create(modelName, obj, function () {
// noop
self.getDataSource().connector.all(self.modelName, {}, function (err, data) {
var memory = new Memory();
var modelName = self.modelName;
if (err) {
cb(err);
} else if (Array.isArray(data)) {
memory.define({
properties: self.dataSource.definitions[self.modelName].properties,
settings: self.dataSource.definitions[self.modelName].settings,
model: self
});
});
memory.all(modelName, query, cb);
} else {
cb(null, []);
}
}.bind(this));
data.forEach(function (obj) {
memory.create(modelName, obj, function () {
// noop
});
});
// FIXME: apply "includes" and other transforms - see allCb below
memory.all(modelName, ctx.query, cb);
} else {
cb(null, []);
}
});
});
// already handled
return;
}
}
this.getDataSource().connector.all(this.modelName, query, function (err, data) {
var allCb = function (err, data) {
if (data && data.forEach) {
data.forEach(function (d, i) {
var Model = self.lookupModel(d);
var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true});
if (query && query.include) {
if (query.collect) {
// The collect property indicates that the query is to return the
@ -815,7 +887,7 @@ DataAccessObject.find = function find(query, cb) {
if (utils.isPlainObject(inc)) {
relationName = Object.keys(inc)[0];
}
// Promote the included model as a direct property
var data = obj.__cachedRelations[relationName];
if(Array.isArray(data)) {
@ -840,7 +912,18 @@ DataAccessObject.find = function find(query, cb) {
}
else
cb(err, []);
});
}
var self = this;
if (options.notify === false) {
self.getDataSource().connector.all(self.modelName, query, allCb);
} else {
this.notifyObserversOf('query', { Model: this, query: query }, function(err, ctx) {
if (err) return cb(err);
var query = ctx.query;
self.getDataSource().connector.all(self.modelName, query, allCb);
});
}
};
/**
@ -850,16 +933,22 @@ DataAccessObject.find = function find(query, cb) {
* For example: `{where: {test: 'me'}}`.
* @param {Function} cb Callback function called with (err, instance)
*/
DataAccessObject.findOne = function findOne(query, cb) {
DataAccessObject.findOne = function findOne(query, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (typeof query === 'function') {
cb = query;
query = {};
}
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
query = query || {};
query.limit = 1;
this.find(query, function (err, collection) {
this.find(query, options, function (err, collection) {
if (err || !collection || !collection.length > 0) return cb(err, null);
cb(err, collection[0]);
});
@ -878,41 +967,79 @@ DataAccessObject.findOne = function findOne(query, cb) {
* @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object.
* @param {Function} [cb] Callback called with (err)
*/
DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, cb) {
DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this;
if (!cb && 'function' === typeof where) {
if (!cb && !options && 'function' === typeof where) {
cb = where;
where = undefined;
}
if (!cb && typeof options === 'function') {
cb = options;
}
if (!cb) cb = function(){};
if (!options) options = {};
var query = { where: where };
this.applyScope(query);
where = query.where;
if (!where || (typeof where === 'object' && Object.keys(where).length === 0)) {
this.getDataSource().connector.destroyAll(this.modelName, function (err, data) {
cb && cb(err, data);
if(!err) Model.emit('deletedAll');
}.bind(this));
var context = { Model: Model, where: whereIsEmpty(where) ? {} : where };
if (options.notify === false) {
doDelete(where);
} else {
try {
// Support an optional where object
where = removeUndefined(where);
where = this._coerce(where);
} catch (err) {
return process.nextTick(function() {
cb && cb(err);
query = { where: whereIsEmpty(where) ? {} : where };
Model.notifyObserversOf('query',
{ Model: Model, query: query },
function(err, ctx) {
if (err) return cb(err);
doDelete(ctx.query.where);
});
}
function doDelete(where) {
if (whereIsEmpty(where)) {
Model.getDataSource().connector.destroyAll(Model.modelName, done);
} else {
try {
// Support an optional where object
where = removeUndefined(where);
where = Model._coerce(where);
} catch (err) {
return process.nextTick(function() {
cb && cb(err);
});
}
Model.getDataSource().connector.destroyAll(Model.modelName, where, done);
}
function done(err, data) {
if (err) return cb(er);
if (options.notify === false) {
return cb(err, data);
}
Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) {
cb(err, data);
if (!err)
Model.emit('deletedAll', whereIsEmpty(where) ? undefined : where);
});
}
this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) {
cb && cb(err, data);
if(!err) Model.emit('deletedAll', where);
}.bind(this));
}
};
function whereIsEmpty(where) {
return !where ||
(typeof where === 'object' && Object.keys(where).length === 0)
}
/**
* Delete the record with the specified ID.
* Aliases are `destroyById` and `deleteById`.
@ -926,7 +1053,7 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this;
this.remove(byIdQuery(this, id).where, function(err) {
if ('function' === typeof cb) {
cb(err);
@ -955,11 +1082,11 @@ DataAccessObject.count = function (where, cb) {
cb = where;
where = null;
}
var query = { where: where };
this.applyScope(query);
where = query.where;
try {
where = removeUndefined(where);
where = this._coerce(where);
@ -968,8 +1095,13 @@ DataAccessObject.count = function (where, cb) {
cb && cb(err);
});
}
this.getDataSource().connector.count(this.modelName, cb, where);
var Model = this;
this.notifyObserversOf('query', { Model: Model, query: { where: where } }, function(err, ctx) {
if (err) return cb(err);
where = ctx.query.where;
Model.getDataSource().connector.count(Model.modelName, cb, where);
});
};
/**
@ -999,59 +1131,67 @@ DataAccessObject.prototype.save = function (options, callback) {
if (!('throws' in options)) {
options.throws = false;
}
var inst = this;
var data = inst.toObject(true);
var modelName = Model.modelName;
Model.applyProperties(data, this);
if (this.isNewRecord()) {
return Model.create(this, callback);
} else {
inst.setAttributes(data);
}
// validate first
if (!options.validate) {
return save();
}
Model.notifyObserversOf('before save', { Model: Model, instance: inst }, function(err) {
if (err) return callback(err);
data = inst.toObject(true);
inst.isValid(function (valid) {
if (valid) {
save();
} else {
var err = new ValidationError(inst);
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, inst);
// validate first
if (!options.validate) {
return save();
}
});
// then save
function save() {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (updateDone) {
data = removeUndefined(data);
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
if (err) {
return callback(err, inst);
}
inst._initProperties(data, { persisted: true });
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
if(!err) {
Model.emit('changed', inst);
}
inst.isValid(function (valid) {
if (valid) {
save();
} else {
var err = new ValidationError(inst);
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, inst);
}
});
// then save
function save() {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (updateDone) {
data = removeUndefined(data);
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
if (err) {
return callback(err, inst);
}
inst._initProperties(data, { persisted: true });
Model.notifyObserversOf('after save', { Model: Model, instance: inst }, function(err) {
if (err) return callback(err, inst);
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
if(!err) {
Model.emit('changed', inst);
}
});
});
});
});
});
}, data, callback);
}, data, callback);
}, data, callback);
}
}
});
};
/**
@ -1093,24 +1233,56 @@ DataAccessObject.updateAll = function (where, data, cb) {
assert(typeof where === 'object', 'The where argument should be an object');
assert(typeof data === 'object', 'The data argument should be an object');
assert(cb === null || typeof cb === 'function', 'The cb argument should be a function');
var query = { where: where };
this.applyScope(query);
this.applyProperties(data);
where = query.where;
try {
where = removeUndefined(where);
where = this._coerce(where);
} catch (err) {
return process.nextTick(function () {
cb && cb(err);
var Model = this;
Model.notifyObserversOf('query', { Model: Model, query: { where: where } }, function(err, ctx) {
if (err) return cb && cb(err);
Model.notifyObserversOf(
'before save',
{
Model: Model,
where: ctx.query.where,
data: data
},
function(err, ctx) {
if (err) return cb && cb(err);
doUpdate(ctx.where, ctx.data);
});
});
function doUpdate(where, data) {
try {
where = removeUndefined(where);
where = Model._coerce(where);
} catch (err) {
return process.nextTick(function () {
cb && cb(err);
});
}
var connector = Model.getDataSource().connector;
connector.update(Model.modelName, where, data, function(err, count) {
if (err) return cb && cb (err);
Model.notifyObserversOf(
'after save',
{
Model: Model,
where: where,
data: data
},
function(err, ctx) {
return cb && cb(err, count);
});
});
}
var connector = this.getDataSource().connector;
connector.update(this.modelName, where, data, cb);
};
DataAccessObject.prototype.isNewRecord = function () {
@ -1134,23 +1306,50 @@ DataAccessObject.prototype.remove =
DataAccessObject.prototype.delete =
DataAccessObject.prototype.destroy = function (cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var self = this;
var Model = this.constructor;
var id = getIdValue(this.constructor, this);
this.trigger('destroy', function (destroyed) {
this._adapter().destroy(this.constructor.modelName, id, function (err) {
if (err) {
return cb(err);
}
Model.notifyObserversOf(
'query',
{ Model: Model, query: byIdQuery(Model, id) },
function(err, ctx) {
if (err) return cb(err);
doDeleteInstance(ctx.query.where);
});
destroyed(function () {
if (cb) cb();
Model.emit('deleted', id);
function doDeleteInstance(where) {
if (!isWhereByGivenId(Model, where, id)) {
// A hook modified the query, it is no longer
// a simple 'delete model with the given id'.
// We must switch to full query-based delete.
Model.deleteAll(where, { notify: false }, function(err) {
if (err) return cb && cb(err);
Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) {
cb && cb(err);
if (!err) Model.emit('deleted', id);
});
});
}.bind(this));
}, null, cb);
return;
}
self.trigger('destroy', function (destroyed) {
self._adapter().destroy(self.constructor.modelName, id, function (err) {
if (err) {
return cb(err);
}
destroyed(function () {
Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) {
cb && cb(err);
if (!err) Model.emit('deleted', id);
});
});
});
}, null, cb);
}
};
/**
* Set a single attribute.
* Equivalent to `setAttributes({name: value})`
@ -1160,7 +1359,7 @@ DataAccessObject.prototype.remove =
*/
DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
this[name] = value; // TODO [fabien] - currently not protected by applyProperties
};
};
/**
* Update a single attribute.
@ -1184,17 +1383,17 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu
*/
DataAccessObject.prototype.setAttributes = function setAttributes(data) {
if (typeof data !== 'object') return;
this.constructor.applyProperties(data, this);
var Model = this.constructor;
var inst = this;
// update instance's properties
for (var key in data) {
inst.setAttribute(key, data[key]);
}
Model.emit('set', inst);
};
@ -1231,15 +1430,29 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
data = {};
}
// update instance's properties
inst.setAttributes(data);
if (!cb) {
cb = function() {};
}
inst.isValid(function (valid) {
if (!valid) {
if (cb) {
var context = {
Model: Model,
where: byIdQuery(Model, getIdValue(Model, inst)).where,
data: data
};
Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err);
data = ctx.data;
// update instance's properties
inst.setAttributes(data);
inst.isValid(function (valid) {
if (!valid) {
cb(new ValidationError(inst), inst);
return;
}
} else {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (done) {
var typedData = {};
@ -1248,7 +1461,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
// Convert the properties by type
inst[key] = data[key];
typedData[key] = inst[key];
if (typeof typedData[key] === 'object'
if (typeof typedData[key] === 'object'
&& typedData[key] !== null
&& typeof typedData[key].toObject === 'function') {
typedData[key] = typedData[key].toObject();
@ -1260,15 +1473,18 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
if (!err) inst.__persisted = true;
done.call(inst, function () {
saveDone.call(inst, function () {
if(cb) cb(err, inst);
if(!err) Model.emit('changed', inst);
if (err) return cb(err, inst);
Model.notifyObserversOf('after save', { Model: Model, instance: inst }, function(err) {
if(!err) Model.emit('changed', inst);
cb(err, inst);
});
});
});
});
}, data, cb);
}, data, cb);
}
}, data);
}, data);
});
};
/**

View File

@ -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).

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);

View File

@ -23,8 +23,8 @@
"node >= 0.6"
],
"devDependencies": {
"should": "~1.2.2",
"mocha": "~1.20.1"
"mocha": "~1.20.1",
"should": "^1.3.0"
},
"dependencies": {
"async": "~0.9.0",

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

View File

@ -445,7 +445,7 @@ function addHooks(name, done) {
};
User['after' + name] = function (next) {
(new Boolean(called)).should.equal(true);
this.email.should.equal(random);
this.should.have.property('email', random);
done();
};
}

View File

@ -5,6 +5,7 @@ var fs = require('fs');
var assert = require('assert');
var async = require('async');
var should = require('./init.js');
var Memory = require('../lib/connectors/memory').Memory;
describe('Memory connector', function () {
var file = path.join(__dirname, 'memory.json');
@ -278,27 +279,27 @@ describe('Memory connector', function () {
}
});
it('should use collection setting', function (done) {
var ds = new DataSource({
connector: 'memory'
});
var Product = ds.createModel('Product', {
name: String
});
var Tool = ds.createModel('Tool', {
name: String
}, {memory: {collection: 'Product'}});
var Widget = ds.createModel('Widget', {
name: String
}, {memory: {collection: 'Product'}});
ds.connector.getCollection('Tool').should.equal('Product');
ds.connector.getCollection('Widget').should.equal('Product');
async.series([
function(next) {
Tool.create({ name: 'Tool A' }, next);
@ -359,6 +360,17 @@ describe('Memory connector', function () {
});
});
require('./persistence-hooks.suite')(
new DataSource({ connector: Memory }),
should);
});
describe('Unoptimized connector', function() {
var ds = new DataSource({ connector: Memory });
// disable optimized methods
ds.connector.updateOrCreate = false;
require('./persistence-hooks.suite')(ds, should);
});

File diff suppressed because it is too large Load Diff