328 lines
9.1 KiB
JavaScript
328 lines
9.1 KiB
JavaScript
var i8n = require('inflection');
|
|
var utils = require('./utils');
|
|
var defineCachedRelations = utils.defineCachedRelations;
|
|
/**
|
|
* Module exports
|
|
*/
|
|
exports.defineScope = defineScope;
|
|
exports.mergeQuery = mergeQuery;
|
|
|
|
function ScopeDefinition(definition) {
|
|
this.isStatic = definition.isStatic;
|
|
this.modelFrom = definition.modelFrom;
|
|
this.modelTo = definition.modelTo || definition.modelFrom;
|
|
this.name = definition.name;
|
|
this.params = definition.params;
|
|
this.methods = definition.methods;
|
|
this.options = definition.options;
|
|
}
|
|
|
|
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
|
var name = this.name;
|
|
var self = receiver;
|
|
|
|
var actualCond = {};
|
|
var actualRefresh = false;
|
|
var saveOnCache = true;
|
|
if (arguments.length === 3) {
|
|
cb = condOrRefresh;
|
|
} else if (arguments.length === 4) {
|
|
if (typeof condOrRefresh === 'boolean') {
|
|
actualRefresh = condOrRefresh;
|
|
} else {
|
|
actualCond = condOrRefresh;
|
|
actualRefresh = true;
|
|
saveOnCache = false;
|
|
}
|
|
} else {
|
|
throw new Error('Method can be only called with one or two arguments');
|
|
}
|
|
|
|
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|
|
|| actualRefresh) {
|
|
// It either doesn't hit the cache or refresh is required
|
|
var params = mergeQuery(actualCond, scopeParams);
|
|
return this.modelTo.find(params, function (err, data) {
|
|
if (!err && saveOnCache) {
|
|
defineCachedRelations(self);
|
|
self.__cachedRelations[name] = data;
|
|
}
|
|
cb(err, data);
|
|
});
|
|
} else {
|
|
// Return from cache
|
|
cb(null, self.__cachedRelations[name]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
|
|
// collect meta info about scope
|
|
if (!cls._scopeMeta) {
|
|
cls._scopeMeta = {};
|
|
}
|
|
|
|
// only makes sense to add scope in meta if base and target classes
|
|
// are same
|
|
if (cls === targetClass) {
|
|
cls._scopeMeta[name] = params;
|
|
} else {
|
|
if (!targetClass._scopeMeta) {
|
|
targetClass._scopeMeta = {};
|
|
}
|
|
}
|
|
|
|
options = options || {};
|
|
// Check if the cls is the class itself or its prototype
|
|
var isStatic = (typeof cls === 'function') || options.isStatic || false;
|
|
var definition = new ScopeDefinition({
|
|
isStatic: isStatic,
|
|
modelFrom: cls,
|
|
modelTo: targetClass,
|
|
name: name,
|
|
params: params,
|
|
methods: methods,
|
|
options: options || {}
|
|
});
|
|
|
|
if(isStatic) {
|
|
cls.scopes = cls.scopes || {};
|
|
cls.scopes[name] = definition;
|
|
} else {
|
|
cls.constructor.scopes = cls.constructor.scopes || {};
|
|
cls.constructor.scopes[name] = definition;
|
|
}
|
|
|
|
// 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 self = this;
|
|
var f = function(condOrRefresh, cb) {
|
|
if(arguments.length === 1) {
|
|
definition.related(self, f._scope, condOrRefresh);
|
|
} else {
|
|
definition.related(self, f._scope, condOrRefresh, cb);
|
|
}
|
|
};
|
|
|
|
f._scope = typeof definition.params === 'function' ?
|
|
definition.params.call(self) : definition.params;
|
|
|
|
f._targetClass = definition.modelTo.modelName;
|
|
if (f._scope.collect) {
|
|
f._targetClass = i8n.capitalize(f._scope.collect);
|
|
}
|
|
|
|
f.build = build;
|
|
f.create = create;
|
|
f.destroyAll = destroyAll;
|
|
f.count = count;
|
|
for (var i in definition.methods) {
|
|
f[i] = definition.methods[i].bind(self);
|
|
}
|
|
|
|
// 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) {
|
|
Object.defineProperty(f, name, {
|
|
enumerable: false,
|
|
get: function () {
|
|
mergeQuery(f._scope, targetClass._scopeMeta[name]);
|
|
return f;
|
|
}
|
|
});
|
|
}.bind(self));
|
|
return f;
|
|
}
|
|
});
|
|
|
|
// Wrap the property into a function for remoting
|
|
var fn = function () {
|
|
// 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 () {
|
|
var f = this[name].create;
|
|
f.apply(this[name], arguments);
|
|
};
|
|
|
|
cls['__create__' + name] = fn_create;
|
|
|
|
var fn_delete = function () {
|
|
var f = this[name].destroyAll;
|
|
f.apply(this[name], arguments);
|
|
};
|
|
|
|
cls['__delete__' + name] = fn_delete;
|
|
|
|
var fn_count = function (cb) {
|
|
var f = this[name].count;
|
|
f.apply(this[name], arguments);
|
|
};
|
|
|
|
cls['__count__' + name] = fn_count;
|
|
|
|
/*
|
|
* Extracting fixed property values for the scope from the where clause into
|
|
* the data object
|
|
*
|
|
* @param {Object} The data object
|
|
* @param {Object} The where clause
|
|
*/
|
|
function setScopeValuesFromWhere(data, where) {
|
|
for (var i in where) {
|
|
if (i === 'and') {
|
|
// Find fixed property values from each subclauses
|
|
for (var w = 0, n = where[i].length; w < n; w++) {
|
|
setScopeValuesFromWhere(data, where[i][w]);
|
|
}
|
|
continue;
|
|
}
|
|
var prop = targetClass.definition.properties[i];
|
|
if (prop) {
|
|
var val = where[i];
|
|
if (typeof val !== 'object' || val instanceof prop.type
|
|
|| prop.type.name === 'ObjectID') // MongoDB key
|
|
{
|
|
// Only pick the {propertyName: propertyValue}
|
|
data[i] = where[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// and it should have create/build methods with binded thisModelNameId param
|
|
function build(data) {
|
|
data = data || {};
|
|
// Find all fixed property values for the scope
|
|
var where = (this._scope && this._scope.where) || {};
|
|
setScopeValuesFromWhere(data, where);
|
|
return new targetClass(data);
|
|
}
|
|
|
|
function create(data, cb) {
|
|
if (typeof data === 'function') {
|
|
cb = data;
|
|
data = {};
|
|
}
|
|
this.build(data).save(cb);
|
|
}
|
|
|
|
/*
|
|
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, cb) {
|
|
if (typeof where === 'function') cb = where, where = {};
|
|
var scoped = (this._scope && this._scope.where) || {};
|
|
var filter = mergeQuery({ where: scoped }, { where: where || {} });
|
|
targetClass.destroyAll(filter.where, cb);
|
|
}
|
|
|
|
function count(where, cb) {
|
|
if (typeof where === 'function') cb = where, where = {};
|
|
var scoped = (this._scope && this._scope.where) || {};
|
|
var filter = mergeQuery({ where: scoped }, { where: where || {} });
|
|
targetClass.count(filter.where, cb);
|
|
}
|
|
|
|
return definition;
|
|
}
|
|
|
|
/*!
|
|
* Merge query parameters
|
|
* @param {Object} base The base object to contain the merged results
|
|
* @param {Object} update The object containing updates to be merged
|
|
* @returns {*|Object} The base object
|
|
* @private
|
|
*/
|
|
function mergeQuery(base, update) {
|
|
if (!update) {
|
|
return;
|
|
}
|
|
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]};
|
|
} else {
|
|
base.where = update.where;
|
|
}
|
|
}
|
|
|
|
// Merge inclusion
|
|
if (update.include) {
|
|
if (!base.include) {
|
|
base.include = update.include;
|
|
} else {
|
|
var saved = base.include;
|
|
base.include = {};
|
|
base.include[update.include] = saved;
|
|
}
|
|
}
|
|
if (update.collect) {
|
|
base.collect = update.collect;
|
|
}
|
|
|
|
// set order
|
|
if (!base.order && update.order) {
|
|
base.order = update.order;
|
|
}
|
|
|
|
// overwrite pagination
|
|
if (update.limit !== undefined) {
|
|
base.limit = update.limit;
|
|
}
|
|
if (update.skip !== undefined) {
|
|
base.skip = update.skip;
|
|
}
|
|
if (update.offset !== undefined) {
|
|
base.offset = update.offset;
|
|
}
|
|
|
|
// Overwrite fields
|
|
if (update.fields !== undefined) {
|
|
base.fields = update.fields;
|
|
}
|
|
return base;
|
|
}
|