Intent-based hooks for persistence
This patch introduces a new API for "intent-based" hooks. These hooks are not tied to a particular method (e.g. "find" or "update"). Instead, they are triggered from all methods that execute a particular "intent". The consumer API is very simple, there is a new method Model.observe(name, observer), where the observer is function observer(context, callback). Observers are inherited by child models and it is possible to register multiple observers for the same hook. List of hooks: - query - before save - after save - after delete
This commit is contained in:
parent
b3d07ebbe8
commit
1fd6eff10f
|
@ -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);
|
this.setIdValue(model, data, id);
|
||||||
|
|
||||||
var cachedModels = this.collection(model);
|
var cachedModels = this.collection(model);
|
||||||
|
|
598
lib/dao.js
598
lib/dao.js
|
@ -7,6 +7,7 @@ module.exports = DataAccessObject;
|
||||||
/*!
|
/*!
|
||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
|
var async = require('async');
|
||||||
var jutil = require('./jutil');
|
var jutil = require('./jutil');
|
||||||
var ValidationError = require('./validations').ValidationError;
|
var ValidationError = require('./validations').ValidationError;
|
||||||
var Relation = require('./relations.js');
|
var Relation = require('./relations.js');
|
||||||
|
@ -62,6 +63,16 @@ function byIdQuery(m, id) {
|
||||||
return query;
|
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) {
|
DataAccessObject._forDB = function (data) {
|
||||||
if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
|
if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
|
||||||
return data;
|
return data;
|
||||||
|
@ -133,7 +144,7 @@ DataAccessObject.create = function (data, callback) {
|
||||||
|
|
||||||
var Model = this;
|
var Model = this;
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (typeof data === 'function') {
|
if (typeof data === 'function') {
|
||||||
callback = data;
|
callback = data;
|
||||||
data = {};
|
data = {};
|
||||||
|
@ -183,39 +194,42 @@ DataAccessObject.create = function (data, callback) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var enforced = {};
|
var enforced = {};
|
||||||
var obj;
|
var obj;
|
||||||
var idValue = getIdValue(this, data);
|
var idValue = getIdValue(this, data);
|
||||||
|
|
||||||
// if we come from save
|
// if we come from save
|
||||||
if (data instanceof Model && !idValue) {
|
if (data instanceof Model && !idValue) {
|
||||||
obj = data;
|
obj = data;
|
||||||
} else {
|
} else {
|
||||||
obj = new Model(data);
|
obj = new Model(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.applyProperties(enforced, obj);
|
this.applyProperties(enforced, obj);
|
||||||
obj.setAttributes(enforced);
|
obj.setAttributes(enforced);
|
||||||
|
|
||||||
Model = this.lookupModel(data); // data-specific
|
Model = this.lookupModel(data); // data-specific
|
||||||
if (Model !== obj.constructor) obj = new Model(data);
|
if (Model !== obj.constructor) obj = new Model(data);
|
||||||
|
|
||||||
data = obj.toObject(true);
|
Model.notifyObserversOf('before save', { Model: Model, instance: obj }, function(err) {
|
||||||
|
if (err) return callback(err);
|
||||||
// validation required
|
|
||||||
obj.isValid(function (valid) {
|
data = obj.toObject(true);
|
||||||
if (valid) {
|
|
||||||
create();
|
// validation required
|
||||||
} else {
|
obj.isValid(function (valid) {
|
||||||
callback(new ValidationError(obj), obj);
|
if (valid) {
|
||||||
}
|
create();
|
||||||
}, data);
|
} else {
|
||||||
|
callback(new ValidationError(obj), obj);
|
||||||
|
}
|
||||||
|
}, data);
|
||||||
|
});
|
||||||
|
|
||||||
function create() {
|
function create() {
|
||||||
obj.trigger('create', function (createDone) {
|
obj.trigger('create', function (createDone) {
|
||||||
obj.trigger('save', function (saveDone) {
|
obj.trigger('save', function (saveDone) {
|
||||||
|
|
||||||
var _idName = idName(Model);
|
var _idName = idName(Model);
|
||||||
var modelName = Model.modelName;
|
var modelName = Model.modelName;
|
||||||
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
|
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;
|
obj.__persisted = true;
|
||||||
saveDone.call(obj, function () {
|
saveDone.call(obj, function () {
|
||||||
createDone.call(obj, function () {
|
createDone.call(obj, function () {
|
||||||
callback(err, obj);
|
if (err) {
|
||||||
if(!err) Model.emit('changed', obj);
|
return callback(err, obj);
|
||||||
|
}
|
||||||
|
Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) {
|
||||||
|
callback(err, obj);
|
||||||
|
if(!err) Model.emit('changed', obj);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, obj);
|
}, obj);
|
||||||
|
@ -267,45 +286,83 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
||||||
}
|
}
|
||||||
var self = this;
|
var self = this;
|
||||||
var Model = this;
|
var Model = this;
|
||||||
if (!getIdValue(this, data)) {
|
var id = getIdValue(this, data);
|
||||||
|
if (!id) {
|
||||||
return this.create(data, callback);
|
return this.create(data, callback);
|
||||||
}
|
}
|
||||||
if (this.getDataSource().connector.updateOrCreate) {
|
|
||||||
var update = data;
|
Model.notifyObserversOf('query', { Model: Model, query: byIdQuery(Model, id) }, doUpdateOrCreate);
|
||||||
var inst = data;
|
|
||||||
if(!(data instanceof Model)) {
|
function doUpdateOrCreate(err, ctx) {
|
||||||
inst = new Model(data);
|
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;
|
var Model = this;
|
||||||
this.findOne(query, function (err, record) {
|
Model.findOne(query, function (err, record) {
|
||||||
if (err) return callback(err);
|
if (err) return callback(err);
|
||||||
if (record) return callback(null, record, false);
|
if (record) return callback(null, record, false);
|
||||||
t.create(data, function (err, record) {
|
Model.create(data, function (err, record) {
|
||||||
callback(err, record, record != null);
|
callback(err, record, record != null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -383,13 +440,13 @@ DataAccessObject.findByIds = function(ids, cond, cb) {
|
||||||
cb = cond;
|
cb = cond;
|
||||||
cond = {};
|
cond = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
var pk = idName(this);
|
var pk = idName(this);
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
process.nextTick(function() { cb(null, []); });
|
process.nextTick(function() { cb(null, []); });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filter = { where: {} };
|
var filter = { where: {} };
|
||||||
filter.where[pk] = { inq: [].concat(ids) };
|
filter.where[pk] = { inq: [].concat(ids) };
|
||||||
mergeQuery(filter, cond || {});
|
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.
|
* @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 (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
|
||||||
if (arguments.length === 1) {
|
if (arguments.length === 1) {
|
||||||
cb = query;
|
cb = query;
|
||||||
query = null;
|
query = null;
|
||||||
|
options = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cb === undefined && typeof options === 'function') {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options) options = {};
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
query = query || {};
|
query = query || {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._normalize(query);
|
this._normalize(query);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -751,7 +817,7 @@ DataAccessObject.find = function find(query, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.applyScope(query);
|
this.applyScope(query);
|
||||||
|
|
||||||
var near = query && geo.nearFilter(query.where);
|
var near = query && geo.nearFilter(query.where);
|
||||||
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
||||||
|
|
||||||
|
@ -763,42 +829,48 @@ DataAccessObject.find = function find(query, cb) {
|
||||||
// do in memory query
|
// do in memory query
|
||||||
// using all documents
|
// using all documents
|
||||||
// TODO [fabien] use default scope here?
|
// 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) {
|
self.notifyObserversOf('query', { Model: self, query: query }, function(err, ctx) {
|
||||||
cb(err);
|
if (err) return 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
|
|
||||||
});
|
|
||||||
|
|
||||||
data.forEach(function (obj) {
|
self.getDataSource().connector.all(self.modelName, {}, function (err, data) {
|
||||||
memory.create(modelName, obj, function () {
|
var memory = new Memory();
|
||||||
// noop
|
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);
|
data.forEach(function (obj) {
|
||||||
} else {
|
memory.create(modelName, obj, function () {
|
||||||
cb(null, []);
|
// noop
|
||||||
}
|
});
|
||||||
}.bind(this));
|
});
|
||||||
|
|
||||||
|
// FIXME: apply "includes" and other transforms - see allCb below
|
||||||
|
memory.all(modelName, ctx.query, cb);
|
||||||
|
} else {
|
||||||
|
cb(null, []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// already handled
|
// already handled
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getDataSource().connector.all(this.modelName, query, function (err, data) {
|
var allCb = function (err, data) {
|
||||||
if (data && data.forEach) {
|
if (data && data.forEach) {
|
||||||
data.forEach(function (d, i) {
|
data.forEach(function (d, i) {
|
||||||
var Model = self.lookupModel(d);
|
var Model = self.lookupModel(d);
|
||||||
var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true});
|
var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true});
|
||||||
|
|
||||||
if (query && query.include) {
|
if (query && query.include) {
|
||||||
if (query.collect) {
|
if (query.collect) {
|
||||||
// The collect property indicates that the query is to return the
|
// 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)) {
|
if (utils.isPlainObject(inc)) {
|
||||||
relationName = Object.keys(inc)[0];
|
relationName = Object.keys(inc)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promote the included model as a direct property
|
// Promote the included model as a direct property
|
||||||
var data = obj.__cachedRelations[relationName];
|
var data = obj.__cachedRelations[relationName];
|
||||||
if(Array.isArray(data)) {
|
if(Array.isArray(data)) {
|
||||||
|
@ -840,7 +912,18 @@ DataAccessObject.find = function find(query, cb) {
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
cb(err, []);
|
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'}}`.
|
* For example: `{where: {test: 'me'}}`.
|
||||||
* @param {Function} cb Callback function called with (err, instance)
|
* @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 (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
|
||||||
if (typeof query === 'function') {
|
if (typeof query === 'function') {
|
||||||
cb = query;
|
cb = query;
|
||||||
query = {};
|
query = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cb === undefined && typeof options === 'function') {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
query = query || {};
|
query = query || {};
|
||||||
query.limit = 1;
|
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);
|
if (err || !collection || !collection.length > 0) return cb(err, null);
|
||||||
cb(err, collection[0]);
|
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 {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)
|
* @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;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
|
||||||
var Model = this;
|
var Model = this;
|
||||||
|
|
||||||
if (!cb && 'function' === typeof where) {
|
if (!cb && !options && 'function' === typeof where) {
|
||||||
cb = where;
|
cb = where;
|
||||||
where = undefined;
|
where = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cb && typeof options === 'function') {
|
||||||
|
cb = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cb) cb = function(){};
|
||||||
|
if (!options) options = {};
|
||||||
|
|
||||||
var query = { where: where };
|
var query = { where: where };
|
||||||
this.applyScope(query);
|
this.applyScope(query);
|
||||||
where = query.where;
|
where = query.where;
|
||||||
|
|
||||||
if (!where || (typeof where === 'object' && Object.keys(where).length === 0)) {
|
var context = { Model: Model, where: whereIsEmpty(where) ? {} : where };
|
||||||
this.getDataSource().connector.destroyAll(this.modelName, function (err, data) {
|
if (options.notify === false) {
|
||||||
cb && cb(err, data);
|
doDelete(where);
|
||||||
if(!err) Model.emit('deletedAll');
|
|
||||||
}.bind(this));
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
query = { where: whereIsEmpty(where) ? {} : where };
|
||||||
// Support an optional where object
|
Model.notifyObserversOf('query',
|
||||||
where = removeUndefined(where);
|
{ Model: Model, query: query },
|
||||||
where = this._coerce(where);
|
function(err, ctx) {
|
||||||
} catch (err) {
|
if (err) return cb(err);
|
||||||
return process.nextTick(function() {
|
doDelete(ctx.query.where);
|
||||||
cb && cb(err);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
* Delete the record with the specified ID.
|
||||||
* Aliases are `destroyById` and `deleteById`.
|
* 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) {
|
DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
var Model = this;
|
var Model = this;
|
||||||
|
|
||||||
this.remove(byIdQuery(this, id).where, function(err) {
|
this.remove(byIdQuery(this, id).where, function(err) {
|
||||||
if ('function' === typeof cb) {
|
if ('function' === typeof cb) {
|
||||||
cb(err);
|
cb(err);
|
||||||
|
@ -955,11 +1082,11 @@ DataAccessObject.count = function (where, cb) {
|
||||||
cb = where;
|
cb = where;
|
||||||
where = null;
|
where = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = { where: where };
|
var query = { where: where };
|
||||||
this.applyScope(query);
|
this.applyScope(query);
|
||||||
where = query.where;
|
where = query.where;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
where = removeUndefined(where);
|
where = removeUndefined(where);
|
||||||
where = this._coerce(where);
|
where = this._coerce(where);
|
||||||
|
@ -968,8 +1095,13 @@ DataAccessObject.count = function (where, cb) {
|
||||||
cb && cb(err);
|
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)) {
|
if (!('throws' in options)) {
|
||||||
options.throws = false;
|
options.throws = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inst = this;
|
var inst = this;
|
||||||
var data = inst.toObject(true);
|
var data = inst.toObject(true);
|
||||||
var modelName = Model.modelName;
|
var modelName = Model.modelName;
|
||||||
|
|
||||||
Model.applyProperties(data, this);
|
Model.applyProperties(data, this);
|
||||||
|
|
||||||
if (this.isNewRecord()) {
|
if (this.isNewRecord()) {
|
||||||
return Model.create(this, callback);
|
return Model.create(this, callback);
|
||||||
} else {
|
} else {
|
||||||
inst.setAttributes(data);
|
inst.setAttributes(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate first
|
Model.notifyObserversOf('before save', { Model: Model, instance: inst }, function(err) {
|
||||||
if (!options.validate) {
|
if (err) return callback(err);
|
||||||
return save();
|
data = inst.toObject(true);
|
||||||
}
|
|
||||||
|
|
||||||
inst.isValid(function (valid) {
|
// validate first
|
||||||
if (valid) {
|
if (!options.validate) {
|
||||||
save();
|
return save();
|
||||||
} else {
|
|
||||||
var err = new ValidationError(inst);
|
|
||||||
// throws option is dangerous for async usage
|
|
||||||
if (options.throws) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
callback(err, inst);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// then save
|
inst.isValid(function (valid) {
|
||||||
function save() {
|
if (valid) {
|
||||||
inst.trigger('save', function (saveDone) {
|
save();
|
||||||
inst.trigger('update', function (updateDone) {
|
} else {
|
||||||
data = removeUndefined(data);
|
var err = new ValidationError(inst);
|
||||||
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
|
// throws option is dangerous for async usage
|
||||||
if (err) {
|
if (options.throws) {
|
||||||
return callback(err, inst);
|
throw err;
|
||||||
}
|
}
|
||||||
inst._initProperties(data, { persisted: true });
|
callback(err, inst);
|
||||||
updateDone.call(inst, function () {
|
}
|
||||||
saveDone.call(inst, function () {
|
});
|
||||||
callback(err, inst);
|
|
||||||
if(!err) {
|
// then save
|
||||||
Model.emit('changed', inst);
|
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);
|
||||||
}, 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 where === 'object', 'The where argument should be an object');
|
||||||
assert(typeof data === 'object', 'The data 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');
|
assert(cb === null || typeof cb === 'function', 'The cb argument should be a function');
|
||||||
|
|
||||||
var query = { where: where };
|
var query = { where: where };
|
||||||
this.applyScope(query);
|
this.applyScope(query);
|
||||||
this.applyProperties(data);
|
this.applyProperties(data);
|
||||||
|
|
||||||
where = query.where;
|
where = query.where;
|
||||||
|
|
||||||
try {
|
var Model = this;
|
||||||
where = removeUndefined(where);
|
|
||||||
where = this._coerce(where);
|
Model.notifyObserversOf('query', { Model: Model, query: { where: where } }, function(err, ctx) {
|
||||||
} catch (err) {
|
if (err) return cb && cb(err);
|
||||||
return process.nextTick(function () {
|
Model.notifyObserversOf(
|
||||||
cb && cb(err);
|
'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 () {
|
DataAccessObject.prototype.isNewRecord = function () {
|
||||||
|
@ -1134,23 +1306,50 @@ DataAccessObject.prototype.remove =
|
||||||
DataAccessObject.prototype.delete =
|
DataAccessObject.prototype.delete =
|
||||||
DataAccessObject.prototype.destroy = function (cb) {
|
DataAccessObject.prototype.destroy = function (cb) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
var self = this;
|
||||||
var Model = this.constructor;
|
var Model = this.constructor;
|
||||||
var id = getIdValue(this.constructor, this);
|
var id = getIdValue(this.constructor, this);
|
||||||
|
|
||||||
this.trigger('destroy', function (destroyed) {
|
Model.notifyObserversOf(
|
||||||
this._adapter().destroy(this.constructor.modelName, id, function (err) {
|
'query',
|
||||||
if (err) {
|
{ Model: Model, query: byIdQuery(Model, id) },
|
||||||
return cb(err);
|
function(err, ctx) {
|
||||||
}
|
if (err) return cb(err);
|
||||||
|
doDeleteInstance(ctx.query.where);
|
||||||
|
});
|
||||||
|
|
||||||
destroyed(function () {
|
function doDeleteInstance(where) {
|
||||||
if (cb) cb();
|
if (!isWhereByGivenId(Model, where, id)) {
|
||||||
Model.emit('deleted', 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));
|
return;
|
||||||
}, null, cb);
|
}
|
||||||
|
|
||||||
|
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.
|
* Set a single attribute.
|
||||||
* Equivalent to `setAttributes({name: value})`
|
* Equivalent to `setAttributes({name: value})`
|
||||||
|
@ -1160,7 +1359,7 @@ DataAccessObject.prototype.remove =
|
||||||
*/
|
*/
|
||||||
DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
|
DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
|
||||||
this[name] = value; // TODO [fabien] - currently not protected by applyProperties
|
this[name] = value; // TODO [fabien] - currently not protected by applyProperties
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a single attribute.
|
* Update a single attribute.
|
||||||
|
@ -1184,17 +1383,17 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu
|
||||||
*/
|
*/
|
||||||
DataAccessObject.prototype.setAttributes = function setAttributes(data) {
|
DataAccessObject.prototype.setAttributes = function setAttributes(data) {
|
||||||
if (typeof data !== 'object') return;
|
if (typeof data !== 'object') return;
|
||||||
|
|
||||||
this.constructor.applyProperties(data, this);
|
this.constructor.applyProperties(data, this);
|
||||||
|
|
||||||
var Model = this.constructor;
|
var Model = this.constructor;
|
||||||
var inst = this;
|
var inst = this;
|
||||||
|
|
||||||
// update instance's properties
|
// update instance's properties
|
||||||
for (var key in data) {
|
for (var key in data) {
|
||||||
inst.setAttribute(key, data[key]);
|
inst.setAttribute(key, data[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Model.emit('set', inst);
|
Model.emit('set', inst);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1231,15 +1430,29 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
||||||
data = {};
|
data = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// update instance's properties
|
if (!cb) {
|
||||||
inst.setAttributes(data);
|
cb = function() {};
|
||||||
|
}
|
||||||
|
|
||||||
inst.isValid(function (valid) {
|
var context = {
|
||||||
if (!valid) {
|
Model: Model,
|
||||||
if (cb) {
|
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);
|
cb(new ValidationError(inst), inst);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
inst.trigger('save', function (saveDone) {
|
inst.trigger('save', function (saveDone) {
|
||||||
inst.trigger('update', function (done) {
|
inst.trigger('update', function (done) {
|
||||||
var typedData = {};
|
var typedData = {};
|
||||||
|
@ -1248,7 +1461,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
||||||
// Convert the properties by type
|
// Convert the properties by type
|
||||||
inst[key] = data[key];
|
inst[key] = data[key];
|
||||||
typedData[key] = inst[key];
|
typedData[key] = inst[key];
|
||||||
if (typeof typedData[key] === 'object'
|
if (typeof typedData[key] === 'object'
|
||||||
&& typedData[key] !== null
|
&& typedData[key] !== null
|
||||||
&& typeof typedData[key].toObject === 'function') {
|
&& typeof typedData[key].toObject === 'function') {
|
||||||
typedData[key] = typedData[key].toObject();
|
typedData[key] = typedData[key].toObject();
|
||||||
|
@ -1260,15 +1473,18 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
||||||
if (!err) inst.__persisted = true;
|
if (!err) inst.__persisted = true;
|
||||||
done.call(inst, function () {
|
done.call(inst, function () {
|
||||||
saveDone.call(inst, function () {
|
saveDone.call(inst, function () {
|
||||||
if(cb) cb(err, inst);
|
if (err) return cb(err, inst);
|
||||||
if(!err) Model.emit('changed', 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, cb);
|
}, data, cb);
|
||||||
}
|
}, data);
|
||||||
}, data);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -445,7 +445,7 @@ function addHooks(name, done) {
|
||||||
};
|
};
|
||||||
User['after' + name] = function (next) {
|
User['after' + name] = function (next) {
|
||||||
(new Boolean(called)).should.equal(true);
|
(new Boolean(called)).should.equal(true);
|
||||||
this.email.should.equal(random);
|
this.should.have.property('email', random);
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ var fs = require('fs');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var should = require('./init.js');
|
var should = require('./init.js');
|
||||||
|
var Memory = require('../lib/connectors/memory').Memory;
|
||||||
|
|
||||||
describe('Memory connector', function () {
|
describe('Memory connector', function () {
|
||||||
var file = path.join(__dirname, 'memory.json');
|
var file = path.join(__dirname, 'memory.json');
|
||||||
|
@ -278,27 +279,27 @@ describe('Memory connector', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use collection setting', function (done) {
|
it('should use collection setting', function (done) {
|
||||||
var ds = new DataSource({
|
var ds = new DataSource({
|
||||||
connector: 'memory'
|
connector: 'memory'
|
||||||
});
|
});
|
||||||
|
|
||||||
var Product = ds.createModel('Product', {
|
var Product = ds.createModel('Product', {
|
||||||
name: String
|
name: String
|
||||||
});
|
});
|
||||||
|
|
||||||
var Tool = ds.createModel('Tool', {
|
var Tool = ds.createModel('Tool', {
|
||||||
name: String
|
name: String
|
||||||
}, {memory: {collection: 'Product'}});
|
}, {memory: {collection: 'Product'}});
|
||||||
|
|
||||||
var Widget = ds.createModel('Widget', {
|
var Widget = ds.createModel('Widget', {
|
||||||
name: String
|
name: String
|
||||||
}, {memory: {collection: 'Product'}});
|
}, {memory: {collection: 'Product'}});
|
||||||
|
|
||||||
ds.connector.getCollection('Tool').should.equal('Product');
|
ds.connector.getCollection('Tool').should.equal('Product');
|
||||||
ds.connector.getCollection('Widget').should.equal('Product');
|
ds.connector.getCollection('Widget').should.equal('Product');
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
function(next) {
|
function(next) {
|
||||||
Tool.create({ name: 'Tool A' }, 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