Merge pull request #403 from strongloop/feature/intent-hooks
Intent-based hooks for persistent models
This commit is contained in:
commit
ce39f8ab01
|
@ -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);
|
||||
|
|
598
lib/dao.js
598
lib/dao.js
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue