Merge remote-tracking branch 'upstream/master'

This commit is contained in:
mamboer 2015-05-26 12:28:11 +08:00
commit 69bd7c1233
20 changed files with 1305 additions and 420 deletions

View File

@ -1,3 +1,39 @@
2015-05-20, Version 2.28.1
==========================
* Remove dep on sinon (Raymond Feng)
* Update deps (Raymond Feng)
2015-05-18, Version 2.28.0
==========================
* Make sure promise is returned (Raymond Feng)
* Update deps to loopback-connector (Raymond Feng)
* Fix comments (Raymond Feng)
* Enable docs (Raymond Feng)
* Add an optional `options` argument to relation methods (Raymond Feng)
* Add transaction apis (Raymond Feng)
* Refactor the observer functions into a plugin (Raymond Feng)
* Add transaction (Raymond Feng)
2015-05-16, Version 2.27.1
==========================
* Make sure relation scope is applied during include (Raymond Feng)
* Updated JSdoc for Datasource constructor (crandmck)
2015-05-13, Version 2.27.0
==========================

View File

@ -7,6 +7,8 @@
"lib/include.js",
"lib/model-builder.js",
"lib/relations.js",
"lib/observer.js",
"lib/transaction.js",
"lib/validations.js"
],
"codeSectionDepth": 4,

View File

@ -12,3 +12,5 @@ var commonTest = './test/common_test';
Object.defineProperty(exports, 'test', {
get: function() {return require(commonTest);}
});
exports.Transaction = require('loopback-connector').Transaction;

View File

@ -366,7 +366,7 @@ Memory.prototype.all = function all(model, filter, options, callback) {
process.nextTick(function () {
if (filter && filter.include) {
self._models[model].model.include(nodes, filter.include, callback);
self._models[model].model.include(nodes, filter.include, options, callback);
} else {
callback(null, nodes);
}

View File

@ -947,7 +947,7 @@ DataAccessObject._normalize = function (filter) {
// normalize fields as array of included property names
if (filter.fields) {
filter.fields = fieldsToArray(filter.fields,
Object.keys(this.definition.properties));
Object.keys(this.definition.properties), this.settings.strict);
}
filter = removeUndefined(filter);
@ -2068,10 +2068,10 @@ DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
* @param {Mixed} value Value of property
* @param {Function} cb Callback function called with (err, instance)
*/
DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, cb) {
DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, options, cb) {
var data = {};
data[name] = value;
return this.updateAttributes(data, cb);
return this.updateAttributes(data, options, cb);
};
/**
@ -2324,3 +2324,8 @@ jutil.mixin(DataAccessObject, Inclusion);
* Add 'relation'
*/
jutil.mixin(DataAccessObject, Relation);
/*
* Add 'transaction'
*/
jutil.mixin(DataAccessObject, require('./transaction'));

View File

@ -4,6 +4,7 @@
var ModelBuilder = require('./model-builder.js').ModelBuilder;
var ModelDefinition = require('./model-definition.js');
var RelationDefinition = require('./relation-definition.js');
var OberserverMixin = require('./observer');
var jutil = require('./jutil');
var utils = require('./utils');
var ModelBaseClass = require('./model.js');
@ -33,11 +34,10 @@ var slice = Array.prototype.slice;
/**
* LoopBack models can manipulate data via the DataSource object.
* Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`;
* some of the added methods may be remote methods.
* Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`.
*
* Define a data source for persisting models.
* Typically, you create a DataSource by calling createDataSource() on the LoopBack object; for example:
* Define a data source to persist model data.
* To create a DataSource programmatically, call `createDataSource()` on the LoopBack object; for example:
* ```js
* var oracle = loopback.createDataSource({
* connector: 'oracle',
@ -49,15 +49,10 @@ var slice = Array.prototype.slice;
* ```
*
* All classes in single dataSource share same the connector type and
* one database connection. The `settings` argument is an object that can have the following properties:
* - host
* - port
* - username
* - password
* - database
* - debug (Boolean, default is false)
* one database connection.
*
* For example, the following creates a DataSource, and waits for a connection callback.
*
* @desc For example, the following creates a DataSource, and waits for a connection callback.
* ```
* var dataSource = new DataSource('mysql', { database: 'myapp_test' });
* dataSource.define(...);
@ -66,8 +61,21 @@ var slice = Array.prototype.slice;
* });
* ```
* @class DataSource
* @param {String} name Type of dataSource connector (mysql, mongoose, oracle, redis)
* @param {Object} settings Database-specific settings to establish connection (settings depend on specific connector). See above.
* @param {String} [name] Optional name for datasource.
* @options {Object} settings Database-specific settings to establish connection (settings depend on specific connector).
* The table below lists a typical set for a relational database.
* @property {String} connector Database connector to use. For any supported connector, can be any of:
*
* - The connector module from `require(connectorName)`.
* - The full name of the connector module, such as 'loopback-connector-oracle'.
* - The short name of the connector module, such as 'oracle'.
* - A local module under `./connectors/` folder.
* @property {String} host Database server host name.
* @property {String} port Database server port number.
* @property {String} username Database user name.
* @property {String} password Database password.
* @property {String} database Name of the database to use.
* @property {Boolean} debug Display debugging information. Default is false.
*/
function DataSource(name, settings, modelBuilder) {
if (!(this instanceof DataSource)) {
@ -175,6 +183,8 @@ DataSource.prototype._setupConnector = function () {
log(q || query, t1);
};
};
// Configure the connector instance to mix in observer functions
jutil.mixin(this.connector, OberserverMixin);
}
};
@ -2097,4 +2107,3 @@ DataSource.Any = ModelBuilder.Any;
DataSource.registerType = function (type) {
ModelBuilder.registerType(type);
};

View File

@ -1,8 +1,8 @@
var async = require('async');
var utils = require('./utils');
var List = require('./list');
var isPlainObject = utils.isPlainObject;
var defineCachedRelations = utils.defineCachedRelations;
var debug = require('debug')('loopback:include');
/*!
* Normalize the include to be an array
@ -145,11 +145,15 @@ Inclusion.normalizeInclude = normalizeInclude;
*
* @param {Array} objects Array of instances
* @param {String|Object|Array} include Which relations to load.
* @param {Object} [options] Options for CRUD
* @param {Function} cb Callback called when relations are loaded
*
*/
Inclusion.include = function (objects, include, cb) {
debug('include', include);
Inclusion.include = function (objects, include, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var self = this;
if (!include || (Array.isArray(include) && include.length === 0) ||
@ -163,12 +167,12 @@ Inclusion.include = function (objects, include, cb) {
include = normalizeInclude(include);
async.each(include, function(item, callback) {
processIncludeItem(objects, item, callback);
processIncludeItem(objects, item, options, callback);
}, function(err) {
cb && cb(err, objects);
});
function processIncludeItem(objs, include, cb) {
function processIncludeItem(objs, include, options, cb) {
var relations = self.relations;
var relationName;
@ -214,8 +218,7 @@ Inclusion.include = function (objects, include, cb) {
// Just skip if inclusion is disabled
if (relation.options.disableInclude) {
cb();
return;
return cb();
}
//prepare filter and fields for making DB Call
var filter = (scope && scope.conditions()) || {};
@ -350,6 +353,7 @@ Inclusion.include = function (objects, include, cb) {
filter.where[modelToIdName] = {
inq: targetIds
};
//make sure that the modelToIdName is included if fields are specified
if (Array.isArray(fields) && fields.indexOf(modelToIdName) === -1) {
fields.push(modelToIdName);
@ -371,7 +375,7 @@ Inclusion.include = function (objects, include, cb) {
//process.
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, next);
relation.modelTo.include(targets, subInclude, options, next);
});
}
//process & link each target with object
@ -430,7 +434,7 @@ Inclusion.include = function (objects, include, cb) {
filter.where[relation.keyTo] = {
inq: allTargetIds
};
relation.applyScope(null, filter);
/**
* Make the DB Call, fetch all target objects
*/
@ -449,7 +453,7 @@ Inclusion.include = function (objects, include, cb) {
//simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, next);
relation.modelTo.include(targets, subInclude, options, next);
});
}
//process each target object
@ -494,6 +498,7 @@ Inclusion.include = function (objects, include, cb) {
filter.where[relation.keyTo] = {
inq: sourceIds
};
relation.applyScope(null, filter);
relation.modelTo.find(filter, targetFetchHandler);
/**
* Process fetched related objects
@ -509,7 +514,7 @@ Inclusion.include = function (objects, include, cb) {
//simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, next);
relation.modelTo.include(targets, subInclude, options, next);
});
}
//process each target object
@ -571,7 +576,6 @@ Inclusion.include = function (objects, include, cb) {
typeFilter.where[relation.keyTo] = {
inq: targetIds
};
var app = relation.modelFrom.app;
var Model = lookupModel(relation.modelFrom.dataSource.modelBuilder.
models, modelType);
if (!Model) {
@ -579,6 +583,7 @@ Inclusion.include = function (objects, include, cb) {
' specified but no model exists with such name'));
return;
}
relation.applyScope(null, typeFilter);
Model.find(typeFilter, targetFetchHandler);
/**
* Process fetched related objects
@ -595,7 +600,7 @@ Inclusion.include = function (objects, include, cb) {
//simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
Model.include(targets, subInclude, next);
Model.include(targets, subInclude, options, next);
});
}
//process each target object
@ -647,6 +652,7 @@ Inclusion.include = function (objects, include, cb) {
filter.where[relation.keyTo] = {
inq: targetIds
};
relation.applyScope(null, filter);
relation.modelTo.find(filter, targetFetchHandler);
/**
* Process fetched related objects
@ -662,7 +668,7 @@ Inclusion.include = function (objects, include, cb) {
//simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, next);
relation.modelTo.include(targets, subInclude, options, next);
});
}
//process each target object
@ -729,6 +735,9 @@ Inclusion.include = function (objects, include, cb) {
*/
function setIncludeData(result, cb) {
if (obj === inst) {
if (Array.isArray(result) && !(result instanceof List)) {
result = new List(result, relation.modelTo);
}
obj.__data[relationName] = result;
obj.setStrict(false);
} else {
@ -766,10 +775,10 @@ Inclusion.include = function (objects, include, cb) {
related = inst[relationName].bind(inst, filter);
} else {
related = inst[relationName].bind(inst);
related = inst[relationName].bind(inst, undefined);
}
related(function (err, result) {
related(options, function (err, result) {
if (err) {
return callback(err);
} else {

View File

@ -181,8 +181,9 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
} else if (ctor.relations[p]) {
var relationType = ctor.relations[p].type;
var modelTo;
if (!properties[p]) {
var modelTo = ctor.relations[p].modelTo || ModelBaseClass;
modelTo = ctor.relations[p].modelTo || ModelBaseClass;
var multiple = ctor.relations[p].multiple;
var typeName = multiple ? 'Array' : modelTo.modelName;
var propType = multiple ? [modelTo] : modelTo;
@ -196,7 +197,8 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];
if (ctor.relations[p].options.embedsProperties) {
var fields = fieldsToArray(ctor.relations[p].properties, modelTo.definition.properties);
var fields = fieldsToArray(ctor.relations[p].properties,
modelTo.definition.properties, modelTo.setting.strict);
if (!~fields.indexOf(ctor.relations[p].keyTo)) {
fields.push(ctor.relations[p].keyTo);
}
@ -604,87 +606,8 @@ 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);
};
/**
* Unregister an asynchronous observer for the given operation (event).
* @param {String} operation The operation name.
* @callback {function} listener The listener function.
* @end
*/
ModelBaseClass.removeObserver = function(operation, listener) {
if (!this._observers[operation]) return;
var index = this._observers[operation].indexOf(listener);
if (index != -1) this._observers[operation].splice(index, 1);
};
/**
* Unregister all asynchronous observers for the given operation (event).
* @param {String} operation The operation name.
* @end
*/
ModelBaseClass.clearObservers = function(operation) {
if (!this._observers[operation]) return;
this._observers[operation].length = 0;
};
/**
* 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];
if (!callback) callback = utils.createPromiseCallback();
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 notifySingleObserver(fn, next) {
var retval = fn(context, next);
if (retval && typeof retval.then === 'function') {
retval.then(
function() { next(); },
next // error handler
);
}
},
function(err) { callback(err, context) }
);
});
return callback.promise;
}
ModelBaseClass._notifyBaseObservers = function(operation, context, callback) {
if (this.base && this.base.notifyObserversOf)
this.base.notifyObserversOf(operation, context, callback);
else
callback();
}
// Mixin observer
jutil.mixin(ModelBaseClass, require('./observer'));
jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable);

146
lib/observer.js Normal file
View File

@ -0,0 +1,146 @@
var async = require('async');
var utils = require('./utils');
module.exports = ObserverMixin;
/**
* ObserverMixin class. Use to add observe/notifyObserversOf APIs to other
* classes.
*
* @class ObserverMixin
*/
function ObserverMixin() {
}
/**
* 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
*/
ObserverMixin.observe = function(operation, listener) {
this._observers = this._observers || {};
if (!this._observers[operation]) {
this._observers[operation] = [];
}
this._observers[operation].push(listener);
};
/**
* Unregister an asynchronous observer for the given operation (event).
* @param {String} operation The operation name.
* @callback {function} listener The listener function.
* @end
*/
ObserverMixin.removeObserver = function(operation, listener) {
if (!(this._observers && this._observers[operation])) return;
var index = this._observers[operation].indexOf(listener);
if (index !== -1) {
return this._observers[operation].splice(index, 1);
}
};
/**
* Unregister all asynchronous observers for the given operation (event).
* @param {String} operation The operation name.
* @end
*/
ObserverMixin.clearObservers = function(operation) {
if (!(this._observers && this._observers[operation])) return;
this._observers[operation].length = 0;
};
/**
* 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.
*/
ObserverMixin.notifyObserversOf = function(operation, context, callback) {
var observers = this._observers && this._observers[operation];
if (!callback) callback = utils.createPromiseCallback();
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 notifySingleObserver(fn, next) {
var retval = fn(context, next);
if (retval && typeof retval.then === 'function') {
retval.then(
function() { next(); },
next // error handler
);
}
},
function(err) { callback(err, context) }
);
});
return callback.promise;
};
ObserverMixin._notifyBaseObservers = function(operation, context, callback) {
if (this.base && this.base.notifyObserversOf)
this.base.notifyObserversOf(operation, context, callback);
else
callback();
};
/**
* Run the given function with before/after observers. It's done in three serial
* steps asynchronously:
*
* - Notify the registered observers under 'before ' + operation
* - Execute the function
* - Notify the registered observers under 'after ' + operation
*
* If an error happens, it fails fast and calls the callback with err.
*
* @param {String} operation The operation name
* @param {Context} context The context object
* @param {Function} fn The task to be invoked as fn(done) or fn(context, done)
* @param {Function} callback The callback function
* @returns {*}
*/
ObserverMixin.notifyObserversAround = function(operation, context, fn, callback) {
var self = this;
return self.notifyObserversOf('before ' + operation, context,
function(err, context) {
if (err) return callback(err, context);
function cbForWork(err) {
if (err) return callback(err, context);
var returnedArgs = [].slice.call(arguments, 1);
context.results = returnedArgs;
self.notifyObserversOf('after ' + operation, context,
function(err, context) {
if (err) return callback(err, context);
var results = returnedArgs;
if (context) {
results = context.results;
}
var args = [err].concat(results);
callback.apply(null, args);
});
}
if (fn.length === 1) {
// fn(done)
fn(cbForWork);
} else {
// fn(context, done)
fn(context, cbForWork);
}
});
};

File diff suppressed because it is too large Load Diff

View File

@ -21,10 +21,11 @@ function ScopeDefinition(definition) {
}
ScopeDefinition.prototype.targetModel = function(receiver) {
var modelTo;
if (typeof this.options.modelTo === 'function') {
var modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
} else {
var modelTo = this.modelTo;
modelTo = this.modelTo;
}
if (!(modelTo.prototype instanceof DefaultModelBaseClass)) {
var msg = 'Invalid target model for scope `';
@ -36,16 +37,34 @@ ScopeDefinition.prototype.targetModel = function(receiver) {
return modelTo;
};
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
/*!
* Find related model instances
* @param {*} receiver The target model class/prototype
* @param {Object|Function} scopeParams
* @param {Boolean|Object} [condOrRefresh] true for refresh or object as a filter
* @param {Object} [options]
* @param {Function} cb
* @returns {*}
*/
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
var name = this.name;
var self = receiver;
var actualCond = {};
var actualRefresh = false;
var saveOnCache = true;
if (arguments.length === 3) {
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// related(receiver, scopeParams, cb)
cb = condOrRefresh;
} else if (arguments.length === 4) {
options = {};
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
options = options || {};
if (condOrRefresh !== undefined) {
if (typeof condOrRefresh === 'boolean') {
actualRefresh = condOrRefresh;
} else {
@ -53,16 +72,15 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
actualRefresh = true;
saveOnCache = false;
}
} else {
throw new Error('Method can be only called with one or two arguments');
}
cb = cb || utils.createPromiseCallback();
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|| actualRefresh) {
// It either doesn't hit the cache or refresh is required
var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
var targetModel = this.targetModel(receiver);
targetModel.find(params, function (err, data) {
targetModel.find(params, options, function (err, data) {
if (!err && saveOnCache) {
defineCachedRelations(self);
self.__cachedRelations[name] = data;
@ -73,6 +91,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
// Return from cache
cb(null, self.__cachedRelations[name]);
}
return cb.promise;
}
/**
@ -149,7 +168,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
var targetModel = definition.targetModel(this);
var self = this;
var f = function(condOrRefresh, cb) {
var f = function(condOrRefresh, options, cb) {
if (arguments.length === 0) {
if (typeof f.value === 'function') {
return f.value(self);
@ -157,6 +176,18 @@ function defineScope(cls, targetClass, name, params, methods, options) {
return self.__cachedRelations[name];
}
} else {
if (typeof condOrRefresh === 'function'
&& options === undefined && cb === undefined) {
// customer.orders(cb)
cb = condOrRefresh;
options = {};
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
// customer.orders(condOrRefresh, cb);
cb = options;
options = {};
}
options = options || {}
// Check if there is a through model
// see https://github.com/strongloop/loopback/issues/1076
if (f._scope.collect &&
@ -176,11 +207,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
};
condOrRefresh = {};
}
if (arguments.length === 1) {
return definition.related(self, f._scope, condOrRefresh);
} else {
return definition.related(self, f._scope, condOrRefresh, cb);
}
return definition.related(self, f._scope, condOrRefresh, options, cb);
}
};
@ -193,23 +220,20 @@ function defineScope(cls, targetClass, name, params, methods, options) {
f._targetClass = i8n.camelize(f._scope.collect);
}
f.getAsync = function (cond, cb) {
if (cb === undefined) {
if (cond === undefined) {
// getAsync()
cb = utils.createPromiseCallback();
cond = true;
} else if (typeof cond !== 'function') {
// getAsync({where:{}})
cb = utils.createPromiseCallback();
} else {
// getAsync(function(){})
cb = cond;
cond = true;
}
f.getAsync = function(condOrRefresh, options, cb) {
if (typeof condOrRefresh === 'function'
&& options === undefined && cb === undefined) {
// customer.orders.getAsync(cb)
cb = condOrRefresh;
options = {};
condOrRefresh = {};
} else if (typeof options === 'function' && cb === undefined) {
// customer.orders.getAsync(condOrRefresh, cb);
cb = options;
options = {};
}
definition.related(self, f._scope, cond, cb);
return cb.promise;
options = options || {}
return definition.related(self, f._scope, condOrRefresh, options, cb);
}
f.build = build;
@ -305,13 +329,19 @@ function defineScope(cls, targetClass, name, params, methods, options) {
return new targetModel(data);
}
function create(data, cb) {
if (typeof data === 'function') {
function create(data, options, cb) {
if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// create(cb)
cb = data;
data = {};
} else if (typeof options === 'function' && cb === undefined) {
// create(data, cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
return this.build(data).save(cb);
options = options || {};
return this.build(data).save(options, cb);
}
/*
@ -320,53 +350,108 @@ function defineScope(cls, targetClass, name, params, methods, options) {
- For every destroy call which results in an error
- If fetching the Elements on which destroyAll is called results in an error
*/
function destroyAll(where, cb) {
if (typeof where === 'function') cb = where, where = {};
cb = cb || utils.createPromiseCallback();
function destroyAll(where, options, cb) {
if (typeof where === 'function') {
// destroyAll(cb)
cb = where;
where = {};
} else if (typeof options === 'function' && cb === undefined) {
// destroyAll(where, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
return targetModel.destroyAll(filter.where, cb);
return targetModel.destroyAll(filter.where, options, cb);
}
function updateAll(where, data, cb) {
if (arguments.length === 2) {
// Handle updateAll(data, cb)
function updateAll(where, data, options, cb) {
if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// updateAll(data, cb)
cb = data;
data = where;
where = {};
options = {};
} else if (typeof options === 'function' && cb === undefined) {
// updateAll(where, data, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetModel.updateAll(filter.where, data, cb);
return targetModel.updateAll(filter.where, data, options, cb);
}
function findById(id, cb) {
function findById(id, filter, options, cb) {
if (options === undefined && cb === undefined) {
if (typeof filter === 'function') {
// findById(id, cb)
cb = filter;
filter = {};
}
} else if (cb === undefined) {
if (typeof options === 'function') {
// findById(id, query, cb)
cb = options;
options = {};
if (typeof filter === 'object' && !(filter.include || filter.fields)) {
// If filter doesn't have include or fields, assuming it's options
options = filter;
filter = {};
}
}
}
options = options || {};
filter = filter || {};
var targetModel = definition.targetModel(this._receiver);
var idName = targetModel.definition.idName();
var filter = { where: {} };
filter.where[idName] = id;
this.findOne(filter, cb);
var query = {where: {}};
query.where[idName] = id;
query = mergeQuery(query, filter);
return this.findOne(query, options, cb);
}
function findOne(filter, cb) {
if (typeof filter === 'function') cb = filter, filter = {};
function findOne(filter, options, cb) {
if (typeof filter === 'function') {
// findOne(cb)
cb = filter;
filter = {};
options = {};
} else if (typeof options === 'function' && cb === undefined) {
// findOne(filter, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, filter || {});
targetModel.findOne(filter, cb);
filter = mergeQuery({ where: scoped }, filter || {});
return targetModel.findOne(filter, options, cb);
}
function count(where, cb) {
if (typeof where === 'function') cb = where, where = {};
cb = cb || utils.createPromiseCallback();
function count(where, options, cb) {
if (typeof where === 'function') {
// count(cb)
cb = where;
where = {};
} else if (typeof options === 'function' && cb === undefined) {
// count(where, cb)
cb = options;
options = {};
}
options = options || {};
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
return targetModel.count(filter.where, cb);
return targetModel.count(filter.where, options, cb);
}
return definition;

181
lib/transaction.js Normal file
View File

@ -0,0 +1,181 @@
var debug = require('debug')('loopback:connector:transaction');
var uuid = require('node-uuid');
var utils = require('./utils');
var jutil = require('./jutil');
var ObserverMixin = require('./observer');
var Transaction = require('loopback-connector').Transaction;
module.exports = TransactionMixin;
/**
* TransactionMixin class. Use to add transaction APIs to a model class.
*
* @class TransactionMixin
*/
function TransactionMixin() {
}
/**
* Begin a new transaction
* @param {Object|String} [options] Options can be one of the forms:
* - Object: {isolationLevel: '...', timeout: 1000}
* - String: isolationLevel
*
* Valid values of `isolationLevel` are:
*
* - Transaction.READ_COMMITTED = 'READ COMMITTED'; // default
* - Transaction.READ_UNCOMMITTED = 'READ UNCOMMITTED';
* - Transaction.SERIALIZABLE = 'SERIALIZABLE';
* - Transaction.REPEATABLE_READ = 'REPEATABLE READ';
*
* @param {Function} cb Callback function. It calls back with (err, transaction).
* To pass the transaction context to one of the CRUD methods, use the `options`
* argument with `transaction` property, for example,
*
* ```js
*
* MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
* MyModel.create({x: 1, y: 'a'}, {transaction: tx}, function(err, inst) {
* MyModel.find({x: 1}, {transaction: tx}, function(err, results) {
* // ...
* tx.commit(function(err) {...});
* });
* });
* });
* ```
*
* The transaction can be committed or rolled back. If timeout happens, the
* transaction will be rolled back. Please note a transaction is typically
* associated with a pooled connection. Committing or rolling back a transaction
* will release the connection back to the pool.
*
* Once the transaction is committed or rolled back, the connection property
* will be set to null to mark the transaction to be inactive. Trying to commit
* or rollback an inactive transaction will receive an error from the callback.
*
* Please also note that the transaction is only honored with the same data
* source/connector instance. CRUD methods will not join the current transaction
* if its model is not attached the same data source.
*
*/
TransactionMixin.beginTransaction = function(options, cb) {
cb = cb || utils.createPromiseCallback();
if (Transaction) {
var connector = this.getConnector();
Transaction.begin(connector, options, function(err, transaction) {
if (err) return cb(err);
if (transaction) {
// Set an informational transaction id
transaction.id = uuid.v1();
}
if (options.timeout) {
setTimeout(function() {
var context = {
transaction: transaction,
operation: 'timeout'
};
transaction.notifyObserversOf('timeout', context, function(err) {
if (!err) {
transaction.rollback(function() {
debug('Transaction %s is rolled back due to timeout',
transaction.id);
});
}
});
}, options.timeout);
}
cb(err, transaction);
});
} else {
process.nextTick(function() {
var err = new Error('Transaction is not supported');
cb(err);
});
}
return cb.promise;
};
// Promisify the transaction apis
if (Transaction) {
jutil.mixin(Transaction.prototype, ObserverMixin);
/**
* Commit a transaction and release it back to the pool
* @param {Function} cb Callback function
* @returns {Promise|undefined}
*/
Transaction.prototype.commit = function(cb) {
var self = this;
cb = cb || utils.createPromiseCallback();
// Report an error if the transaction is not active
if (!self.connection) {
process.nextTick(function() {
cb(new Error('The transaction is not active: ' + self.id));
});
return cb.promise;
}
var context = {
transaction: self,
operation: 'commit'
};
function work(done) {
self.connector.commit(self.connection, done);
}
self.notifyObserversAround('commit', context, work, function(err) {
// Deference the connection to mark the transaction is not active
// The connection should have been released back the pool
self.connection = null;
cb(err);
});
return cb.promise;
};
/**
* Rollback a transaction and release it back to the pool
* @param {Function} cb Callback function
* @returns {Promise|undefined}
*/
Transaction.prototype.rollback = function(cb) {
var self = this;
cb = cb || utils.createPromiseCallback();
// Report an error if the transaction is not active
if (!self.connection) {
process.nextTick(function() {
cb(new Error('The transaction is not active: ' + self.id));
});
return cb.promise;
}
var context = {
transaction: self,
operation: 'rollback'
};
function work(done) {
self.connector.rollback(self.connection, done);
}
self.notifyObserversAround('rollback', context, work, function(err) {
// Deference the connection to mark the transaction is not active
// The connection should have been released back the pool
self.connection = null;
cb(err);
});
return cb.promise;
};
Transaction.prototype.toJSON = function() {
return this.id;
};
Transaction.prototype.toString = function() {
return this.id;
};
}
TransactionMixin.Transaction = Transaction;

View File

@ -149,7 +149,7 @@ function mergeQuery(base, update, spec) {
}
spec = spec || {};
base = base || {};
if (update.where && Object.keys(update.where).length > 0) {
if (base.where && Object.keys(base.where).length > 0) {
base.where = {and: [base.where, update.where]};
@ -178,81 +178,96 @@ function mergeQuery(base, update, spec) {
}
}
}
if (spec.collect !== false && update.collect) {
base.collect = update.collect;
}
// Overwrite fields
if (spec.fields !== false && update.fields !== undefined) {
base.fields = update.fields;
} else if (update.fields !== undefined) {
base.fields = [].concat(base.fields).concat(update.fields);
}
// set order
if ((!base.order || spec.order === false) && update.order) {
base.order = update.order;
}
// overwrite pagination
if (spec.limit !== false && update.limit !== undefined) {
base.limit = update.limit;
}
var skip = spec.skip !== false && spec.offset !== false;
if (skip && update.skip !== undefined) {
base.skip = update.skip;
}
if (skip && update.offset !== undefined) {
base.offset = update.offset;
}
return base;
}
function fieldsToArray(fields, properties) {
/**
* Normalize fields to an array of included properties
* @param {String|String[]|Object} fields Fields filter
* @param {String[]} properties Property names
* @param {Boolean} excludeUnknown To exclude fields that are unknown properties
* @returns {String[]} An array of included property names
*/
function fieldsToArray(fields, properties, excludeUnknown) {
if (!fields) return;
// include all properties by default
var result = properties;
var i, n;
if (typeof fields === 'string') {
return [fields];
}
if (Array.isArray(fields) && fields.length > 0) {
result = [fields];
} else if (Array.isArray(fields) && fields.length > 0) {
// No empty array, including all the fields
return fields;
}
if ('object' === typeof fields) {
result = fields;
} else if ('object' === typeof fields) {
// { field1: boolean, field2: boolean ... }
var included = [];
var excluded = [];
var keys = Object.keys(fields);
if (!keys.length) return;
keys.forEach(function (k) {
for (i = 0, n = keys.length; i < n; i++) {
var k = keys[i];
if (fields[k]) {
included.push(k);
} else if ((k in fields) && !fields[k]) {
excluded.push(k);
}
});
}
if (included.length > 0) {
result = included;
} else if (excluded.length > 0) {
excluded.forEach(function (e) {
var index = result.indexOf(e);
for (i = 0, n = excluded.length; i < n; i++) {
var index = result.indexOf(excluded[i]);
result.splice(index, 1);
});
}
}
}
return result;
var fieldArray = [];
if (excludeUnknown) {
for (i = 0, n = result.length; i < n; i++) {
if (properties.indexOf(result[i]) !== -1) {
fieldArray.push(result[i]);
}
}
} else {
fieldArray = result;
}
return fieldArray;
}
function selectFields(fields) {
@ -404,16 +419,16 @@ function sortObjectsByIds(idName, ids, objects, strict) {
ids = ids.map(function(id) {
return (typeof id === 'object') ? String(id) : id;
});
var indexOf = function(x) {
var isObj = (typeof x[idName] === 'object'); // ObjectID
var id = isObj ? String(x[idName]) : x[idName];
return ids.indexOf(id);
};
var heading = [];
var tailing = [];
objects.forEach(function(x) {
if (typeof x === 'object') {
var idx = indexOf(x);
@ -421,7 +436,7 @@ function sortObjectsByIds(idName, ids, objects, strict) {
idx === -1 ? tailing.push(x) : heading.push(x);
}
});
heading.sort(function(x, y) {
var a = indexOf(x);
var b = indexOf(y);
@ -430,7 +445,7 @@ function sortObjectsByIds(idName, ids, objects, strict) {
if (a > b) return 1;
if (a < b) return -1;
});
return heading.concat(tailing);
};

View File

@ -1,6 +1,6 @@
{
"name": "loopback-datasource-juggler",
"version": "2.27.0",
"version": "2.28.1",
"description": "LoopBack DataSoure Juggler",
"keywords": [
"StrongLoop",
@ -28,15 +28,14 @@
"devDependencies": {
"bluebird": "^2.9.9",
"mocha": "^2.1.0",
"should": "^5.0.0",
"sinon": "^1.14.1"
"should": "^5.0.0"
},
"dependencies": {
"async": "^0.9.0",
"debug": "^2.1.1",
"depd": "^1.0.0",
"inflection": "^1.6.0",
"loopback-connector": "1.x",
"loopback-connector": "^2.1.0",
"node-uuid": "^1.4.2",
"qs": "^2.3.3",
"traverse": "^0.6.6"

View File

@ -132,6 +132,70 @@ describe('async observer', function() {
});
});
describe('notifyObserversAround', function() {
var notifications;
beforeEach(function() {
notifications = [];
TestModel.observe('before execute',
pushAndNext(notifications, 'before execute'));
TestModel.observe('after execute',
pushAndNext(notifications, 'after execute'));
});
it('should notify before/after observers', function(done) {
var context = {};
function work(done) {
process.nextTick(function() {
done(null, 1);
});
}
TestModel.notifyObserversAround('execute', context, work,
function(err, result) {
notifications.should.eql(['before execute', 'after execute']);
result.should.eql(1);
done();
});
});
it('should allow work with context', function(done) {
var context = {};
function work(context, done) {
process.nextTick(function() {
done(null, 1);
});
}
TestModel.notifyObserversAround('execute', context, work,
function(err, result) {
notifications.should.eql(['before execute', 'after execute']);
result.should.eql(1);
done();
});
});
it('should notify before/after observers with multiple results',
function(done) {
var context = {};
function work(done) {
process.nextTick(function() {
done(null, 1, 2);
});
}
TestModel.notifyObserversAround('execute', context, work,
function(err, r1, r2) {
r1.should.eql(1);
r2.should.eql(2);
notifications.should.eql(['before execute', 'after execute']);
done();
});
});
});
it('resolves promises returned by observers', function(done) {
TestModel.observe('event', function(ctx) {
return Promise.resolve('value-to-ignore');

View File

@ -1,7 +1,9 @@
// This test written in mocha+should.js
var should = require('./init.js');
var sinon = require('sinon');
var async = require('async');
var assert = require('assert');
var DataSource = require('../').DataSource;
var db, User, Profile, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
@ -382,12 +384,19 @@ describe('include', function () {
});
});
describe(' performance - ', function () {
beforeEach(function () {
this.callSpy = sinon.spy(db.connector, 'all');
describe('performance', function () {
var all;
beforeEach(function() {
this.called = 0;
var self = this;
all = db.connector.all;
db.connector.all = function(model, filter, options, cb) {
self.called++;
return all.apply(db.connector, arguments);
};
});
afterEach(function () {
db.connector.all.restore();
afterEach(function() {
db.connector.all = all;
});
it('including belongsTo should make only 2 db calls', function (done) {
var self = this;
@ -407,7 +416,7 @@ describe('include', function () {
owner.id.should.eql(p.ownerId);
}
});
self.callSpy.calledTwice.should.be.true;
self.called.should.eql(2);
done();
});
});
@ -434,7 +443,7 @@ describe('include', function () {
});
}, next);
}, function (err) {
self.callSpy.reset();
self.called = 0;
Assembly.find({
where: {
name: {
@ -452,7 +461,7 @@ describe('include', function () {
result[1].parts().should.have.length(2);
//SUV
result[2].parts().should.have.length(0);
self.callSpy.calledThrice.should.be.true;
self.called.should.eql(3);
done();
});
});
@ -489,7 +498,7 @@ describe('include', function () {
pp.ownerId.should.eql(user.id);
});
});
self.callSpy.calledThrice.should.be.true;
self.called.should.eql(3);
done();
});
});
@ -528,7 +537,7 @@ describe('include', function () {
pp.ownerId.should.eql(user.id);
});
});
self.callSpy.calledThrice.should.be.true;
self.called.should.eql(3);
done();
});
});
@ -684,3 +693,95 @@ function clearAndCreate(model, data, callback) {
itemIndex++;
}
}
describe('Model instance with included relation .toJSON()', function() {
var db, ChallengerModel, GameParticipationModel, ResultModel;
before(function(done) {
db = new DataSource({connector: 'memory'});
ChallengerModel = db.createModel('Challenger',
{
name: String
},
{
relations: {
gameParticipations: {
type: 'hasMany',
model: 'GameParticipation',
foreignKey: ''
}
}
}
);
GameParticipationModel = db.createModel('GameParticipation',
{
date: Date
},
{
relations: {
challenger: {
type: 'belongsTo',
model: 'Challenger',
foreignKey: ''
},
results: {
type: 'hasMany',
model: 'Result',
foreignKey: ''
}
}
}
);
ResultModel = db.createModel('Result', {
points: Number,
}, {
relations: {
gameParticipation: {
type: 'belongsTo',
model: 'GameParticipation',
foreignKey: ''
}
}
});
async.waterfall([
createChallengers,
createGameParticipations,
createResults],
function(err) {
done(err);
});
});
function createChallengers(callback) {
ChallengerModel.create([{name: 'challenger1'}, {name: 'challenger2'}], callback);
}
function createGameParticipations(challengers, callback) {
GameParticipationModel.create([
{challengerId: challengers[0].id, date: Date.now()},
{challengerId: challengers[0].id, date: Date.now()}
], callback);
}
function createResults(gameParticipations, callback) {
ResultModel.create([
{gameParticipationId: gameParticipations[0].id, points: 10},
{gameParticipationId: gameParticipations[0].id, points: 20}
], callback);
}
it('should recursively serialize objects', function(done) {
var filter = {include: {gameParticipations: 'results'}};
ChallengerModel.find(filter, function(err, challengers) {
var levelOneInclusion = challengers[0].toJSON().gameParticipations[0];
assert(levelOneInclusion.__data === undefined, '.__data of a level 1 inclusion is undefined.');
var levelTwoInclusion = challengers[0].toJSON().gameParticipations[0].results[0];
assert(levelTwoInclusion.__data === undefined, '__data of a level 2 inclusion is undefined.');
done();
});
});
});

View File

@ -584,4 +584,48 @@ describe('Memory connector with options', function() {
});
describe('Memory connector with observers', function() {
var ds = new DataSource({
connector: 'memory'
});
it('should have observer mixed into the connector', function() {
ds.connector.observe.should.be.a.function;
ds.connector.notifyObserversOf.should.be.a.function;
});
it('should notify observers', function(done) {
var events = [];
ds.connector.execute = function(command, params, options, cb) {
var self = this;
var context = {command: command, params: params, options: options};
self.notifyObserversOf('before execute', context, function(err) {
process.nextTick(function() {
if (err) return cb(err);
events.push('execute');
self.notifyObserversOf('after execute', context, function(err) {
cb(err);
});
});
});
};
ds.connector.observe('before execute', function(context, next) {
events.push('before execute');
next();
});
ds.connector.observe('after execute', function(context, next) {
events.push('after execute');
next();
});
ds.connector.execute('test', [1, 2], {x: 2}, function(err) {
if (err) return done(err);
events.should.eql(['before execute', 'execute', 'after execute']);
done();
});
});
});

View File

@ -2926,7 +2926,17 @@ describe('relations', function () {
});
});
it('should find record that match scope', function (done) {
it('should include record that matches scope', function(done) {
Supplier.findById(supplierId, {include: 'account'}, function(err, supplier) {
should.exists(supplier.toJSON().account);
supplier.account(function(err, account) {
should.exists(account);
done();
});
});
});
it('should not find record that does not match scope', function (done) {
Account.updateAll({ block: true }, function (err) {
Supplier.findById(supplierId, function (err, supplier) {
supplier.account(function (err, account) {
@ -2937,6 +2947,18 @@ describe('relations', function () {
});
});
it('should not include record that does not match scope', function (done) {
Account.updateAll({ block: true }, function (err) {
Supplier.findById(supplierId, {include: 'account'}, function (err, supplier) {
should.not.exists(supplier.toJSON().account);
supplier.account(function (err, account) {
should.not.exists(account);
done();
});
});
});
});
it('can be used to query data with promises', function (done) {
db.automigrate(function () {
Supplier.create({name: 'Supplier 1'})

View File

@ -6,12 +6,6 @@
}
*/
try {
global.sinon = require('sinon');
} catch (e) {
// ignore
}
var group_name = false, EXT_EXP;
function it(should, test_case) {
check_external_exports();

View File

@ -7,16 +7,17 @@ var mergeIncludes = utils.mergeIncludes;
var sortObjectsByIds = utils.sortObjectsByIds;
describe('util.fieldsToArray', function () {
it('Turn objects and strings into an array of fields to include when finding models', function () {
function sample(fields) {
var properties = ['foo', 'bar', 'bat', 'baz'];
return {
expect: function (arr) {
should.deepEqual(fieldsToArray(fields, properties), arr);
}
function sample(fields, excludeUnknown) {
var properties = ['foo', 'bar', 'bat', 'baz'];
return {
expect: function (arr) {
should.deepEqual(fieldsToArray(fields, properties, excludeUnknown), arr);
}
}
};
}
it('Turn objects and strings into an array of fields' +
' to include when finding models', function () {
sample(false).expect(undefined);
sample(null).expect(undefined);
@ -28,6 +29,19 @@ describe('util.fieldsToArray', function () {
sample({'bat': 0}).expect(['foo', 'bar', 'baz']);
sample({'bat': false}).expect(['foo', 'bar', 'baz']);
});
it('should exclude unknown properties', function () {
sample(false, true).expect(undefined);
sample(null, true).expect(undefined);
sample({}, true).expect(undefined);
sample('foo', true).expect(['foo']);
sample(['foo', 'unknown'], true).expect(['foo']);
sample({'foo': 1, unknown: 1}, true).expect(['foo']);
sample({'bat': true, unknown: true}, true).expect(['bat']);
sample({'bat': 0}, true).expect(['foo', 'bar', 'baz']);
sample({'bat': false}, true).expect(['foo', 'bar', 'baz']);
});
});
describe('util.removeUndefined', function () {
@ -190,7 +204,7 @@ describe('mergeSettings', function () {
});
describe('sortObjectsByIds', function () {
var items = [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
@ -211,7 +225,7 @@ describe('sortObjectsByIds', function () {
var names = sorted.map(function(u) { return u.name; });
should.deepEqual(names, ['e', 'c', 'b', 'a', 'd', 'f']);
});
it('should sort - strict', function() {
var sorted = sortObjectsByIds('id', [5, 3, 2], items, true);
var names = sorted.map(function(u) { return u.name; });