loopback-datasource-juggler/lib/scope.js

498 lines
15 KiB
JavaScript
Raw Normal View History

2016-04-06 14:51:49 +00:00
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2016-10-19 21:35:26 +00:00
'use strict';
2016-04-06 14:51:49 +00:00
2016-05-10 21:25:33 +00:00
/*eslint-disable camelcase*/
var i8n = require('inflection');
var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations;
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var mergeQuery = utils.mergeQuery;
var DefaultModelBaseClass = require('./model.js');
var collectTargetIds = utils.collectTargetIds;
var idName = utils.idName;
2013-04-11 23:23:34 +00:00
/**
* Module exports
*/
exports.defineScope = defineScope;
2014-06-16 17:50:42 +00:00
function ScopeDefinition(definition) {
2014-08-08 22:52:30 +00:00
this.isStatic = definition.isStatic;
this.modelFrom = definition.modelFrom;
this.modelTo = definition.modelTo || definition.modelFrom;
2014-06-16 17:50:42 +00:00
this.name = definition.name;
this.params = definition.params;
this.methods = definition.methods || {};
this.options = definition.options || {};
2014-06-16 17:50:42 +00:00
}
ScopeDefinition.prototype.targetModel = function(receiver) {
var modelTo;
if (typeof this.options.modelTo === 'function') {
modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
} else {
modelTo = this.modelTo;
}
if (!(modelTo.prototype instanceof DefaultModelBaseClass)) {
var msg = 'Invalid target model for scope `';
msg += (this.isStatic ? this.modelFrom : this.modelFrom.constructor).modelName;
msg += this.isStatic ? '.' : '.prototype.';
msg += this.name + '`.';
throw new Error(msg);
}
return modelTo;
};
/*!
* 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) {
2014-06-16 17:50:42 +00:00
var name = this.name;
var self = receiver;
2014-06-16 17:50:42 +00:00
var actualCond = {};
var actualRefresh = false;
var saveOnCache = receiver instanceof DefaultModelBaseClass;
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// related(receiver, scopeParams, cb)
2014-06-16 17:50:42 +00:00
cb = condOrRefresh;
options = {};
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
options = options || {};
if (condOrRefresh !== undefined) {
2014-06-16 17:50:42 +00:00
if (typeof condOrRefresh === 'boolean') {
actualRefresh = condOrRefresh;
} else {
actualCond = condOrRefresh;
actualRefresh = true;
saveOnCache = false;
}
}
cb = cb || utils.createPromiseCallback();
2016-05-10 21:25:33 +00:00
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined ||
actualRefresh) {
2014-06-16 17:50:42 +00:00
// It either doesn't hit the cache or refresh is required
2016-10-19 21:04:05 +00:00
var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
var targetModel = this.targetModel(receiver);
// If there is a through model
// run another query to apply filter on relatedModel(targetModel)
// see github.com/strongloop/loopback-datasource-juggler/issues/166
var scopeOnRelatedModel = params.collect &&
params.include.scope !== null &&
typeof params.include.scope === 'object';
if (scopeOnRelatedModel) {
var filter = params.include;
// The filter applied on relatedModel
var queryRelated = filter.scope;
delete params.include.scope;
};
targetModel.find(params, options, function(err, data) {
2014-06-16 17:50:42 +00:00
if (!err && saveOnCache) {
defineCachedRelations(self);
self.__cachedRelations[name] = data;
}
if (scopeOnRelatedModel === true) {
var relatedModel = targetModel.relations[filter.relation].modelTo;
var IdKey = idName(relatedModel);
// Merge queryRelated filter and targetId filter
var buildWhere = function() {
var IdKeyCondition = {};
IdKeyCondition[IdKey] = collectTargetIds(data, IdKey);
var mergedWhere = {
and: [IdKeyCondition, queryRelated.where],
};
return mergedWhere;
};
if (queryRelated.where !== undefined) {
queryRelated.where = buildWhere();
} else {
queryRelated.where = {};
queryRelated.where[IdKey] = collectTargetIds(data, IdKey);
}
relatedModel.find(queryRelated, cb);
} else {
cb(err, data);
}
2014-06-16 17:50:42 +00:00
});
} else {
// Return from cache
cb(null, self.__cachedRelations[name]);
}
return cb.promise;
};
2014-06-16 17:50:42 +00:00
/**
* Define a scope method
* @param {String} name of the method
* @param {Function} function to define
*/
ScopeDefinition.prototype.defineMethod = function(name, fn) {
return this.methods[name] = fn;
};
/**
* Define a scope to the class
* @param {Model} cls The class where the scope method is added
* @param {Model} targetClass The class that a query to run against
* @param {String} name The name of the scope
* @param {Object|Function} params The parameters object for the query or a function
* to return the query object
* @param methods An object of methods keyed by the method name to be bound to the class
*/
function defineScope(cls, targetClass, name, params, methods, options) {
2014-01-24 17:09:53 +00:00
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
}
2014-06-16 17:50:42 +00:00
// only makes sense to add scope in meta if base and target classes
2014-01-24 17:09:53 +00:00
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
} else if (targetClass) {
2014-01-24 17:09:53 +00:00
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
2013-04-11 23:23:34 +00:00
}
2014-01-24 17:09:53 +00:00
}
2014-08-08 22:52:30 +00:00
options = options || {};
// Check if the cls is the class itself or its prototype
var isStatic = (typeof cls === 'function') || options.isStatic || false;
2014-06-16 17:50:42 +00:00
var definition = new ScopeDefinition({
2014-08-08 22:52:30 +00:00
isStatic: isStatic,
modelFrom: cls,
modelTo: targetClass,
2014-06-16 17:50:42 +00:00
name: name,
params: params,
methods: methods,
options: options,
2014-06-16 17:50:42 +00:00
});
if (isStatic) {
2014-08-08 22:52:30 +00:00
cls.scopes = cls.scopes || {};
cls.scopes[name] = definition;
} else {
cls.constructor.scopes = cls.constructor.scopes || {};
cls.constructor.scopes[name] = definition;
}
2014-01-24 17:09:53 +00:00
// Define a property for the scope
Object.defineProperty(cls, name, {
enumerable: false,
configurable: true,
/**
* This defines a property for the scope. For example, user.accounts or
* User.vips. Please note the cls can be the model class or prototype of
* the model class.
*
* The property value is function. It can be used to query the scope,
* such as user.accounts(condOrRefresh, cb) or User.vips(cb). The value
* can also have child properties for create/build/delete. For example,
* user.accounts.create(act, cb).
*
*/
get: function() {
var targetModel = definition.targetModel(this);
2014-03-13 23:43:38 +00:00
var self = this;
var f = function(condOrRefresh, options, cb) {
if (arguments.length === 0) {
if (typeof f.value === 'function') {
return f.value(self);
} else if (self.__cachedRelations) {
return self.__cachedRelations[name];
}
2014-01-24 17:09:53 +00:00
} else {
2016-05-10 21:25:33 +00:00
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 &&
condOrRefresh !== null && typeof condOrRefresh === 'object') {
f._scope.include = {
relation: f._scope.collect,
scope: condOrRefresh,
};
condOrRefresh = {};
}
return definition.related(self, f._scope, condOrRefresh, options, cb);
2013-04-11 23:23:34 +00:00
}
2014-01-24 17:09:53 +00:00
};
f._receiver = this;
2014-06-16 17:50:42 +00:00
f._scope = typeof definition.params === 'function' ?
definition.params.call(self) : definition.params;
f._targetClass = targetModel.modelName;
if (f._scope.collect) {
f._targetClass = i8n.camelize(f._scope.collect);
}
f.getAsync = function(condOrRefresh, options, cb) {
2016-05-10 21:25:33 +00:00
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 = {};
}
options = options || {};
return definition.related(self, f._scope, condOrRefresh, options, cb);
};
2014-01-24 17:09:53 +00:00
f.build = build;
f.create = create;
2015-03-28 20:25:24 +00:00
f.updateAll = updateAll;
2014-01-24 17:09:53 +00:00
f.destroyAll = destroyAll;
2015-03-20 15:37:46 +00:00
f.findById = findById;
2015-03-21 12:44:06 +00:00
f.findOne = findOne;
f.count = count;
2015-03-24 14:35:56 +00:00
2014-06-16 17:50:42 +00:00
for (var i in definition.methods) {
f[i] = definition.methods[i].bind(self);
2014-01-24 17:09:53 +00:00
}
2015-03-24 14:35:56 +00:00
if (!targetClass) return f;
2014-01-24 17:09:53 +00:00
2014-06-16 17:50:42 +00:00
// Define scope-chaining, such as
// Station.scope('active', {where: {isActive: true}});
// Station.scope('subway', {where: {isUndeground: true}});
// Station.active.subway(cb);
Object.keys(targetClass._scopeMeta).forEach(function(name) {
2014-01-24 17:09:53 +00:00
Object.defineProperty(f, name, {
enumerable: false,
get: function() {
mergeQuery(f._scope, targetModel._scopeMeta[name]);
2014-01-24 17:09:53 +00:00
return f;
},
2014-01-24 17:09:53 +00:00
});
2014-06-16 17:50:42 +00:00
}.bind(self));
2014-01-24 17:09:53 +00:00
return f;
},
2014-01-24 17:09:53 +00:00
});
// Wrap the property into a function for remoting
var fn = function() {
2014-01-24 17:09:53 +00:00
// primaryObject.scopeName, such as user.accounts
var f = this[name];
// set receiver to be the scope property whose value is a function
f.apply(this[name], arguments);
};
cls['__get__' + name] = fn;
var fn_create = function() {
2014-01-24 17:09:53 +00:00
var f = this[name].create;
f.apply(this[name], arguments);
};
cls['__create__' + name] = fn_create;
var fn_delete = function() {
2014-01-24 17:09:53 +00:00
var f = this[name].destroyAll;
f.apply(this[name], arguments);
};
cls['__delete__' + name] = fn_delete;
var fn_update = function() {
2015-03-28 20:25:24 +00:00
var f = this[name].updateAll;
f.apply(this[name], arguments);
};
cls['__update__' + name] = fn_update;
var fn_findById = function(cb) {
2015-03-20 15:37:46 +00:00
var f = this[name].findById;
f.apply(this[name], arguments);
};
cls['__findById__' + name] = fn_findById;
var fn_findOne = function(cb) {
2015-03-21 12:44:06 +00:00
var f = this[name].findOne;
f.apply(this[name], arguments);
};
cls['__findOne__' + name] = fn_findOne;
var fn_count = function(cb) {
var f = this[name].count;
f.apply(this[name], arguments);
};
cls['__count__' + name] = fn_count;
2014-01-24 17:09:53 +00:00
// and it should have create/build methods with binded thisModelNameId param
function build(data) {
2014-06-16 17:50:42 +00:00
data = data || {};
2014-06-19 19:00:49 +00:00
// Find all fixed property values for the scope
var targetModel = definition.targetModel(this._receiver);
2014-06-19 19:00:49 +00:00
var where = (this._scope && this._scope.where) || {};
setScopeValuesFromWhere(data, where, targetModel);
return new targetModel(data);
2014-01-24 17:09:53 +00:00
}
function create(data, options, cb) {
if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// create(cb)
2014-01-24 17:09:53 +00:00
cb = data;
data = {};
} else if (typeof options === 'function' && cb === undefined) {
// create(data, cb)
cb = options;
options = {};
2014-01-24 17:09:53 +00:00
}
options = options || {};
return this.build(data).save(options, cb);
2014-01-24 17:09:53 +00:00
}
2014-01-24 17:09:53 +00:00
/*
Callback
- The callback will be called after all elements are destroyed
- 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, 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 || {};
2015-03-20 15:37:46 +00:00
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
2016-10-19 21:04:05 +00:00
var filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.destroyAll(filter.where, options, cb);
2014-01-24 17:09:53 +00:00
}
function updateAll(where, data, options, cb) {
if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// updateAll(data, cb)
2015-03-28 20:25:24 +00:00
cb = data;
data = where;
where = {};
options = {};
} else if (typeof options === 'function' && cb === undefined) {
// updateAll(where, data, cb)
cb = options;
options = {};
2015-03-28 20:25:24 +00:00
}
options = options || {};
2015-03-28 20:25:24 +00:00
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
2016-10-19 21:04:05 +00:00
var filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.updateAll(filter.where, data, options, cb);
2015-03-28 20:25:24 +00:00
}
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 || {};
2015-03-20 15:37:46 +00:00
var targetModel = definition.targetModel(this._receiver);
var idName = targetModel.definition.idName();
2016-10-19 21:04:05 +00:00
var query = {where: {}};
query.where[idName] = id;
query = mergeQuery(query, filter);
return this.findOne(query, options, cb);
2015-03-21 12:44:06 +00:00
}
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 || {};
2015-03-21 12:44:06 +00:00
var targetModel = definition.targetModel(this._receiver);
2015-03-20 15:37:46 +00:00
var scoped = (this._scope && this._scope.where) || {};
2016-10-19 21:04:05 +00:00
filter = mergeQuery({where: scoped}, filter || {});
return targetModel.findOne(filter, options, cb);
2015-03-20 15:37:46 +00:00
}
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 || {};
2015-03-20 15:37:46 +00:00
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
2016-10-19 21:04:05 +00:00
var filter = mergeQuery({where: scoped}, {where: where || {}});
return targetModel.count(filter.where, options, cb);
}
2015-03-20 15:37:46 +00:00
2014-07-27 14:30:45 +00:00
return definition;
2014-06-16 17:50:42 +00:00
}