@themost/data
Version:
MOST Web Framework 2.0 - ORM module
1,430 lines (1,379 loc) • 113 kB
JavaScript
/**
* @license
* MOST Web Framework 2.0 Codename Blueshift
* Copyright (c) 2017, THEMOST LP All rights reserved
*
* Use of this source code is governed by an BSD-3-Clause license that can be
* found in the LICENSE file at https://themost.io/license
*/
///
var async = require('async');
var sprintf = require('sprintf').sprintf;
var Symbol = require('symbol');
var _ = require("lodash");
var TextUtils = require("@themost/common/utils").TextUtils;
var mappingExtensions = require('./data-mapping-extensions');
var DataAssociationMapping = require('./types').DataAssociationMapping;
var DataError = require("@themost/common/errors").DataError;
var QueryField = require('@themost/query/query').QueryField;
var QueryEntity = require('@themost/query/query').QueryEntity;
var QueryUtils = require('@themost/query/utils').QueryUtils;
var Q = require('q');
var aliasProperty = Symbol('alias');
/**
* @class
* @constructor
* @ignore
*/
function DataAttributeResolver() {
}
DataAttributeResolver.prototype.orderByNestedAttribute = function(attr) {
var nestedAttribute = DataAttributeResolver.prototype.testNestedAttribute(attr);
if (nestedAttribute) {
var matches = /^(\w+)\((\w+)\/(\w+)\)$/i.exec(nestedAttribute.name);
if (matches) {
return DataAttributeResolver.prototype.selectAggregatedAttribute.call(this, matches[1], matches[2] + "/" + matches[3]);
}
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\)$/i.exec(nestedAttribute.name);
if (matches) {
return DataAttributeResolver.prototype.selectAggregatedAttribute.call(this, matches[1], matches[2] + "/" + matches[3] + "/" + matches[4]);
}
}
return DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr);
};
DataAttributeResolver.prototype.selecteNestedAttribute = function(attr, alias) {
var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr);
if (expr) {
if (_.isNil(alias))
expr.as(attr.replace(/\//g,'_'));
else
expr.as(alias)
}
return expr;
};
/**
* @param {string} aggregation
* @param {string} attribute
* @param {string=} alias
* @returns {*}
*/
DataAttributeResolver.prototype.selectAggregatedAttribute = function(aggregation, attribute, alias) {
var self=this, result;
if (DataAttributeResolver.prototype.testNestedAttribute(attribute)) {
result = DataAttributeResolver.prototype.selecteNestedAttribute.call(self,attribute, alias);
}
else {
result = self.fieldOf(attribute);
}
var sAlias = result.as(), name = result.getName(), expr;
if (sAlias) {
expr = result[sAlias];
result[sAlias] = { };
result[sAlias]['$' + aggregation ] = expr;
}
else {
expr = result.$name;
result[name] = { };
result[name]['$' + aggregation ] = expr;
}
return result;
};
DataAttributeResolver.prototype.resolveNestedAttribute = function(attr) {
var self = this;
if (typeof attr === 'string' && /\//.test(attr)) {
var member = attr.split('/'), expr, arr, obj, select;
//change: 18-Feb 2016
//description: Support many to many (junction) resolving
var mapping = self.model.inferMapping(member[0]);
if (mapping && mapping.associationType === 'junction') {
var expr1 = DataAttributeResolver.prototype.resolveJunctionAttributeJoin.call(self.model, attr);
//select field
select = expr1.$select;
//get expand
expr = expr1.$expand;
}
else {
expr = DataAttributeResolver.prototype.resolveNestedAttributeJoin.call(self.model, attr);
//select field
if (member.length>2)
select = QueryField.select(member[member.length-1]).from(member[member.length-2]);
else
select = QueryField.select(member[1]).from(member[0]);
}
if (expr) {
if (_.isNil(self.query.$expand)) {
self.query.$expand = expr;
}
else {
arr = [];
if (!_.isArray(self.query.$expand)) {
arr.push(self.query.$expand);
this.query.$expand = arr;
}
arr = [];
if (_.isArray(expr))
arr.push.apply(arr, expr);
else
arr.push(expr);
arr.forEach(function(y) {
obj = self.query.$expand.find(function(x) {
if (x.$entity && x.$entity.$as) {
return (x.$entity.$as === y.$entity.$as);
}
return false;
});
if (typeof obj === 'undefined') {
self.query.$expand.push(y);
}
});
}
return select;
}
else {
throw new Error('Member join expression cannot be empty at this context');
}
}
};
/**
*
* @param {string} memberExpr - A string that represents a member expression e.g. user/id or article/published etc.
* @returns {*} - An object that represents a query join expression
*/
DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr) {
var self = this, childField, parentField, res, expr, entity;
if (/\//.test(memberExpr)) {
//if the specified member contains '/' e.g. user/name then prepare join
var arrMember = memberExpr.split('/');
var attrMember = self.field(arrMember[0]);
if (_.isNil(attrMember)) {
throw new Error(sprintf('The target model does not have an attribute named as %s',arrMember[0]));
}
//search for field mapping
var mapping = self.inferMapping(arrMember[0]);
if (_.isNil(mapping)) {
throw new Error(sprintf('The target model does not have an association defined for attribute named %s',arrMember[0]));
}
if (mapping.childModel===self.name && mapping.associationType==='association') {
//get parent model
var parentModel = self.context.model(mapping.parentModel);
if (_.isNil(parentModel)) {
throw new Error(sprintf('Association parent model (%s) cannot be found.', mapping.parentModel));
}
childField = self.field(mapping.childField);
if (_.isNil(childField)) {
throw new Error(sprintf('Association field (%s) cannot be found.', mapping.childField));
}
parentField = parentModel.field(mapping.parentField);
if (_.isNil(parentField)) {
throw new Error(sprintf('Referenced field (%s) cannot be found.', mapping.parentField));
}
/**
* store temp query expression
* @type QueryExpression
*/
res =QueryUtils.query(self.viewAdapter).select(['*']);
expr = QueryUtils.query().where(QueryField.select(childField.name).from(self[aliasProperty] || self.viewAdapter)).equal(QueryField.select(mapping.parentField).from(mapping.childField));
entity = new QueryEntity(parentModel.viewAdapter).as(mapping.childField).left();
res.join(entity).with(expr);
if (arrMember.length>2) {
parentModel[aliasProperty] = mapping.childField;
expr = DataAttributeResolver.prototype.resolveNestedAttributeJoin.call(parentModel, arrMember.slice(1).join('/'));
return [].concat(res.$expand).concat(expr);
}
//--set active field
return res.$expand;
}
else if (mapping.parentModel===self.name && mapping.associationType==='association') {
var childModel = self.context.model(mapping.childModel);
if (_.isNil(childModel)) {
throw new Error(sprintf('Association child model (%s) cannot be found.', mapping.childModel));
}
childField = childModel.field(mapping.childField);
if (_.isNil(childField)) {
throw new Error(sprintf('Association field (%s) cannot be found.', mapping.childField));
}
parentField = self.field(mapping.parentField);
if (_.isNil(parentField)) {
throw new Error(sprintf('Referenced field (%s) cannot be found.', mapping.parentField));
}
res =QueryUtils.query('Unknown').select(['*']);
expr = QueryUtils.query().where(QueryField.select(parentField.name).from(self[aliasProperty] || self.viewAdapter)).equal(QueryField.select(childField.name).from(arrMember[0]));
entity = new QueryEntity(childModel.viewAdapter).as(arrMember[0]).left();
res.join(entity).with(expr);
return res.$expand;
}
else {
throw new Error(sprintf('The association type between %s and %s model is not supported for filtering, grouping or sorting data.', mapping.parentModel , mapping.childModel));
}
}
};
/**
* @param {string} s
* @returns {*}
*/
DataAttributeResolver.prototype.testAttribute = function(s) {
if (typeof s !== 'string')
return;
/**
* @private
*/
var matches;
/**
* attribute aggregate function with alias e.g. f(x) as a
* @ignore
*/
matches = /^(\w+)\((\w+)\)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + ')' , property:matches[3] };
}
/**
* attribute aggregate function with alias e.g. x as a
* @ignore
*/
matches = /^(\w+)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] , property:matches[2] };
}
/**
* attribute aggregate function with alias e.g. f(x)
* @ignore
*/
matches = /^(\w+)\((\w+)\)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + ')' };
}
// only attribute e.g. x
if (/^(\w+)$/.test(s)) {
return { name: s};
}
};
/**
* @param {string} s
* @returns {*}
*/
DataAttributeResolver.prototype.testAggregatedNestedAttribute = function(s) {
if (typeof s !== 'string')
return;
/**
* @private
*/
var matches;
/**
* nested attribute aggregate function with alias e.g. f(x/b) as a
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { aggr: matches[1], name: matches[2] + '/' + matches[3], property:matches[4] };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b/c) as a
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { aggr: matches[1], name: matches[2] + '/' + matches[3] + '/' + matches[4], property:matches[5] };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b)
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\)$/i.exec(s);
if (matches) {
return { aggr: matches[1], name: matches[2] + '/' + matches[3] };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b/c)
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\)$/i.exec(s);
if (matches) {
return { aggr: matches[1], name: matches[2] + '/' + matches[3] + '/' + matches[4] };
}
};
/**
* @param {string} s
* @returns {*}
*/
DataAttributeResolver.prototype.testNestedAttribute = function(s) {
if (typeof s !== 'string')
return;
/**
* @private
*/
var matches;
/**
* nested attribute aggregate function with alias e.g. f(x/b) as a
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + ')', property:matches[4] };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b/c) as a
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + '/' + matches[4] + ')', property:matches[5] };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b/c/d) as a
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\/(\w+)\)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + '/' + matches[4] + '/' + matches[5] + ')', property:matches[6] };
}
/**
* nested attribute with alias e.g. x/b as a
* @ignore
*/
matches = /^(\w+)\/(\w+)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '/' + matches[2], property:matches[3] };
}
/**
* nested attribute with alias e.g. x/b/c as a
* @ignore
*/
matches = /^(\w+)\/(\w+)\/(\w+)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '/' + matches[2] + '/' + matches[3], property:matches[4] };
}
/**
* nested attribute with alias e.g. x/b/c/d as a
* @ignore
*/
matches = /^(\w+)\/(\w+)\/(\w+)\/(\w+)\sas\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '/' + matches[2] + '/' + matches[3] + '/' + matches[4], property:matches[5] };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b)
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + ')' };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b/c)
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + '/' + matches[4] + ')' };
}
/**
* nested attribute aggregate function with alias e.g. f(x/b/c/d)
* @ignore
*/
matches = /^(\w+)\((\w+)\/(\w+)\/(\w+)\/(\w+)\)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + '/' + matches[4] + matches[5] + ')' };
}
/**
* nested attribute with alias e.g. x/b
* @ignore
*/
matches = /^(\w+)\/(\w+)$/.exec(s);
if (matches) {
return { name: s };
}
/**
* nested attribute with alias e.g. x/b/c
* @ignore
*/
matches = /^(\w+)\/(\w+)\/(\w+)$/.exec(s);
if (matches) {
return { name: s };
}
/**
* nested attribute with alias e.g. x/b/c/d
* @ignore
*/
matches = /^(\w+)\/(\w+)\/(\w+)\/(\w+)$/.exec(s);
if (matches) {
return { name: s };
}
};
/**
* @param {string} attr
* @returns {*}
*/
DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) {
var self = this, member = attr.split("/");
//get the data association mapping
var mapping = self.inferMapping(member[0]);
//if mapping defines a junction between two models
if (mapping && mapping.associationType === "junction") {
//get field
var field = self.field(member[0]), entity, expr, q;
//first approach (default association adapter)
//the underlying model is the parent model e.g. Group > Group Members
if (mapping.parentModel === self.name) {
q =QueryUtils.query(self.viewAdapter).select(['*']);
//init an entity based on association adapter (e.g. GroupMembers as members)
entity = new QueryEntity(mapping.associationAdapter).as(field.name);
//init join expression between association adapter and current data model
//e.g. Group.id = GroupMembers.parent
expr = QueryUtils.query().where(QueryField.select(mapping.parentField).from(self.viewAdapter))
.equal(QueryField.select(mapping.associationObjectField).from(field.name));
//append join
q.join(entity).with(expr);
//data object tagging
if (typeof mapping.childModel === 'undefined') {
return {
$expand:[q.$expand],
$select:QueryField.select(mapping.associationValueField).from(field.name)
}
}
//return the resolved attribute for futher processing e.g. members.id
if (member[1] === mapping.childField) {
return {
$expand:[q.$expand],
$select:QueryField.select(mapping.associationValueField).from(field.name)
}
}
else {
//get child model
var childModel = self.context.model(mapping.childModel);
if (_.isNil(childModel)) {
throw new DataError("EJUNC","The associated model cannot be found.");
}
//create new join
var alias = field.name + "_" + childModel.name;
entity = new QueryEntity(childModel.viewAdapter).as(alias);
expr = QueryUtils.query().where(QueryField.select(mapping.associationValueField).from(field.name))
.equal(QueryField.select(mapping.childField).from(alias));
//append join
q.join(entity).with(expr);
return {
$expand:q.$expand,
$select:QueryField.select(member[1]).from(alias)
}
}
}
else {
q =QueryUtils.query(self.viewAdapter).select(['*']);
//the underlying model is the child model
//init an entity based on association adapter (e.g. GroupMembers as groups)
entity = new QueryEntity(mapping.associationAdapter).as(field.name);
//init join expression between association adapter and current data model
//e.g. Group.id = GroupMembers.parent
expr = QueryUtils.query().where(QueryField.select(mapping.childField).from(self.viewAdapter))
.equal(QueryField.select(mapping.associationValueField).from(field.name));
//append join
q.join(entity).with(expr);
//return the resolved attribute for further processing e.g. members.id
if (member[1] === mapping.parentField) {
return {
$expand:[q.$expand],
$select:QueryField.select(mapping.associationObjectField).from(field.name)
}
}
else {
//get parent model
var parentModel = self.context.model(mapping.parentModel);
if (_.isNil(parentModel)) {
throw new DataError("EJUNC","The associated model cannot be found.");
}
//create new join
var parentAlias = field.name + "_" + parentModel.name;
entity = new QueryEntity(parentModel.viewAdapter).as(parentAlias);
expr = QueryUtils.query().where(QueryField.select(mapping.associationObjectField).from(field.name))
.equal(QueryField.select(mapping.parentField).from(parentAlias));
//append join
q.join(entity).with(expr);
return {
$expand:q.$expand,
$select:QueryField.select(member[1]).from(parentAlias)
}
}
}
}
else {
throw new DataError("EJUNC","The target model does not have a many to many association defined by the given attribute.","", self.name, attr);
}
};
/**
* @classdesc Represents a dynamic query helper for filtering, paging, grouping and sorting data associated with an instance of DataModel class.
* @class
* @property {QueryExpression|*} query - Gets or sets the current query expression
* @property {DataModel|*} model - Gets or sets the underlying data model
* @constructor
* @param model {DataModel|*}
* @augments DataContextEmitter
*/
function DataQueryable(model) {
/**
* @type {QueryExpression}
* @private
*/
var q = null;
/**
* Gets or sets an array of expandable models
* @type {Array}
* @private
*/
this.$expand = undefined;
/**
* @type {Boolean}
* @private
*/
this.$flatten = undefined;
/**
* @type {DataModel}
* @private
*/
var m = model;
Object.defineProperty(this, 'query', { get: function() {
if (!q) {
if (!m) {
return null;
}
q = QueryUtils.query(m.viewAdapter);
}
return q;
}, configurable:false, enumerable:false});
Object.defineProperty(this, 'model', { get: function() {
return m;
}, configurable:false, enumerable:false});
//get silent property
if (m)
this.silent(m.$silent);
}
/**
* Clones the current DataQueryable instance.
* @returns {DataQueryable|*} - The cloned object.
*/
DataQueryable.prototype.clone = function() {
var result = new DataQueryable(this.model);
//set view if any
result.$view = this.$view;
//set silent property
result.$silent = this.$silent;
//set silent property
result.$levels = this.$levels;
//set flatten property
result.$flatten = this.$flatten;
//set expand property
result.$expand = this.$expand;
//set query
_.assign(result.query, this.query);
return result;
};
/**
* Ensures data queryable context and returns the current data context. This function may be overriden.
* @returns {DataContext}
* @ignore
*/
DataQueryable.prototype.ensureContext = function() {
if (this.model!==null)
if (this.model.context!==null)
return this.model.context;
return null;
};
/**
* Serializes the underlying query and clears current filter expression for further filter processing. This operation may be used in complex filtering.
* @param {Boolean=} useOr - Indicates whether an or statement will be used in the resulted statement.
* @returns {DataQueryable}
* @example
//retrieve a list of order
context.model('Order')
.where('orderStatus').equal(1).and('paymentMethod').equal(2)
.prepare().where('orderStatus').equal(2).and('paymentMethod').equal(2)
.prepare(true)
//(((OrderData.orderStatus=1) AND (OrderData.paymentMethod=2)) OR ((OrderData.orderStatus=2) AND (OrderData.paymentMethod=2)))
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.prepare = function(useOr) {
this.query.prepare(useOr);
return this;
};
/**
* Initializes a where expression
* @param attr {string} - A string which represents the field name that is going to be used as the left operand of this expression
* @returns {DataQueryable}
* @example
context.model('Person')
.where('user/name').equal('user1@exampl.com')
.select('description')
.first().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.where = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr));
return this;
}
this.query.where(this.fieldOf(attr));
return this;
};
/**
* Initializes a full-text search expression
* @param {string} text - A string which represents the text we want to search for
* @returns {DataQueryable}
* @example
context.model('Person')
.search('Peter')
.select('description')
.take(25).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.search = function(text) {
var self = this;
// eslint-disable-next-line no-unused-vars
var options = { multiword:true };
var terms = [];
if (typeof text !== 'string') { return self; }
var re = /("(.*?)")|([^\s]+)/g;
var match = re.exec(text);
while(match) {
if (match[2]) {
terms.push(match[2]);
}
else {
terms.push(match[0]);
}
match = re.exec(text);
}
if (terms.length===0) {
return self;
}
self.prepare();
var stringTypes = [ "Text", "URL", "Note" ];
self.model.attributes.forEach(function(x) {
if (x.many) { return; }
var mapping = self.model.inferMapping(x.name);
if (mapping) {
if ((mapping.associationType === 'association') && (mapping.childModel===self.model.name)) {
var parentModel = self.model.context.model(mapping.parentModel);
if (parentModel) {
parentModel.attributes.forEach(function(z) {
if (stringTypes.indexOf(z.type)>=0) {
terms.forEach(function (w) {
if (!/^\s+$/.test(w))
self.or(x.name + '/' + z.name).contains(w);
});
}
});
}
}
}
if (stringTypes.indexOf(x.type)>=0) {
terms.forEach(function (y) {
if (!/^\s+$/.test(y))
self.or(x.name).contains(y);
});
}
});
self.prepare();
return self;
};
DataQueryable.prototype.join = function(model)
{
var self = this;
if (_.isNil(model))
return this;
/**
* @type {DataModel}
*/
var joinModel = self.model.context.model(model);
//validate joined model
if (_.isNil(joinModel))
throw new Error(sprintf("The %s model cannot be found", model));
var arr = self.model.attributes.filter(function(x) { return x.type===joinModel.name; });
if (arr.length===0)
throw new Error(sprintf("An internal error occurred. The association between %s and %s cannot be found", this.model.name ,model));
var mapping = self.model.inferMapping(arr[0].name);
var expr = QueryUtils.query();
expr.where(self.fieldOf(mapping.childField)).equal(joinModel.fieldOf(mapping.parentField));
/**
* @type DataAssociationMapping
*/
var entity = new QueryEntity(joinModel.viewAdapter).left();
//set join entity (without alias and join type)
self.select().query.join(entity).with(expr);
return self;
};
/**
* Prepares a logical AND expression
* @param attr {string} - The name of field that is going to be used in this expression
* @returns {DataQueryable}
* @example
context.model('Order').where('customer').equal(298)
.and('orderStatus').equal(1)
.list().then(function(result) {
//SQL: WHERE ((OrderData.customer=298) AND (OrderData.orderStatus=1)
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.and = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.and(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr));
return this;
}
this.query.and(this.fieldOf(attr));
return this;
};
/**
* Prepares a logical OR expression
* @param attr {string} - The name of field that is going to be used in this expression
* @returns {DataQueryable}
* @example
//((OrderData.orderStatus=1) OR (OrderData.orderStatus=2)
context.model('Order').where('orderStatus').equal(1)
.or('orderStatus').equal(2)
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.or = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.or(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr));
return this;
}
this.query.or(this.fieldOf(attr));
return this;
};
/**
* @private
* @memberof DataQueryable#
* @param {*} obj
* @returns {*}
*/
function resolveValue(obj) {
var self = this;
if (typeof obj === 'string' && /^\$it\//.test(obj)) {
var attr = obj.replace(/^\$it\//,'');
if (DataAttributeResolver.prototype.testNestedAttribute(attr)) {
return DataAttributeResolver.prototype.resolveNestedAttribute.call(self, attr);
}
else {
attr = DataAttributeResolver.prototype.testAttribute(attr);
if (attr) {
return self.fieldOf(attr.name);
}
}
}
return obj;
}
/**
* Performs an equality comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders with order status equal to 1
context.model('Order').where('orderStatus').equal(1)
.list().then(function(result) {
//WHERE (OrderData.orderStatus=1)
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.equal = function(obj) {
this.query.equal(resolveValue.bind(this)(obj));
return this;
};
/**
* Performs an equality comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a person with id equal to 299
context.model('Person').where('id').is(299)
.first().then(function(result) {
//WHERE (PersonData.id=299)
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.is = function(obj) {
return this.equal(obj);
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares a not equal comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders with order status different than 1
context.model('Order')
.where('orderStatus').notEqual(1)
.orderByDescending('orderDate')
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.notEqual = function(obj) {
this.query.notEqual(resolveValue.bind(this)(obj));
return this;
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares a greater than comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders where product price is greater than 800
context.model('Order')
.where('orderedItem/price').greaterThan(800)
.orderByDescending('orderDate')
.select('id','orderedItem/name as productName', 'orderedItem/price as productPrice', 'orderDate')
.take(5)
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
id productName productPrice orderDate
--- -------------------------------------------- ------------ -----------------------------
304 Apple iMac (27-Inch, 2013 Version) 1336.27 2015-11-27 23:49:17.000+02:00
322 Dell B1163w Mono Laser Multifunction Printer 842.86 2015-11-27 20:16:52.000+02:00
167 Razer Blade (2013) 1553.43 2015-11-27 04:17:08.000+02:00
336 Apple iMac (27-Inch, 2013 Version) 1336.27 2015-11-26 07:25:35.000+02:00
89 Nvidia GeForce GTX 650 Ti Boost 1625.49 2015-11-21 17:29:21.000+02:00
*/
DataQueryable.prototype.greaterThan = function(obj) {
this.query.greaterThan(resolveValue.bind(this)(obj));
return this;
};
/**
* Prepares a greater than or equal comparison.
* @param obj {*} The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders where product price is greater than or equal to 800
context.model('Order')
.where('orderedItem/price').greaterOrEqual(800)
.orderByDescending('orderDate')
.take(5)
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.greaterOrEqual = function(obj) {
this.query.greaterOrEqual(resolveValue.bind(this)(obj));
return this;
};
/**
* Prepares a bitwise and comparison.
* @param {*} value - The right operand of the express
* @param {Number=} result - The result of a bitwise and expression
* @returns {DataQueryable}
* @example
//retrieve a list of permissions for model Person and insert permission mask (2)
context.model('Permission')
//prepare bitwise AND (((PermissionData.mask & 2)=2)
.where('mask').bit(2)
.and('privilege').equal('Person')
.and('parentPrivilege').equal(null)
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*
*/
DataQueryable.prototype.bit = function(value, result) {
if (_.isNil(result))
this.query.bit(value, value);
else
this.query.bit(value, result);
return this;
};
/**
* Prepares a lower than comparison
* @param obj {*}
* @returns {DataQueryable}
*/
DataQueryable.prototype.lowerThan = function(obj) {
this.query.lowerThan(resolveValue.bind(this)(obj));
return this;
};
/**
* Prepares a lower than or equal comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve orders based on payment due date
context.model('Order')
.orderBy('paymentDue')
.where('paymentDue').lowerOrEqual(moment().subtract('days',-7).toDate())
.and('paymentDue').greaterThan(new Date())
.take(10).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.lowerOrEqual = function(obj) {
this.query.lowerOrEqual(resolveValue.bind(this)(obj));
return this;
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares an ends with comparison
* @param obj {*} - The string to be searched for at the end of a field.
* @returns {DataQueryable}
* @example
//retrieve people whose given name starts with 'D'
context.model('Person')
.where('givenName').startsWith('D')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
id givenName familyName
--- --------- ----------
257 Daisy Lambert
275 Dustin Brooks
333 Dakota Gallagher
*/
DataQueryable.prototype.startsWith = function(obj) {
this.query.startsWith(obj);
return this;
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares an ends with comparison
* @param obj {*} - The string to be searched for at the end of a field.
* @returns {DataQueryable}
* @example
//retrieve people whose given name ends with 'y'
context.model('Person')
.where('givenName').endsWith('y')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results
id givenName familyName
--- --------- ----------
257 Daisy Lambert
287 Zachary Field
295 Anthony Berry
339 Brittney Hunt
341 Kimberly Wheeler
*/
DataQueryable.prototype.endsWith = function(obj) {
this.query.endsWith(obj);
return this;
};
/**
* Prepares a typical IN comparison.
* @param objs {Array} - An array of values which represents the values to be used in expression
* @returns {DataQueryable}
* @example
//retrieve orders with order status 1 or 2
context.model('Order').where('orderStatus').in([1,2])
.list().then(function(result) {
//WHERE (OrderData.orderStatus IN (1, 2))
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.in = function(objs) {
this.query.in(objs);
return this;
};
/**
* Prepares a typical NOT IN comparison.
* @param objs {Array} - An array of values which represents the values to be used in expression
* @returns {DataQueryable}
* @example
//retrieve orders with order status 1 or 2
context.model('Order').where('orderStatus').notIn([1,2])
.list().then(function(result) {
//WHERE (NOT OrderData.orderStatus IN (1, 2))
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.notIn = function(objs) {
this.query.notIn(objs);
return this;
};
/**
* Prepares a modular arithmetic operation
* @param {*} obj The value to be compared
* @param {Number} result The result of modular expression
* @returns {DataQueryable}
*/
DataQueryable.prototype.mod = function(obj, result) {
this.query.mod(obj, result);
return this;
};
/**
* Prepares a contains comparison (e.g. a string contains another string).
* @param value {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve person where the given name contains
context.model('Person').select(['id','givenName','familyName'])
.where('givenName').contains('ex')
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id givenName familyName
--- --------- ----------
297 Alex Miles
353 Alexis Rees
*/
DataQueryable.prototype.contains = function(value) {
this.query.contains(value);
return this;
};
/**
* Prepares a not contains comparison (e.g. a string contains another string).
* @param value {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve persons where the given name not contains 'ar'
context.model('Person').select(['id','givenName','familyName'])
.where('givenName').notContains('ar')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id givenName familyName
--- --------- ----------
257 Daisy Lambert
259 Peter French
261 Kylie Jordan
263 Maxwell Hall
265 Christian Marshall
*/
DataQueryable.prototype.notContains = function(value) {
this.query.notContains(value);
return this;
};
/**
* Prepares a comparison where the left operand is between two values
* @param {*} value1 - The minimum value
* @param {*} value2 - The maximum value
* @returns {DataQueryable}
* @example
//retrieve products where price is between 150 and 250
context.model('Product')
.where('price').between(150,250)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id name model price
--- ------------------------------------------ ------ ------
367 Asus Transformer Book T100 HD2895 224.52
380 Zotac Zbox Nano XS AD13 Plus WC5547 228.05
384 Apple iPad Air ZE6015 177.44
401 Intel Core i7-4960X Extreme Edition SM5853 194.61
440 Bose SoundLink Bluetooth Mobile Speaker II HS5288 155.27
*/
DataQueryable.prototype.between = function(value1, value2) {
this.query.between(resolveValue.bind(this)(value1), resolveValue.bind(this)(value2));
return this;
};
/**
* @memberOf DataQueryable#
* @param arg
* @returns {*}
* @private
*/
function select_(arg) {
var self = this;
if (typeof arg === 'string' && arg.length===0) {
return;
}
var a = DataAttributeResolver.prototype.testAggregatedNestedAttribute.call(self,arg);
if (a) {
return DataAttributeResolver.prototype.selectAggregatedAttribute.call(self, a.aggr , a.name, a.property);
}
else {
a = DataAttributeResolver.prototype.testNestedAttribute.call(self,arg);
if (a) {
return DataAttributeResolver.prototype.selecteNestedAttribute.call(self, a.name, a.property);
}
else {
a = DataAttributeResolver.prototype.testAttribute.call(self,arg);
if (a) {
return self.fieldOf(a.name, a.property);
}
else {
return self.fieldOf(arg);
}
}
}
}
/**
* Selects a field or a collection of fields of the current model.
* @param {...string} attr An array of fields, a field or a view name
* @returns {DataQueryable}
* @example
//retrieve the last 5 orders
context.model('Order').select('id','customer','orderDate','orderedItem')
.orderBy('orderDate')
.take(5).list().then(function(result) {
console.table(result.records);
done(null, result);
}).catch(function(err) {
done(err);
});
* @example
//retrieve the last 5 orders by getting the associated customer name and product name
context.model('Order').select('id','customer/description as customerName','orderDate','orderedItem/name as productName')
.orderBy('orderDate')
.take(5).list().then(function(result) {
console.table(result.records);
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id customerName orderDate orderedItemName
--- ------------------- ----------------------------- ----------------------------------------------------
46 Nicole Armstrong 2014-12-31 13:35:41.000+02:00 LaCie Blade Runner
288 Cheyenne Hudson 2015-01-01 13:24:21.000+02:00 Canon Pixma MG5420 Wireless Photo All-in-One Printer
139 Christian Whitehead 2015-01-01 23:21:24.000+02:00 Olympus OM-D E-M1
3 Katelyn Kelly 2015-01-02 04:42:58.000+02:00 Kobo Aura
59 Cheyenne Hudson 2015-01-02 10:47:53.000+02:00 Google Nexus 7 (2013)
@example
//retrieve the best customers by getting the associated customer name and a count of orders made by the customer
context.model('Order').select('customer/description as customerName','count(id) as orderCount')
.orderBy('count(id)')
.groupBy('customer/description')
.take(3).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
customerName orderCount
---------------- ----------
Miranda Bird 19
Alex Miles 16
Isaiah Morton 16
*/
DataQueryable.prototype.select = function(attr) {
var self = this, arr, expr,
arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr;
if (typeof arg === 'string') {
if (arg==="*") {
//delete select
delete self.query.$select;
return this;
}
//validate field or model view
var field = self.model.field(arg);
if (field) {
//validate field
if (field.many || (field.mapping && field.mapping.associationType === 'junction')) {
self.expand(field.name);
}
else {
arr = [];
arr.push(self.fieldOf(field.name));
}
}
else {
//get data view
self.$view = self.model.dataviews(arg);
//if data view was found
if (self.$view) {
arr = [];
var name;
self.$view.fields.forEach(function(x) {
name = x.name;
field = self.model.field(name);
//if a field with the given name exists in target model
if (field) {
//check if this field has an association mapping
if (field.many || (field.mapping && field.mapping.associationType === 'junction'))
self.expand(field.name);
else
arr.push(self.fieldOf(field.name));
}
else {
var b = DataAttributeResolver.prototype.testAggregatedNestedAttribute.call(self,name);
if (b) {
expr = DataAttributeResolver.prototype.selectAggregatedAttribute.call(self, b.aggr , b.name);
if (expr) { arr.push(expr); }
}
else {
b = DataAttributeResolver.prototype.testNestedAttribute.call(self,name);
if (b) {
expr = DataAttributeResolver.prototype.selecteNestedAttribute.call(self, b.name, x.property);
if (expr) { arr.push(expr); }
}
else {
b = DataAttributeResolver.prototype.testAttribute.call(self,name);
if (b) {
arr.push(self.fieldOf(b.name, x.property));
}
else if (/\./g.test(name)) {
name = name.split('.')[0];
arr.push(self.fieldOf(name));
}
else
{
arr.push(self.fieldOf(name));
}
}
}
}
});
}
//select a field from a joined entity
else {
expr = select_.call(self, arg);
if (expr) {
arr = arr || [];
arr.push(expr);
}
}
}
if (_.isArray(arr)) {
if (arr.length===0)
arr = null;
}
}
else {
//get array of attributes
if (_.isArray(arg)) {
arr = [];
//check if field is a data view
if (arg.length === 1 && typeof arg[0] === 'string') {
if (self.model.dataviews(arg[0])) {
return self.select(arg[0]);
}
}
arg.forEach(function(x) {
if (typeof x === 'string') {
field = self.model.field(x);
if (field) {
if (field.many || (field.mapping && field.mapping.associationType === 'junction')) {
self.expand({
"name":field.name,
"options":field.options
});
}
else {
arr.push(self.fieldOf(field.name));
}
}
//test nested attribute and simple attribute expression
else {
expr = select_.call(self, x);
if (expr) {
arr = arr || [];
arr.push(expr);
}
}
}
else {
//validate if x is an object (QueryField)
arr.push(x);
}
});
}
}
if (_.isNil(arr)) {
if (!self.query.hasFields()) {
// //enumerate fields
var fields = self.model.attributes.filter(function (x) {
return (x.many === false) || (_.isNil(x.many)) || ((x.expandable === true) && (self.getLevels()>0));
}).map(function(x) {
return x.property || x.name;
});
if (fields.length === 0) {
return this;
}
return self.select.apply(self, fields);
}
}
else {
self.query.select(arr);
}
return this;
};
// noinspection JSUnusedGlobalSymbols
DataQueryable.prototype.dateOf = function(attr) {
if (typeof attr ==='undefined' || attr === null)
return attr;
if (typeof attr !=='string')
return attr;
return this.fieldOf('date(' + attr + ')');
};
/**
* @param attr {string|*}
* @param alias {string=}
* @returns {DataQueryable|QueryField|*}
*/
DataQueryable.prototype.fieldOf = function(attr, alias) {
if (typeof attr ==='undefined' || attr === null)
return attr;
if (typeof attr !=='string')
return attr;
var matches = /(count|avg|sum|min|max)\((.*?)\)/i.exec(attr), res, field, aggr, prop;
if (matches) {
//get field
field = this.model.field(matches[2]);
//get aggregate function
aggr = matches[1].toLowerCase();
//test nested attribute aggregation
if (_.isNil(field) && /\//.test(matches[2])) {
//resolve nested attribute
var nestedAttr = DataAttributeResolver.prototype.resolveNestedAttribute.call(this, matches[2]);
//if nested attribute exists
if (nestedAttr) {
if (_.isNil(alias)) {
var nestedMatches = /as\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(attr);
alias = _.isNil(nestedMatches) ? aggr.concat('Of_', matches[2].replace(/\//g, "_")) : nestedMatches[1];
}
/**
* @type {Function}
*/
var fn = QueryField[aggr];
//return query field
return fn(nestedAttr.$name).as(alias);
}
}
if (typeof field === 'undefined' || field === null)
throw new Error(sprintf('The specified field %s cannot be found in target model.', matches[2]));
if (_.isNil(alias)) {
matches = /as\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.