Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
69bd7c1233
36
CHANGES.md
36
CHANGES.md
|
@ -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
|
||||
==========================
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
2
index.js
2
index.js
|
@ -12,3 +12,5 @@ var commonTest = './test/common_test';
|
|||
Object.defineProperty(exports, 'test', {
|
||||
get: function() {return require(commonTest);}
|
||||
});
|
||||
|
||||
exports.Transaction = require('loopback-connector').Transaction;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
11
lib/dao.js
11
lib/dao.js
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
89
lib/model.js
89
lib/model.js
|
@ -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);
|
||||
|
|
|
@ -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
193
lib/scope.js
193
lib/scope.js
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
73
lib/utils.js
73
lib/utils.js
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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'})
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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; });
|
||||
|
|
Loading…
Reference in New Issue