Add an optional `options` argument to all CRUD methods

This commit is contained in:
Raymond Feng 2015-02-10 23:57:05 -08:00
parent e9c966227d
commit 760ac97902
2 changed files with 816 additions and 160 deletions

View File

@ -23,6 +23,7 @@ var mergeQuery = utils.mergeQuery;
var util = require('util'); var util = require('util');
var assert = require('assert'); var assert = require('assert');
var BaseModel = require('./model'); var BaseModel = require('./model');
var debug = require('debug')('loopback:dao');
/** /**
* Base class for all persistent objects. * Base class for all persistent objects.
@ -123,6 +124,12 @@ DataAccessObject.lookupModel = function(data) {
return this; return this;
}; };
// Empty callback function
function noCallback(err, result) {
// NOOP
debug('callback is ignored: err=%j, result=%j', err, result);
}
/** /**
* Create an instance of Model with given data and save to the attached data source. Callback is optional. * Create an instance of Model with given data and save to the attached data source. Callback is optional.
* Example: * Example:
@ -134,30 +141,39 @@ DataAccessObject.lookupModel = function(data) {
* Note: You must include a callback and use the created model provided in the callback if your code depends on your model being * Note: You must include a callback and use the created model provided in the callback if your code depends on your model being
* saved or having an ID. * saved or having an ID.
* *
* @param {Object} data Optional data object * @param {Object} [data] Optional data object
* @param {Function} callback Callback function called with these arguments: * @param {Object} [options] Options for create
* @param {Function} [cb] Callback function called with these arguments:
* - err (null or Error) * - err (null or Error)
* - instance (null or Model) * - instance (null or Model)
*/ */
DataAccessObject.create = function (data, callback) { DataAccessObject.create = function (data, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this; var Model = this;
var self = this; var self = this;
if (typeof data === 'function') { if (options === undefined && cb === undefined) {
callback = data; if (typeof data === 'function') {
data = {}; // create(cb)
cb = data;
data = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// create(data, cb);
cb = options;
options = {};
}
} }
if (typeof callback !== 'function') { cb = cb || noCallback;
callback = function () { data = data || {};
}; options = options || {};
}
if (!data) { assert(typeof data === 'object', 'The data argument must be an object or array');
data = {}; assert(typeof options === 'object', 'The options argument must be an object');
} assert(typeof cb === 'function', 'The cb argument must be a function');
if (Array.isArray(data)) { if (Array.isArray(data)) {
// Undefined item will be skipped by async.map() which internally uses // Undefined item will be skipped by async.map() which internally uses
@ -169,13 +185,13 @@ DataAccessObject.create = function (data, callback) {
} }
} }
async.map(data, function(item, done) { async.map(data, function(item, done) {
self.create(item, function(err, result) { self.create(item, options, function(err, result) {
// Collect all errors and results // Collect all errors and results
done(null, {err: err, result: result || item}); done(null, {err: err, result: result || item});
}); });
}, function(err, results) { }, function(err, results) {
if (err) { if (err) {
return callback && callback(err, results); return cb(err, results);
} }
// Convert the results into two arrays // Convert the results into two arrays
var errors = null; var errors = null;
@ -189,7 +205,7 @@ DataAccessObject.create = function (data, callback) {
} }
data[i] = results[i].result; data[i] = results[i].result;
} }
callback && callback(errors, data); cb(errors, data);
}); });
return data; return data;
} }
@ -212,7 +228,7 @@ DataAccessObject.create = function (data, callback) {
if (Model !== obj.constructor) obj = new Model(data); if (Model !== obj.constructor) obj = new Model(data);
Model.notifyObserversOf('before save', { Model: Model, instance: obj }, function(err) { Model.notifyObserversOf('before save', { Model: Model, instance: obj }, function(err) {
if (err) return callback(err); if (err) return cb(err);
data = obj.toObject(true); data = obj.toObject(true);
@ -221,7 +237,7 @@ DataAccessObject.create = function (data, callback) {
if (valid) { if (valid) {
create(); create();
} else { } else {
callback(new ValidationError(obj), obj); cb(new ValidationError(obj), obj);
} }
}, data); }, data);
}); });
@ -241,23 +257,23 @@ DataAccessObject.create = function (data, callback) {
obj._rev = rev; obj._rev = rev;
} }
if (err) { if (err) {
return callback(err, obj); return cb(err, obj);
} }
obj.__persisted = true; obj.__persisted = true;
saveDone.call(obj, function () { saveDone.call(obj, function () {
createDone.call(obj, function () { createDone.call(obj, function () {
if (err) { if (err) {
return callback(err, obj); return cb(err, obj);
} }
Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) { Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) {
callback(err, obj); cb(err, obj);
if(!err) Model.emit('changed', obj); if(!err) Model.emit('changed', obj);
}); });
}); });
}); });
}, obj); }, obj);
}, obj, callback); }, obj, cb);
}, obj, callback); }, obj, cb);
} }
// for chaining // for chaining
@ -275,32 +291,57 @@ function stillConnecting(dataSource, obj, args) {
* NOTE: No setters, validations, or hooks are applied when using upsert. * NOTE: No setters, validations, or hooks are applied when using upsert.
* `updateOrCreate` is an alias * `updateOrCreate` is an alias
* @param {Object} data The model instance data * @param {Object} data The model instance data
* @param {Function} callback The callback function (optional). * @param {Object} [options] Options for upsert
* @param {Function} cb The callback function (optional).
*/ */
// [FIXME] rfeng: This is a hack to set up 'upsert' first so that // [FIXME] rfeng: This is a hack to set up 'upsert' first so that
// 'upsert' will be used as the name for strong-remoting to keep it backward // 'upsert' will be used as the name for strong-remoting to keep it backward
// compatible for angular SDK // compatible for angular SDK
DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data, callback) { DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) { if (stillConnecting(this.getDataSource(), this, arguments)) {
return; return;
} }
if (options === undefined && cb === undefined) {
if (typeof data === 'function') {
// upsert(cb)
cb = data;
data = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// upsert(data, cb)
cb = options;
options = {};
}
}
cb = cb || noCallback;
data = data || {};
options = options || {};
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var self = this; var self = this;
var Model = this; var Model = this;
var id = getIdValue(this, data); var id = getIdValue(this, data);
if (!id) { if (!id) {
return this.create(data, callback); return this.create(data, options, cb);
} }
Model.notifyObserversOf('access', { Model: Model, query: byIdQuery(Model, id) }, doUpdateOrCreate); Model.notifyObserversOf('access', { Model: Model, query: byIdQuery(Model, id) }, doUpdateOrCreate);
function doUpdateOrCreate(err, ctx) { function doUpdateOrCreate(err, ctx) {
if (err) return callback(err); if (err) return cb(err);
var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id) var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id)
if (Model.getDataSource().connector.updateOrCreate && isOriginalQuery) { if (Model.getDataSource().connector.updateOrCreate && isOriginalQuery) {
var context = { Model: Model, where: ctx.query.where, data: data }; var context = { Model: Model, where: ctx.query.where, data: data };
Model.notifyObserversOf('before save', context, function(err, ctx) { Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return callback(err); if (err) return cb(err);
data = ctx.data; data = ctx.data;
var update = data; var update = data;
@ -329,13 +370,13 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
obj = data; obj = data;
} }
if (err) { if (err) {
callback(err, obj); cb(err, obj);
if(!err) { if(!err) {
Model.emit('changed', inst); Model.emit('changed', inst);
} }
} else { } else {
Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) { Model.notifyObserversOf('after save', { Model: Model, instance: obj }, function(err) {
callback(err, obj); cb(err, obj);
if(!err) { if(!err) {
Model.emit('changed', inst); Model.emit('changed', inst);
} }
@ -346,7 +387,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
} else { } else {
Model.findOne({ where: ctx.query.where }, { notify: false }, function (err, inst) { Model.findOne({ where: ctx.query.where }, { notify: false }, function (err, inst) {
if (err) { if (err) {
return callback(err); return cb(err);
} }
if (!isOriginalQuery) { if (!isOriginalQuery) {
// The custom query returned from a hook may hide the fact that // The custom query returned from a hook may hide the fact that
@ -354,11 +395,11 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
delete data[idName(Model)]; delete data[idName(Model)];
} }
if (inst) { if (inst) {
inst.updateAttributes(data, callback); inst.updateAttributes(data, options, cb);
} else { } else {
Model = self.lookupModel(data); Model = self.lookupModel(data);
var obj = new Model(data); var obj = new Model(data);
obj.save(data, callback); obj.save(options, cb);
} }
}); });
} }
@ -373,27 +414,44 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
* @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format. * @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format.
* For example: `{where: {test: 'me'}}`. * For example: `{where: {test: 'me'}}`.
* @param {Object} data Object to create. * @param {Object} data Object to create.
* @param {Object} [options] Option for findOrCreate
* @param {Function} cb Callback called with (err, instance, created) * @param {Function} cb Callback called with (err, instance, created)
*/ */
DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) { DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) {
if (query === undefined) {
query = {where: {}}; assert(arguments.length >= 2, 'At least two arguments are required');
} if (options === undefined && cb === undefined) {
if (typeof data === 'function' || typeof data === 'undefined') { if (typeof data === 'function') {
callback = data; // findOrCreate(data, cb);
data = query && query.where; // query will be built from data
} cb = data;
if (typeof callback === 'undefined') { data = query;
callback = function () { query = {where: data};
}; }
} else if (cb === undefined) {
if (typeof options === 'function') {
// findOrCreate(query, data, cb)
cb = options;
options = {};
}
} }
cb = cb || noCallback;
query = query || {where: {}};
data = data || {};
options = options || {};
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var Model = this; var Model = this;
Model.findOne(query, function (err, record) { Model.findOne(query, function (err, record) {
if (err) return callback(err); if (err) return cb(err);
if (record) return callback(null, record, false); if (record) return cb(null, record, false);
Model.create(data, function (err, record) { Model.create(data, options, function (err, record) {
callback(err, record, record != null); cb(err, record, record != null);
}); });
}); });
}; };
@ -402,17 +460,35 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) {
* Check whether a model instance exists in database * Check whether a model instance exists in database
* *
* @param {id} id Identifier of object (primary key value) * @param {id} id Identifier of object (primary key value)
* @param {Object} [options] Options
* @param {Function} cb Callback function called with (err, exists: Bool) * @param {Function} cb Callback function called with (err, exists: Bool)
*/ */
DataAccessObject.exists = function exists(id, cb) { DataAccessObject.exists = function exists(id, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
assert(arguments.length >= 1, 'The id argument is required');
if (cb === undefined) {
if (typeof options === 'function') {
// exists(id, cb)
cb = options;
options = {};
}
}
cb = cb || noCallback;
options = options || {};
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (id !== undefined && id !== null && id !== '') { if (id !== undefined && id !== null && id !== '') {
this.count(byIdQuery(this, id).where, function(err, count) { this.count(byIdQuery(this, id).where, options, function(err, count) {
cb(err, err ? false : count === 1); cb(err, err ? false : count === 1);
}); });
} else { } else {
cb(new Error('Model::exists requires the id argument')); return process.nextTick(function() {
cb(new Error('Model::exists requires the id argument'));
});
} }
}; };
@ -427,19 +503,65 @@ DataAccessObject.exists = function exists(id, cb) {
* ``` * ```
* *
* @param {*} id Primary key value * @param {*} id Primary key value
* @param {Object} [options] Options
* @param {Function} cb Callback called with (err, instance) * @param {Function} cb Callback called with (err, instance)
*/ */
DataAccessObject.findById = function find(id, cb) { DataAccessObject.findById = function find(id, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
this.findOne(byIdQuery(this, id), cb);
assert(arguments.length >= 1, 'The id argument is required');
if (cb === undefined) {
if (typeof options === 'function') {
// findById(id, cb)
cb = options;
options = {};
}
}
cb = cb || noCallback;
options = options || {};
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (id == null || id === '') {
return process.nextTick(function() {
cb(new Error('Model::findById requires the id argument'));
});
}
this.findOne(byIdQuery(this, id), options, cb);
}; };
DataAccessObject.findByIds = function(ids, cond, cb) { /**
if (typeof cond === 'function') { * Find model instances by ids
cb = cond; * @param {Array} ids An array of ids
cond = {}; * @param {Object} query Query filter
* @param {Object} [options] Options
* @param {Function} cb Callback called with (err, instance)
*/
DataAccessObject.findByIds = function(ids, query, options, cb) {
if (options === undefined && cb === undefined) {
if (typeof query === 'function') {
// findByIds(ids, cb)
cb = query;
query = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// findByIds(ids, query, cb)
cb = options;
options = {};
}
} }
cb = cb || noCallback;
options = options || {};
query = query || {};
assert(Array.isArray(ids), 'The ids argument must be an array');
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
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, []); });
@ -448,10 +570,10 @@ DataAccessObject.findByIds = function(ids, cond, cb) {
var filter = { where: {} }; var filter = { where: {} };
filter.where[pk] = { inq: [].concat(ids) }; filter.where[pk] = { inq: [].concat(ids) };
mergeQuery(filter, cond || {}); mergeQuery(filter, query || {});
this.find(filter, function(err, results) { this.find(filter, options, function(err, results) {
cb(err, err ? results : utils.sortObjectsByIds(pk, ids, results)); cb(err, err ? results : utils.sortObjectsByIds(pk, ids, results));
}.bind(this)); });
}; };
function convertNullToNotFoundError(ctx, cb) { function convertNullToNotFoundError(ctx, cb) {
@ -784,34 +906,41 @@ DataAccessObject._coerce = function (where) {
* - `{foo: true}` - include only foo * - `{foo: true}` - include only foo
* - `{bat: false}` - include all properties, exclude bat * - `{bat: false}` - include all properties, exclude bat
* *
* @param {Function} callback Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances. * @param {Function} cb Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances.
*/ */
DataAccessObject.find = function find(query, options, 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 (options === undefined && cb === undefined) {
cb = query; if (typeof query === 'function') {
query = null; // find(cb);
options = {}; cb = query;
query = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// find(query, cb);
cb = options;
options = {};
}
} }
if (cb === undefined && typeof options === 'function') { cb = cb || noCallback;
cb = options; query = query || {};
options = {}; options = options || {};
}
if (!options) options = {}; assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var self = this; var self = this;
query = query || {};
try { try {
this._normalize(query); this._normalize(query);
} catch (err) { } catch (err) {
return process.nextTick(function () { return process.nextTick(function () {
cb && cb(err); cb(err);
}); });
} }
@ -913,7 +1042,6 @@ DataAccessObject.find = function find(query, options, cb) {
cb(err, []); cb(err, []);
} }
var self = this;
if (options.notify === false) { if (options.notify === false) {
self.getDataSource().connector.all(self.modelName, query, allCb); self.getDataSource().connector.all(self.modelName, query, allCb);
} else { } else {
@ -928,24 +1056,34 @@ DataAccessObject.find = function find(query, options, cb) {
/** /**
* Find one record, same as `find`, but limited to one result. This function returns an object, not a collection. * Find one record, same as `find`, but limited to one result. This function returns an object, not a collection.
* *
* @param {Object} query Sarch conditions. See [find](#dataaccessobjectfindquery-callback) for query format. * @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format.
* For example: `{where: {test: 'me'}}`. * For example: `{where: {test: 'me'}}`.
* @param {Object} [options] Options
* @param {Function} cb Callback function called with (err, instance) * @param {Function} cb Callback function called with (err, instance)
*/ */
DataAccessObject.findOne = function findOne(query, options, 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 (options === undefined && cb === undefined) {
cb = query; if (typeof query === 'function') {
query = {}; cb = query;
} query = {};
}
if (cb === undefined && typeof options === 'function') { } else if (cb === undefined) {
cb = options; if (typeof options === 'function') {
options = {}; cb = options;
options = {};
}
} }
cb = cb || noCallback;
query = query || {}; query = query || {};
options = options || {};
assert(typeof query === 'object', 'The query argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
query.limit = 1; query.limit = 1;
this.find(query, options, 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);
@ -964,6 +1102,7 @@ DataAccessObject.findOne = function findOne(query, options, 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 {Object) [options] Options
* @param {Function} [cb] Callback called with (err) * @param {Function} [cb] Callback called with (err)
*/ */
DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, options, cb) { DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, options, cb) {
@ -971,17 +1110,25 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
var Model = this; var Model = this;
if (!cb && !options && 'function' === typeof where) { if (options === undefined && cb === undefined) {
cb = where; if (typeof where === 'function') {
where = undefined; cb = where;
where = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
cb = options;
options = {};
}
} }
if (!cb && typeof options === 'function') { cb = cb || noCallback;
cb = options; where = where || {};
} options = options || {};
if (!cb) cb = function(){}; assert(typeof where === 'object', 'The where argument must be an object');
if (!options) options = {}; assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var query = { where: where }; var query = { where: where };
this.applyScope(query); this.applyScope(query);
@ -1014,7 +1161,7 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
where = Model._coerce(where); where = Model._coerce(where);
} catch (err) { } catch (err) {
return process.nextTick(function() { return process.nextTick(function() {
cb && cb(err); cb(err);
}); });
} }
@ -1053,11 +1200,29 @@ function whereIsEmpty(where) {
// [FIXME] rfeng: This is a hack to set up 'deleteById' first so that // [FIXME] rfeng: This is a hack to set up 'deleteById' first so that
// 'deleteById' will be used as the name for strong-remoting to keep it backward // 'deleteById' will be used as the name for strong-remoting to keep it backward
// compatible for angular SDK // compatible for angular SDK
DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) { DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
assert(arguments.length >= 1, 'The id argument is required');
if (cb === undefined) {
if (typeof options === 'function') {
// destroyById(id, cb)
cb = options;
options = {};
}
}
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
if (id == null || id === '') {
return process.nextTick(function() {
cb(new Error('Model::deleteById requires the id argument'));
});
}
var Model = this; var Model = this;
this.remove(byIdQuery(this, id).where, function(err) { this.remove(byIdQuery(this, id).where, options, function(err) {
if ('function' === typeof cb) { if ('function' === typeof cb) {
cb(err); cb(err);
} }
@ -1076,16 +1241,34 @@ DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.de
* ``` * ```
* *
* @param {Object} [where] Search conditions (optional) * @param {Object} [where] Search conditions (optional)
* @param {Object} [options] Options
* @param {Function} cb Callback, called with (err, count) * @param {Function} cb Callback, called with (err, count)
*/ */
DataAccessObject.count = function (where, cb) { DataAccessObject.count = function (where, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (typeof where === 'function') { if (options === undefined && cb === undefined) {
cb = where; if (typeof where === 'function') {
where = null; // count(cb)
cb = where;
where = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// count(where, cb)
cb = options;
options = {};
}
} }
cb = cb || noCallback;
where = where || {};
options = options || {};
assert(typeof where === 'object', 'The where argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var query = { where: where }; var query = { where: where };
this.applyScope(query); this.applyScope(query);
where = query.where; where = query.where;
@ -1095,7 +1278,7 @@ DataAccessObject.count = function (where, cb) {
where = this._coerce(where); where = this._coerce(where);
} catch (err) { } catch (err) {
return process.nextTick(function () { return process.nextTick(function () {
cb && cb(err); cb(err);
}); });
} }
@ -1113,37 +1296,39 @@ DataAccessObject.count = function (where, cb) {
* @options {Object} options Optional options to use. * @options {Object} options Optional options to use.
* @property {Boolean} validate Default is true. * @property {Boolean} validate Default is true.
* @property {Boolean} throws Default is false. * @property {Boolean} throws Default is false.
* @param {Function} callback Callback function with err and object arguments * @param {Function} cb Callback function with err and object arguments
*/ */
DataAccessObject.prototype.save = function (options, callback) { DataAccessObject.prototype.save = function (options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this.constructor; var Model = this.constructor;
if (typeof options == 'function') { if (typeof options === 'function') {
callback = options; cb = options;
options = {}; options = {};
} }
callback = callback || function () { cb = cb || noCallback;
};
options = options || {}; options = options || {};
if (!('validate' in options)) { assert(typeof options === 'object', 'The options argument should be an object');
assert(typeof cb === 'function', 'The cb argument should be a function');
if (options.validate === undefined) {
options.validate = true; options.validate = true;
} }
if (!('throws' in options)) { if (options.throws === undefined) {
options.throws = false; options.throws = false;
} }
if (this.isNewRecord()) { if (this.isNewRecord()) {
return Model.create(this, callback); return Model.create(this, cb);
} }
var inst = this; var inst = this;
var modelName = Model.modelName; var modelName = Model.modelName;
Model.notifyObserversOf('before save', { Model: Model, instance: inst }, function(err) { Model.notifyObserversOf('before save', { Model: Model, instance: inst }, function(err) {
if (err) return callback(err); if (err) return cb(err);
var data = inst.toObject(true); var data = inst.toObject(true);
Model.applyProperties(data, inst); Model.applyProperties(data, inst);
@ -1163,7 +1348,7 @@ DataAccessObject.prototype.save = function (options, callback) {
if (options.throws) { if (options.throws) {
throw err; throw err;
} }
callback(err, inst); cb(err, inst);
} }
}); });
@ -1174,14 +1359,14 @@ DataAccessObject.prototype.save = function (options, callback) {
data = removeUndefined(data); data = removeUndefined(data);
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) { inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
if (err) { if (err) {
return callback(err, inst); return cb(err, inst);
} }
inst._initProperties(data, { persisted: true }); inst._initProperties(data, { persisted: true });
Model.notifyObserversOf('after save', { Model: Model, instance: inst }, function(err) { Model.notifyObserversOf('after save', { Model: Model, instance: inst }, function(err) {
if (err) return callback(err, inst); if (err) return cb(err, inst);
updateDone.call(inst, function () { updateDone.call(inst, function () {
saveDone.call(inst, function () { saveDone.call(inst, function () {
callback(err, inst); cb(err, inst);
if(!err) { if(!err) {
Model.emit('changed', inst); Model.emit('changed', inst);
} }
@ -1189,8 +1374,8 @@ DataAccessObject.prototype.save = function (options, callback) {
}); });
}); });
}); });
}, data, callback); }, data, cb);
}, data, callback); }, data, cb);
} }
}); });
}; };
@ -1208,32 +1393,37 @@ DataAccessObject.prototype.save = function (options, callback) {
* *
* @param {Object} [where] Search conditions (optional) * @param {Object} [where] Search conditions (optional)
* @param {Object} data Changes to be made * @param {Object} data Changes to be made
* @param {Object} [options] Options for update
* @param {Function} cb Callback, called with (err, count) * @param {Function} cb Callback, called with (err, count)
*/ */
DataAccessObject.update = DataAccessObject.update =
DataAccessObject.updateAll = function (where, data, cb) { DataAccessObject.updateAll = function (where, data, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (arguments.length === 1) { assert(arguments.length >= 2, 'At least two arguments are required');
// update(data) is being called if (options === undefined && cb === undefined) {
data = where;
where = null;
cb = null;
} else if (arguments.length === 2) {
if (typeof data === 'function') { if (typeof data === 'function') {
// update(data, cb) is being called // updateAll(data, cb);
cb = data; cb = data;
data = where; data = where;
where = null; where = {};
} else { }
// update(where, data) is being called } else if (cb === undefined) {
cb = null; if (typeof options === 'function') {
// updateAll(query, data, cb)
cb = options;
options = {};
} }
} }
assert(typeof where === 'object', 'The where argument should be an object'); cb = cb || noCallback;
assert(typeof data === 'object', 'The data argument should be an object'); data = data || {};
assert(cb === null || typeof cb === 'function', 'The cb argument should be a function'); options = options || {};
assert(typeof where === 'object', 'The where argument must be an object');
assert(typeof data === 'object', 'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var query = { where: where }; var query = { where: where };
this.applyScope(query); this.applyScope(query);
@ -1244,7 +1434,7 @@ DataAccessObject.updateAll = function (where, data, cb) {
var Model = this; var Model = this;
Model.notifyObserversOf('access', { Model: Model, query: { where: where } }, function(err, ctx) { Model.notifyObserversOf('access', { Model: Model, query: { where: where } }, function(err, ctx) {
if (err) return cb && cb(err); if (err) return cb(err);
Model.notifyObserversOf( Model.notifyObserversOf(
'before save', 'before save',
{ {
@ -1253,7 +1443,7 @@ DataAccessObject.updateAll = function (where, data, cb) {
data: data data: data
}, },
function(err, ctx) { function(err, ctx) {
if (err) return cb && cb(err); if (err) return cb(err);
doUpdate(ctx.where, ctx.data); doUpdate(ctx.where, ctx.data);
}); });
}); });
@ -1265,13 +1455,13 @@ DataAccessObject.updateAll = function (where, data, cb) {
where = Model._coerce(where); where = Model._coerce(where);
} catch (err) { } catch (err) {
return process.nextTick(function () { return process.nextTick(function () {
cb && cb(err); cb(err);
}); });
} }
var connector = Model.getDataSource().connector; var connector = Model.getDataSource().connector;
connector.update(Model.modelName, where, data, function(err, count) { connector.update(Model.modelName, where, data, function(err, count) {
if (err) return cb && cb (err); if (err) return cb (err);
Model.notifyObserversOf( Model.notifyObserversOf(
'after save', 'after save',
{ {
@ -1280,7 +1470,7 @@ DataAccessObject.updateAll = function (where, data, cb) {
data: data data: data
}, },
function(err, ctx) { function(err, ctx) {
return cb && cb(err, count); return cb(err, count);
}); });
}); });
} }
@ -1302,11 +1492,26 @@ DataAccessObject.prototype._adapter = function () {
* Delete object from persistence * Delete object from persistence
* *
* Triggers `destroy` hook (async) before and after destroying object * Triggers `destroy` hook (async) before and after destroying object
*
* @param {Object} [options] Options for delete
* @param {Function} cb Callback
*/ */
DataAccessObject.prototype.remove = DataAccessObject.prototype.remove =
DataAccessObject.prototype.delete = DataAccessObject.prototype.delete =
DataAccessObject.prototype.destroy = function (cb) { DataAccessObject.prototype.destroy = function (options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
cb = cb || noCallback;
options = options || {};
assert(typeof options === 'object', 'The options argument should be an object');
assert(typeof cb === 'function', 'The cb argument should be a function');
var self = this; var self = this;
var Model = this.constructor; var Model = this.constructor;
var id = getIdValue(this.constructor, this); var id = getIdValue(this.constructor, this);
@ -1331,9 +1536,9 @@ DataAccessObject.prototype.remove =
// a simple 'delete model with the given id'. // a simple 'delete model with the given id'.
// We must switch to full query-based delete. // We must switch to full query-based delete.
Model.deleteAll(where, { notify: false }, function(err) { Model.deleteAll(where, { notify: false }, function(err) {
if (err) return cb && cb(err); if (err) return cb(err);
Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) { Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) {
cb && cb(err); cb(err);
if (!err) Model.emit('deleted', id); if (!err) Model.emit('deleted', id);
}); });
}); });
@ -1348,7 +1553,7 @@ DataAccessObject.prototype.remove =
destroyed(function () { destroyed(function () {
Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) { Model.notifyObserversOf('after delete', { Model: Model, where: where }, function(err) {
cb && cb(err); cb(err);
if (!err) Model.emit('deleted', id); if (!err) Model.emit('deleted', id);
}); });
}); });
@ -1374,12 +1579,12 @@ DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
* *
* @param {String} name Name of property * @param {String} name Name of property
* @param {Mixed} value Value of property * @param {Mixed} value Value of property
* @param {Function} callback Callback function called with (err, instance) * @param {Function} cb Callback function called with (err, instance)
*/ */
DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, callback) { DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, cb) {
var data = {}; var data = {};
data[name] = value; data[name] = value;
this.updateAttributes(data, callback); this.updateAttributes(data, cb);
}; };
/** /**
@ -1419,28 +1624,38 @@ DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullif
* *
* @trigger `validation`, `save` and `update` hooks * @trigger `validation`, `save` and `update` hooks
* @param {Object} data Data to update * @param {Object} data Data to update
* @param {Function} callback Callback function called with (err, instance) * @param {Object} [options] Options for updateAttributes
* @param {Function} cb Callback function called with (err, instance)
*/ */
DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb) { DataAccessObject.prototype.updateAttributes = function updateAttributes(data, options, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (options === undefined && cb === undefined) {
if (typeof data === 'function') {
// updateAttributes(cb)
cb = data;
data = undefined;
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// updateAttributes(data, cb)
cb = options;
options = {};
}
}
cb = cb || noCallback;
options = options || {};
assert((typeof data === 'object') && (data !== null),
'The data argument must be an object');
assert(typeof options === 'object', 'The options argument must be an object');
assert(typeof cb === 'function', 'The cb argument must be a function');
var inst = this; var inst = this;
var Model = this.constructor; var Model = this.constructor;
var model = Model.modelName; var model = Model.modelName;
if (typeof data === 'function') {
cb = data;
data = null;
}
if (!data) {
data = {};
}
if (!cb) {
cb = function() {};
}
// Convert the data to be plain object so that update won't be confused // Convert the data to be plain object so that update won't be confused
if (data instanceof Model) { if (data instanceof Model) {
data = data.toObject(false); data = data.toObject(false);
@ -1503,15 +1718,15 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
/** /**
* Reload object from persistence * Reload object from persistence
* Requires `id` member of `object` to be able to call `find` * Requires `id` member of `object` to be able to call `find`
* @param {Function} callback Called with (err, instance) arguments * @param {Function} cb Called with (err, instance) arguments
* @private * @private
*/ */
DataAccessObject.prototype.reload = function reload(callback) { DataAccessObject.prototype.reload = function reload(cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) { if (stillConnecting(this.getDataSource(), this, arguments)) {
return; return;
} }
this.constructor.findById(getIdValue(this.constructor, this), callback); this.constructor.findById(getIdValue(this.constructor, this), cb);
}; };

View File

@ -0,0 +1,441 @@
// This test written in mocha+should.js
var should = require('./init.js');
var async = require('async');
var db, User, options;
describe('crud-with-options', function () {
before(function (done) {
db = getSchema();
User = db.define('User', {
seq: {type: Number, index: true},
name: {type: String, index: true, sort: true},
email: {type: String, index: true},
birthday: {type: Date, index: true},
role: {type: String, index: true},
order: {type: Number, index: true, sort: true},
vip: {type: Boolean}
});
options = {};
db.automigrate(done);
});
describe('findById', function () {
before(function (done) {
User.destroyAll(done);
});
it('should allow findById(id, options, cb)', function (done) {
User.findById(1, options, function (err, u) {
should.not.exist(u);
should.not.exist(err);
done();
});
});
it('should allow findById(id)', function () {
User.findById(1);
});
it('should allow findById(id, options)', function () {
User.findById(1, options);
});
it('should throw when invalid options are provided for findById',
function(done) {
(function() {
User.findById(1, '123', function(err, u) {
});
}).should.throw('The options argument must be an object');
done();
});
it('should report an invalid id via callback for findById',
function(done) {
User.findById(undefined, {}, function(err, u) {
err.should.be.eql(
new Error('Model::findById requires the id argument'));
done();
});
});
it('should allow findById(id, options, cb) for a matching id',
function(done) {
User.create(function(err, u) {
should.not.exist(err);
should.exist(u.id);
User.findById(u.id, options, function(err, u) {
should.exist(u);
should.not.exist(err);
u.should.be.an.instanceOf(User);
done();
});
});
});
});
describe('findByIds', function () {
before(function(done) {
var people = [
{ id: 1, name: 'a', vip: true },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd', vip: true },
{ id: 5, name: 'e' },
{ id: 6, name: 'f' }
];
// Use automigrate so that serial keys are 1-6
db.automigrate(['User'], function(err) {
User.create(people, options, function(err, users) {
done();
});
});
});
it('should allow findByIds(ids, cb)', function (done) {
User.findByIds([3, 2, 1], function (err, users) {
should.exist(users);
should.not.exist(err);
var names = users.map(function(u) { return u.name; });
names.should.eql(['c', 'b', 'a']);
done();
});
});
it('should allow findByIds(ids, filter, options, cb)',
function(done) {
User.findByIds([4, 3, 2, 1],
{ where: { vip: true } }, options, function(err, users) {
should.exist(users);
should.not.exist(err);
var names = users.map(function(u) {
return u.name;
});
names.should.eql(['d', 'a']);
done();
});
});
});
describe('find', function () {
before(seed);
it('should allow find(cb)', function(done) {
User.find(function(err, users) {
should.exists(users);
should.not.exists(err);
users.should.have.lengthOf(6);
done();
});
});
it('should allow find(filter, cb)', function(done) {
User.find({limit: 3}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.should.have.lengthOf(3);
done();
});
});
it('should allow find(filter, options, cb)', function(done) {
User.find({}, options, function(err, users) {
should.exists(users);
should.not.exists(err);
users.should.have.lengthOf(6);
done();
});
});
it('should allow find(filter, options)', function() {
User.find({limit: 3}, options);
});
it('should allow find(filter)', function() {
User.find({limit: 3});
});
it('should skip trailing undefined args', function(done) {
User.find({limit: 3}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.should.have.lengthOf(3);
done();
}, undefined, undefined);
});
it('should throw on an invalid query arg', function() {
(function() {
User.find('invalid query', function(err, users) {
// noop
});
}).should.throw('The query argument must be an object');
});
it('should throw on an invalid options arg', function() {
(function() {
User.find({limit: 3}, 'invalid option', function(err, users) {
// noop
});
}).should.throw('The options argument must be an object');
});
it('should throw on an invalid cb arg', function() {
(function() {
User.find({limit: 3}, {}, 'invalid cb');
}).should.throw('The cb argument must be a function');
});
});
describe('count', function () {
before(seed);
it('should allow count(cb)', function (done) {
User.count(function (err, n) {
should.not.exist(err);
should.exist(n);
n.should.equal(6);
done();
});
});
it('should allow count(where, cb)', function (done) {
User.count({role: 'lead'}, function (err, n) {
should.not.exist(err);
should.exist(n);
n.should.equal(2);
done();
});
});
it('should allow count(where, options, cb)', function (done) {
User.count({role: 'lead'}, options, function (err, n) {
should.not.exist(err);
should.exist(n);
n.should.equal(2);
done();
});
});
});
describe('findOne', function () {
before(seed);
it('should allow findOne(cb)', function (done) {
User.find({order: 'id'}, function (err, users) {
User.findOne(function (e, u) {
should.not.exist(e);
should.exist(u);
u.id.toString().should.equal(users[0].id.toString());
done();
});
});
});
it('should allow findOne(filter, options, cb)', function (done) {
User.findOne({order: 'order'}, options, function (e, u) {
should.not.exist(e);
should.exist(u);
u.order.should.equal(1);
u.name.should.equal('Paul McCartney');
done();
});
});
it('should allow findOne(filter, cb)', function (done) {
User.findOne({order: 'order'}, function (e, u) {
should.not.exist(e);
should.exist(u);
u.order.should.equal(1);
u.name.should.equal('Paul McCartney');
done();
});
});
it('should allow trailing undefined args', function (done) {
User.findOne({order: 'order'}, function (e, u) {
should.not.exist(e);
should.exist(u);
u.order.should.equal(1);
u.name.should.equal('Paul McCartney');
done();
}, undefined);
});
});
describe('exists', function () {
before(seed);
it('should allow exists(id, cb)', function (done) {
User.findOne(function (e, u) {
User.exists(u.id, function (err, exists) {
should.not.exist(err);
should.exist(exists);
exists.should.be.ok;
done();
});
});
});
it('should allow exists(id, options, cb)', function (done) {
User.destroyAll(function () {
User.exists(42, options, function (err, exists) {
should.not.exist(err);
exists.should.not.be.ok;
done();
});
});
});
});
describe('destroyAll with options', function () {
beforeEach(seed);
it('should allow destroyAll(where, options, cb)', function (done) {
User.destroyAll({name: 'John Lennon'}, options, function (err) {
should.not.exist(err);
User.find({where: {name: 'John Lennon'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(0);
User.find({where: {name: 'Paul McCartney'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(1);
done();
});
});
});
});
it('should allow destroyAll(where, cb)', function (done) {
User.destroyAll({name: 'John Lennon'}, function (err) {
should.not.exist(err);
User.find({where: {name: 'John Lennon'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(0);
User.find({where: {name: 'Paul McCartney'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(1);
done();
});
});
});
});
it('should allow destroyAll(cb)', function (done) {
User.destroyAll(function (err) {
should.not.exist(err);
User.find({where: {name: 'John Lennon'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(0);
User.find({where: {name: 'Paul McCartney'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(0);
done();
});
});
});
});
});
describe('updateAll ', function () {
beforeEach(seed);
it('should allow updateAll(where, data, cb)', function (done) {
User.update({name: 'John Lennon'}, {name: 'John Smith'}, function (err) {
should.not.exist(err);
User.find({where: {name: 'John Lennon'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(0);
User.find({where: {name: 'John Smith'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(1);
done();
});
});
});
});
it('should allow updateAll(where, data, options, cb)', function(done) {
User.update({name: 'John Lennon'}, {name: 'John Smith'}, options,
function(err) {
should.not.exist(err);
User.find({where: {name: 'John Lennon'}}, function(err, data) {
should.not.exist(err);
data.length.should.equal(0);
User.find({where: {name: 'John Smith'}}, function(err, data) {
should.not.exist(err);
data.length.should.equal(1);
done();
});
});
});
});
it('should allow updateAll(data, cb)', function (done) {
User.update({name: 'John Smith'}, function () {
User.find({where: {name: 'John Lennon'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(0);
User.find({where: {name: 'John Smith'}}, function (err, data) {
should.not.exist(err);
data.length.should.equal(6);
done();
});
});
});
});
});
});
function seed(done) {
var beatles = [
{
seq: 0,
name: 'John Lennon',
email: 'john@b3atl3s.co.uk',
role: 'lead',
birthday: new Date('1980-12-08'),
order: 2,
vip: true
},
{
seq: 1,
name: 'Paul McCartney',
email: 'paul@b3atl3s.co.uk',
role: 'lead',
birthday: new Date('1942-06-18'),
order: 1,
vip: true
},
{seq: 2, name: 'George Harrison', order: 5, vip: false},
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
{seq: 4, name: 'Pete Best', order: 4},
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true}
];
async.series([
User.destroyAll.bind(User),
function(cb) {
async.each(beatles, User.create.bind(User), cb);
}
], done);
}