/**
* MOST Web Framework
* A JavaScript Web Framework
* http://themost.io
* Created by Kyriakos Barbounakis<k.barbounakis@gmail.com> on 2014-10-13.
*
* Copyright (c) 2014, Kyriakos Barbounakis k.barbounakis@gmail.com
Anthi Oikonomou anthioikonomou@gmail.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MOST Web Framework nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @ignore
*/
var async=require('async'),
util = require('util'),
_ = require("lodash"),
dataCommon = require('./data-common'),
types = require('./types'),
qry = require('most-query'),
Q = require('q');
/**
* @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.name(), 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 = qry.fields.select(member[2]).from(member[1]);
else
select = qry.fields.select(member[1]).from(member[0]);
}
if (expr) {
if (typeof this.query.$expand === 'undefined' || null) {
this.query.$expand = expr;
}
else {
arr = [];
if (!util.isArray(self.query.$expand)) {
arr.push(self.query.$expand);
this.query.$expand = arr;
}
arr = [];
if (util.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;
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(util.format('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(util.format('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(util.format('Association parent model (%s) cannot be found.', mapping.parentModel));
}
/**
* store temp query expression
* @type QueryExpression
*/
var res =qry.query(self.viewAdapter).select(['*']);
var expr = qry.query().where(qry.fields.select(mapping.childField).from(self._alias || self.viewAdapter)).equal(qry.fields.select(mapping.parentField).from(mapping.childField));
var entity = qry.entity(parentModel.viewAdapter).as(mapping.childField).left();
res.join(entity).with(expr);
if (arrMember.length>2) {
parentModel._alias = mapping.childField;
var expr = DataAttributeResolver.prototype.resolveNestedAttributeJoin.call(parentModel, arrMember[1] + '/' + arrMember[2]);
var arr = [];
arr.push(res.$expand);
arr.push(expr);
return arr;
}
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(util.format('Association child model (%s) cannot be found.', mapping.childModel));
}
var res =qry.query('Unknown').select(['*']);
var expr = qry.query().where(qry.fields.select(mapping.parentField).from(self.viewAdapter)).equal(qry.fields.select(mapping.childField).from(arrMember[0]));
var entity = qry.entity(childModel.viewAdapter).as(arrMember[0]).left();
res.join(entity).with(expr);
return res.$expand;
}
else {
throw new Error(util.format('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(\w+)$/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(\w+)$/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(\w+)$/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(\w+)$/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(\w+)$/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(\w+)$/i.exec(s);
if (matches) {
return { name: matches[1] + '(' + matches[2] + '/' + matches[3] + '/' + matches[4] + ')', property:matches[5] };
}
/**
* nested attribute with alias e.g. x/b as a
* @ignore
*/
matches = /^(\w+)\/(\w+)\sas\s(\w+)$/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(\w+)$/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)
* @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 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 };
}
};
/**
* @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) {
var parentField = "parentId", valueField = "valueId";
if (typeof mapping.childModel === 'undefined') {
parentField = "object"; valueField = "value"
}
q =qry.query(self.viewAdapter).select(['*']);
//init an entity based on association adapter (e.g. GroupMembers as members)
entity = qry.entity(mapping.associationAdapter).as(field.name);
//init join expression between association adapter and current data model
//e.g. Group.id = GroupMembers.parentId
expr = qry.query().where(qry.fields.select(mapping.parentField).from(self.viewAdapter))
.equal(qry.fields.select(parentField).from(field.name));
//append join
q.join(entity).with(expr);
//data object tagging
if (typeof mapping.childModel === 'undefined') {
return {
$expand:[q.$expand],
$select:qry.fields.select(valueField).from(field.name)
}
}
//return the resolved attribute for futher processing e.g. members.id
if (member[1] === mapping.childField) {
return {
$expand:[q.$expand],
$select:qry.fields.select(valueField).from(field.name)
}
}
else {
//get child model
var childModel = self.context.model(mapping.childModel);
if (_.isNil(childModel)) {
throw new types.DataException("EJUNC","The associated model cannot be found.");
}
//create new join
var alias = field.name + "_" + childModel.name;
entity = qry.entity(childModel.viewAdapter).as(alias);
expr = qry.query().where(qry.fields.select("valueId").from(field.name))
.equal(qry.fields.select(mapping.childField).from(alias));
//append join
q.join(entity).with(expr);
return {
$expand:q.$expand,
$select:qry.fields.select(member[1]).from(alias)
}
}
}
else {
q =qry.query(self.viewAdapter).select(['*']);
//the underlying model is the child model
//init an entity based on association adapter (e.g. GroupMembers as groups)
entity = qry.entity(mapping.associationAdapter).as(field.name);
//init join expression between association adapter and current data model
//e.g. Group.id = GroupMembers.parentId
expr = qry.query().where(qry.fields.select(mapping.childField).from(self.viewAdapter))
.equal(qry.fields.select("valueId").from(field.name));
//append join
q.join(entity).with(expr);
//return the resolved attribute for futher proccesing e.g. members.id
if (member[1] === mapping.parentField) {
return {
$expand:[q.$expand],
$select:qry.fields.select("parentId").from(field.name)
}
}
else {
//get parent model
var parentModel = self.context.model(mapping.parentModel);
if (_.isNil(parentModel)) {
throw new types.DataException("EJUNC","The associated model cannot be found.");
}
//create new join
var parentAlias = field.name + "_" + parentModel.name;
entity = qry.entity(parentModel.viewAdapter).as(parentAlias);
expr = qry.query().where(qry.fields.select("parentId").from(field.name))
.equal(qry.fields.select(mapping.parentField).from(parentAlias));
//append join
q.join(entity).with(expr);
return {
$expand:q.$expand,
$select:qry.fields.select(member[1]).from(parentAlias)
}
}
}
}
else {
throw new types.DataException("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 = qry.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 {DataQuerable|*} - 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
util._extend(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;
var options = { multiword:true };
var terms = [];
if (typeof text !== 'string') { return self; }
var re = /("(.*?)")|(\w+)/g;
var match;
while(match = re.exec(text)) {
if (match[2]) {
terms.push(match[2]);
}
else {
terms.push(match[0]);
}
}
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(util.format("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(util.format("An internal error occured. The association between %s and %s cannot be found", this.model.name ,model));
var mapping = self.model.inferMapping(arr[0].name);
var expr = qry.query();
expr.where(self.fieldOf(mapping.childField)).equal(joinModel.fieldOf(mapping.parentField));
/**
* @type DataAssociationMapping
*/
var entity = qry.entity(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;
};
/**
* 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(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);
};
/**
* 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(obj);
return this;
};
/**
* 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(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(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(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(obj);
return this;
};
/**
* 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;
};
/**
* 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(value1, value2);
return this;
};
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 (util.isArray(arr)) {
if (arr.length==0)
arr = null;
}
}
else {
//get array of attributes
if (util.isArray(arg)) {
arr = [];
//check if field is a model dataview
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(field.name);
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 || (x.mapping && x.mapping.associationType === 'junction'));
}).map(function(x) {
var f = qry.fields.select(x.name).from(self.model.viewAdapter);
if (x.property)
f.as(x.property);
return f;
});
//and select fields
self.select(fields);
}
}
else {
self.query.select(arr);
}
return this;
};
/**
* Adds a field or an array of fields to select statement
* @param {String|Array|DataField|*} attr
* @return {DataQueryable}
* @deprecated
*/
DataQueryable.prototype.alsoSelect = function(attr) {
var self = this;
if (!self.query.hasFields()) {
return self.select(attr);
}
else {
if (_.isNil(attr))
return self;
var arr = [];
if (typeof attr === 'string') {
arr.push(attr);
}
else if (util.isArray(attr)) {
arr = attr.slice(0);
}
else if (typeof attr === 'object') {
arr.push(attr);
}
var $select = self.query.$select;
arr.forEach(function(x) {
var field = self.fieldOf(x);
if (util.isArray($select[self.model.viewAdapter]))
$select[self.model.viewAdapter].push(field);
});
return self;
}
};
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(\w+)$/i.exec(attr);
alias = _.isNil(nestedMatches) ? aggr.concat('Of_', matches[2].replace(/\//g, "_")) : nestedMatches[1];
}
/**
* @type {Function}
*/
var fn = qry.fields[aggr];
//return query field
return fn(nestedAttr.$name).as(alias);
}
}
if (typeof field === 'undefined' || field === null)
throw new Error(util.format('The specified field %s cannot be found in target model.', matches[2]));
if (_.isNil(alias)) {
matches = /as\s(\w+)$/i.exec(attr);
if (matches) {
alias = matches[1];
}
else {
alias = aggr.concat('Of', field.name);
}
}
if (aggr=='count')
return qry.fields.count(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr=='avg')
return qry.fields.average(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr=='sum')
return qry.fields.sum(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr=='min')
return qry.fields.min(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr=='max')
return qry.fields.max(field.name).from(this.model.viewAdapter).as(alias);
}
else {
matches = /(\w+)\((.*?)\)/i.exec(attr);
if (matches) {
res = { };
field = this.model.field(matches[2]);
aggr = matches[1];
if (typeof field === 'undefined' || field === null)
throw new Error(util.format('The specified field %s cannot be found in target model.', matches[2]));
if (_.isNil(alias)) {
matches = /as\s(\w+)$/i.exec(attr);
if (matches) {
alias = matches[1];
}
}
prop = alias || field.property || field.name;
res[prop] = { }; res[prop]['$' + aggr] = [ qry.fields.select(field.name).from(this.model.viewAdapter) ];
return res;
}
else {
//matche expression [field] as [alias] e.g. itemType as type
matches = /^(\w+)\s+as\s+(.*?)$/i.exec(attr);
if (matches) {
field = this.model.field(matches[1]);
if (typeof field === 'undefined' || field === null)
throw new Error(util.format('The specified field %s cannot be found in target model.', attr));
alias = matches[2];
prop = alias || field.property || field.name;
return qry.fields.select(field.name).from(this.model.viewAdapter).as(prop);
}
else {
//try to match field with expression [field] as [alias] or [nested]/[field] as [alias]
field = this.model.field(attr);
if (typeof field === 'undefined' || field === null)
throw new Error(util.format('The specified field %s cannot be found in target model.', attr));
var f = qry.fields.select(field.name).from(this.model.viewAdapter);
if (field.property)
return f.as(field.property);
return f;
}
}
}
return this;
};
/**
* Prepares an ascending sorting operation
* @param {string} attr - The field name to use for sorting results
* @returns {DataQueryable}
*/
DataQueryable.prototype.orderBy = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.orderBy(DataAttributeResolver.prototype.orderByNestedAttribute.call(this, attr));
return this;
}
this.query.orderBy(this.fieldOf(attr));
return this;
};
/**
* Prepares a group by expression
* @param {...string} attr - A param array of string that represents the attributes which are going to be used in group by expression
* @returns {DataQueryable}
* @example
//retrieve products with highest sales during last month
context.model('Order')
.select('orderedItem/model as productModel', 'orderedItem/name as productName','count(id) as orderCount')
.where('orderDate').greaterOrEqual(moment().startOf('month').toDate())
.groupBy('orderedItem')
.orderByDescending('count(id)')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results
productModel productName orderCount
------------ --------------------------------------- ----------
SM5111 Brother MFC-J6920DW 3
FY8135 LaCie Blade Runner 3
HA6910 Apple iMac (27-Inch, 2013 Version) 2
LD4238 Dell XPS 18 2
HR6205 Samsung Galaxy Note 10.1 (2014 Edition) 2
*/
DataQueryable.prototype.groupBy = function(attr) {
var arr = [],
arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr;
if (util.isArray(arg)) {
for (var i = 0; i < arg.length; i++) {
var x = arg[i];
if (DataAttributeResolver.prototype.testNestedAttribute.call(this,x)) {
//nested group by
arr.push(DataAttributeResolver.prototype.orderByNestedAttribute.call(this, x));
}
else {
arr.push(this.fieldOf(x));
}
}
}
else {
if (DataAttributeResolver.prototype.testNestedAttribute.call(this,arg)) {
//nested group by
arr.push(DataAttributeResolver.prototype.orderByNestedAttribute.call(this, arg));
}
else {
arr.push(this.fieldOf(arg));
}
}
if (arr.length>0) {
this.query.groupBy(arr);
}
return this;
};
/**
* Continues a ascending sorting operation
* @param {string} attr - The field to use for sorting results
* @returns {DataQueryable}
*/
DataQueryable.prototype.thenBy = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.thenBy(DataAttributeResolver.prototype.orderByNestedAttribute.call(this, attr));
return this;
}
this.query.thenBy(this.fieldOf(attr));
return this;
};
/**
* Prepares a descending sorting operation
* @param {string} attr - The field name to use for sorting results
* @returns {DataQueryable}
*/
DataQueryable.prototype.orderByDescending = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.orderByDescending(DataAttributeResolver.prototype.orderByNestedAttribute.call(this, attr));
return this;
}
this.query.orderByDescending(this.fieldOf(attr));
return this;
};
/**
* Continues a descending sorting operation
* @param {string} - The field name to use for sorting results
* @returns {DataQueryable}
*/
DataQueryable.prototype.thenByDescending = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.thenByDescending(DataAttributeResolver.prototype.orderByNestedAttribute.call(this, attr));
return this;
}
this.query.thenByDescending(this.fieldOf(attr));
return this;
};
/**
* Executes the specified query against the underlying model and returns the first item.
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result.
* @returns {Deferred|*}
* @example
//retrieve an order by id
context.model('Order')
.where('id').equal(302)
.first().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.first = function(callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
firstInternal.call(this, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return firstInternal.call(this, callback);
}
};
/**
* @private
* @param {function(Error=,*=)} callback
*/
function firstInternal(callback) {
var self = this;
callback = callback || function() {};
self.skip(0).take(1, function(err, result) {
if (err) {
callback(err);
}
else {
if (result.length>0)
callback(null, result[0]);
else
callback(null);
}
});
}
/**
* @param {function(Error=,Array=)=} callback
* @returns {Deferred|*} - If callback function is missing returns a promise.
*/
DataQueryable.prototype.all = function(callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
allInternal.call(this, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
allInternal.call(this, callback);
}
};
/**
* @private
* @param {Function} callback
*/
function allInternal(callback) {
var self = this;
//remove skip and take
delete this.query.$skip;
delete this.query.$take;
//validate already selected fields
if (!self.query.hasFields()) {
self.select();
}
callback = callback || function() {};
//execute select
execute_.call(self, callback);
}
/**
* Prepares a paging operation by skipping the specified number of records
* @param n {number} - The number of records to be skipped
* @returns {DataQueryable}
* @example
//retrieve a list of products
context.model('Product')
.skip(10)
.take(10)
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.skip = function(n) {
this.query.$skip = n;
return this;
};
/**
* @private
* @param {Number} n - Defines the number of items to take
* @param {function=} callback
* @returns {*} - A collection of objects that meet the query provided
*/
function takeInternal(n, callback) {
var self = this;
self.query.take(n);
callback = callback || function() {};
//validate already selected fields
if (!self.query.hasFields()) {
self.select();
}
//execute select
execute_.call(self,callback);
}
/**
* Prepares a data paging operation by taking the specified number of records
* @param {Number} n - The number of records to take
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result.
* @returns {DataQueryable|*} - If callback function is missing returns a promise.
*/
DataQueryable.prototype.take = function(n, callback) {
if (typeof callback !== 'function') {
this.query.take(n);
return this;
}
else {
takeInternal.call(this, n, callback);
}
};
/**
* Executes current query and returns a result set based on the specified paging parameters.
* <p>
* The result is an instance of <a href="DataResultSet.html">DataResultSet</a>. The returned records may contain nested objects
* based on the definition of the current model (expandable fields).
* This operation is one of the common data operations on MOST Data Applications
* where the affected records may have nested objects which contain the associated objects of each object.
* </p>
<pre class="prettyprint"><code>
{
"total": 242,
"records": [
...
{
"id": 46,
"orderDate": "2014-12-31 13:35:41.000+02:00",
"orderedItem": {
"id": 413,
"additionalType": "Product",
"category": "Storage and Networking Gear",
"price": 647.13,
"model": "FY8135",
"releaseDate": "2015-01-15 18:07:42.000+02:00",
"name": "LaCie Blade Runner",
"dateCreated": "2015-11-23 14:53:04.927+02:00",
"dateModified": "2015-11-23 14:53:04.934+02:00"
},
"orderNumber": "DEF193",
"orderStatus": {
"id": 7,
"name": "Problem",
"alternateName": "OrderProblem",
"description": "Representing that there is a problem with the order."
},
"paymentDue": "2015-01-20 13:35:41.000+02:00",
"paymentMethod": {
"id": 7,
"name": "PayPal",
"alternateName": "PayPal",
"description": "Payment via the PayPal payment service."
},
"additionalType": "Order",
"description": null,
"dateCreated": "2015-11-23 21:00:18.306+02:00",
"dateModified": "2015-11-23 21:00:18.307+02:00"
}
...
]
}
</code></pre>
* @param {function(Error=,DataResultSet=)=} callback - A callback function with arguments (err, result) where the first argument is the error, if any
* and the second argument is an object that represents a result set
* @returns {Deferred|*} - If callback is missing returns a promise.
@example
//retrieve products list order by price
context.model('Product')
.where('category').equal('LCDs and Peripherals')
.orderByDescending('price')
.take(3).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.list = function(callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
listInternal.call(this, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return listInternal.call(this, callback);
}
};
/**
* @returns {Promise|*}
*/
DataQueryable.prototype.getItems = function() {
var self = this, d = Q.defer();
process.nextTick(function() {
delete self.query.$inlinecount;
if ((parseInt(self.query.$take) || 0) < 0) {
delete self.query.$take;
delete self.query.$skip;
}
if (!self.query.hasFields()) {
self.select();
}
execute_.call(self,function(err, result) {
if (err) {
return d.reject(err);
}
return d.resolve(result);
});
});
return d.promise;
};
/**
* @private
* @param {Function} callback
*/
function listInternal(callback) {
var self = this;
try {
callback = callback || function() {};
//ensure take attribute
var take = self.query.$take || 25;
//ensure that fields are already selected (or select all)
self.select();
//clone object
var q1 = self.clone();
//take objects
self.take(take, function(err, result)
{
if (err) {
callback(err);
}
else {
// get count of records
q1.count(function(err, total) {
if (err) {
callback(err);
}
else {
//and finally create result set
var res = { total: total, skip: parseInt(self.query.$skip) || 0 , records: (result || []) };
callback(null, res);
}
});
}
});
}
catch(e) {
callback(e);
}
}
/**
* @param {string} name
* @param {string=} alias
* @returns {*|QueryField}
*/
DataQueryable.prototype.countOf = function(name, alias) {
alias = alias || 'countOf'.concat(name);
var res = this.fieldOf(util.format('count(%s)', name));
if (typeof alias !== 'undefined' && alias!=null)
res.as(alias);
return res;
};
/**
* @param {string} name
* @param {string=} alias
* @returns {*|QueryField}
* @deprecated
* @ignore
*/
DataQueryable.prototype.maxOf = function(name, alias) {
alias = alias || 'maxOf'.concat(name);
var res = this.fieldOf(util.format('max(%s)', name));
if (typeof alias !== 'undefined' && alias!=null)
res.as(alias);
return res;
};
/**
* @param {string} name
* @param {string=} alias
* @returns {*|QueryField}
* @deprecated
* @ignore
*/
DataQueryable.prototype.minOf = function(name, alias) {
alias = alias || 'minOf'.concat(name);
var res = this.fieldOf(util.format('min(%s)', name));
if (typeof alias !== 'undefined' && alias!=null)
res.as(alias);
return res;
};
/**
* @param {string} name
* @param {string=} alias
* @returns {*|QueryField}
* @deprecated
* @ignore
*/
DataQueryable.prototype.averageOf = function(name, alias) {
alias = alias || 'avgOf'.concat(name);
var res = this.fieldOf(util.format('avg(%s)', name));
if (typeof alias !== 'undefined' && alias!=null)
res.as(alias);
return res;
};
/**
* @param {string} name
* @param {string=} alias
* @returns {*|QueryField}
*/
DataQueryable.prototype.sumOf = function(name, alias) {
alias = alias || 'sumOf'.concat(name);
var res = this.fieldOf(util.format('sum(%s)', name));
if (typeof alias !== 'undefined' && alias!=null)
res.as(alias);
return res;
};
/**
* @private
* @param callback {Function}
* @returns {*} - A collection of objects that meet the query provided
*/
function countInternal(callback) {
var self = this;
callback = callback || function() {};
//add a count expression
var field = self.model.attributes[0];
if (field==null)
return callback.call(this, new Error('Queryable collection does not have any property.'));
//normalize query and remove skip
delete self.query.$skip;
delete self.query.$take;
delete self.query.$order;
delete self.query.$group;
//append count expression
self.query.select([qry.fields.count(field.name).from(self.model.viewAdapter)]);
//execute select
execute_.call(self, function(err, result) {
if (err) { callback.call(self, err, result); return; }
var value = null;
if (util.isArray(result)) {
//get first value
if (result.length>0)
value = result[0][field.name];
}
callback.call(self, err, value);
});
}
/**
* Executes the query against the current model and returns the count of items found.
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result, if any.
* @returns {Deferred|*} - If callback parameter is missing then returns a Deferred object.
* @example
//retrieve the number of a product's orders
context.model('Order')
.where('orderedItem').equal(302)
.count().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.count = function(callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
countInternal.call(this, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return countInternal.call(this, callback);
}
};
/**
* @private
* @param {string} attr
* @param callback {Function}
*/
function maxInternal(attr, callback) {
var self = this;
delete self.query.$skip;
var field = DataAttributeResolver.prototype.selectAggregatedAttribute.call(self, 'max', attr);
self.select([field]).flatten().value(function(err, result) {
if (err) { return callback(err); }
callback(null, result)
});
}
/**
* Executes the query against the current model and returns the maximum value of the given attribute.
* @param {string} attr - A string that represents a field of the current model
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result, if any.
* @returns {Deferred|*} - If callback parameter is missing then returns a Deferred object.
* @example
//retrieve the maximum price of products sold during last month
context.model('Order')
.where('orderDate').greaterOrEqual(moment().startOf('month').toDate())
.max('orderedItem/price').then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.max = function(attr, callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
maxInternal.call(this, attr, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return maxInternal.call(this, attr, callback);
}
};
/**
* @private
* @param attr {String}
* @param callback {Function}
*/
function minInternal(attr, callback) {
var self = this;
delete self.query.$skip;
var field = DataAttributeResolver.prototype.selectAggregatedAttribute.call(self, 'min', attr);
self.select([field]).flatten().value(function(err, result) {
if (err) { return callback(err); }
callback(null, result)
});
}
/**
* Executes the query against the current model and returns the average value of the given attribute.
* @param {string} attr - A string that represents a field of the current model
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result, if any.
* @returns {Deferred|*} - If callback parameter is missing then returns a Deferred object.
* @example
//retrieve the mininum price of products sold during last month
context.model('Order')
.where('orderDate').greaterOrEqual(moment().startOf('month').toDate())
.min('orderedItem/price').then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.min = function(attr, callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
minInternal.call(this, attr, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return minInternal.call(this, attr, callback);
}
};
/**
* @private
* @param {string} attr
* @param {Function} callback
*/
function averageInternal_(attr, callback) {
var self = this;
delete self.query.$skip;
var field = DataAttributeResolver.prototype.selectAggregatedAttribute.call(self, 'avg', attr);
self.select([field]).flatten().value(function(err, result) {
if (err) { return callback(err); }
callback(null, result)
});
}
/**
* Executes the query against the current model and returns the average value of the given attribute.
* @param {string} attr - A string that represents a field of the current model
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result, if any.
* @returns {Deferred|*} - If callback parameter is missing then returns a Deferred object.
* @example
//retrieve the average price of products sold during last month
context.model('Order')
.where('orderDate').greaterOrEqual(moment().startOf('month').toDate())
.average('orderedItem/price').then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.average = function(attr, callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
averageInternal_.call(this, attr, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return averageInternal_.call(this, attr, callback);
}
};
/**
* @private
* @param {Function} callback
*/
function executeCount_(callback) {
try {
var self = this, context = self.ensureContext();
var clonedQuery = self.query.clone();
//delete properties
delete clonedQuery.$skip;
delete clonedQuery.$take;
//add wildcard field
clonedQuery.select([qry.fields.count('*')]);
//execute count
context.db.execute(clonedQuery, null, function(err, result) {
if (err) {
callback(err);
return;
}
callback(err, result.length>0 ? result[0] : 0);
});
}
catch (e) {
callback(e);
}
};
/**
* Migrates the underlying data model
* @param {function(Error=)} callback - A callback function that should be called at the end of this operation. The first argument may be an error if any occured.
*/
DataQueryable.prototype.migrate = function(callback) {
var self = this;
try {
//ensure context
self.ensureContext();
if (self.model) {
self.model.migrate(function(err) {
callback(err);
})
}
else {
callback();
}
}
catch (e) {
callback(e);
}
};
DataQueryable.prototype.postExecute = function(result, callback) {
callback();
};
/**
* Executes the underlying query statement.
* @param {function(Error,*=)} callback
* @private
*/
function execute_(callback) {
var self = this, context = self.ensureContext();
self.migrate(function(err) {
if (err) { callback(err); return; }
var e = { model:self.model, query:self.query, type:'select' };
var flatten = self.$flatten || (self.getLevels()==0);
if (!flatten) {
//get expandable fields
var expandables = self.model.attributes.filter(function(x) { return x.expandable; });
//get selected fields
var selected = self.query.$select[self.model.viewAdapter];
if (util.isArray(selected)) {
//remove hidden fields
var hiddens = self.model.attributes.filter(function(x) { return x.hidden; });
if (hiddens.length>0) {
for (var i = 0; i < selected.length; i++) {
var x = selected[i];
var hiddenField = hiddens.find(function(y) {
var f = x instanceof qry.classes.QueryField ? x : new qry.classes.QueryField(x);
return f.name() == y.name;
});
if (hiddenField) {
selected.splice(i, 1);
i-=1;
}
}
}
//expand fields
if (expandables.length>0) {
selected.forEach(function(x) {
//get field
var field = expandables.find(function(y) {
var f = x instanceof qry.classes.QueryField ? x : new qry.classes.QueryField(x);
return f.name() == y.name;
});
//add expandable models
if (field) {
var mapping = self.model.inferMapping(field.name);
if (mapping) {
self.$expand = self.$expand || [ ];
var expand1 = self.$expand.find(function(x) {
return x.name === field.name;
});
if (typeof expand1 === 'undefined') {
self.expand(mapping);
}
}
}
});
}
}
}
//merge view filter. if any
if (self.$view) {
self.model.filter({ $filter: self.$view.filter, $order:self.$view.order, $group:self.$view.group }, function(err, q) {
if (err) {
if (err) { callback(err); }
}
else {
//prepare current filter
if (q.query.$prepared) {
if (e.query.$where)
e.query.prepare();
e.query.$where = q.query.$prepared;
}
if (q.query.$group)
//replace group fields
e.query.$group = q.query.$group;
//add order fields
if (q.query.$order) {
if (util.isArray(e.query.$order)) {
q.query.$order.forEach(function(x) { e.query.$order.push(x); });
}
else {
e.query.$order = q.query.$order;
}
}
//execute query
finalExecuteInternal_.call(self, e, callback);
}
});
}
else {
//execute query
finalExecuteInternal_.call(self, e, callback);
}
});
}
/**
* @private
* @param {*} e
* @param {function(Error=,*=)} callback
*/
function finalExecuteInternal_(e, callback) {
var self = this, context = self.ensureContext();
//pass data queryable to event
e.emitter = this;
var afterListenerCount = self.model.listeners('after.execute').length;
self.model.emit('before.execute', e, function(err) {
if (err) {
callback(err);
}
else {
//if command has been completed, do not execute the command against the underlying database
if (typeof e['result'] !== 'undefined') {
//call after execute
var result = e['result'];
afterExecute_.call(self, result, function(err, result) {
if (err) { callback(err); return; }
if (afterListenerCount==0) { callback(null, result); return; }
//raise after execute event
self.model.emit('after.execute', e, function(err) {
if (err) { callback(err); return; }
callback(null, result);
});
});
return;
}
context.db.execute(e.query, null, function(err, result) {
if (err) { callback(err); return; }
afterExecute_.call(self, result, function(err, result) {
if (err) { callback(err); return; }
if (afterListenerCount==0) { callback(null, result); return; }
//raise after execute event
e.result = result;
self.model.emit('after.execute', e, function(err) {
if (err) { callback(err); return; }
callback(null, result);
});
});
});
}
});
}
/**
* @param {*} result
* @param {Function} callback
* @private
*/
function afterExecute_(result, callback) {
var self = this, field, parentField, junction;
if (self.$expand) {
//get distinct values
var expands = self.$expand.distinct(function(x) { return x; });
async.eachSeries(expands, function(expand, cb) {
/**
* get mapping
* @type {DataAssociationMapping|*}
*/
var mapping = null, options = { };
if (expand instanceof types.DataAssociationMapping) {
mapping = expand;
if (typeof expand.select !== 'undefined' && expand.select != null) {
if (typeof expand.select === 'string')
options["$select"] = expand.select;
else if (util.isArray(expand.select))
options["$select"] = expand.select.join(",");
}
//get expand options
if (typeof expand.options !== 'undefined' && expand.options != null) {
util._extend(options, expand.options);
}
}
else {
//get mapping from expand attribute
var expandAttr;
if (typeof expand === 'string') {
//get expand attribute as string
expandAttr = expand;
}
else if ((typeof expand === 'object') && expand.hasOwnProperty('name')) {
//get expand attribute from Object.name property
expandAttr = expand.name;
//get expand options
if (typeof expand.options !== 'undefined' && expand.options != null) {
options = expand.options;
}
}
else {
//invalid expand parameter
return callback(new Error("Invalid expand option. Expected string or a named object."));
}
field = self.model.field(expandAttr);
if (typeof field === 'undefined')
field = self.model.attributes.find(function(x) { return x.type==expandAttr });
if (field) {
mapping = self.model.inferMapping(field.name);
if (expands.find(function(x) {
return (x.parentField === mapping.parentField) &&
(x.parentModel === mapping.parentModel) &&
(x.childField === mapping.childField) &&
(x.childModel === mapping.childModel)
})) {
return cb();
}
if (mapping) { mapping.refersTo = mapping.refersTo || field.name; }
}
}
if (options instanceof DataQueryable) {
// do nothing
}
else {
//set default $top option to -1 (backward compatibility issue)
if (!options.hasOwnProperty("$top")) {
options["$top"] = -1;
}
//set default $levels option to 1 (backward compatibility issue)
if (!options.hasOwnProperty("$levels")) {
if (typeof self.$levels === 'number') {
options["$levels"] = self.getLevels() - 1;
}
}
}
if (mapping) {
var associatedModel, values, keyField, arr, junction;
if (mapping.associationType=='association' || mapping.associationType=='junction') {
//1. current model is the parent model and association type is association
if ((mapping.parentModel==self.model.name) && (mapping.associationType=='association') && (mapping.parentModel!=mapping.childModel)) {
associatedModel = self.model.context.model(mapping.childModel);
values=[];
keyField = mapping.parentField;
if (util.isArray(result)) {
iterator = function(x) { if (x[keyField]) { if (values.indexOf(x[keyField])==-1) { values.push(x[keyField]); } } };
result.forEach(iterator);
}
else {
if (result[keyField])
values.push(result[keyField]);
}
if (values.length==0) {
return cb(null);
}
else {
field = associatedModel.field(mapping.childField);
parentField = mapping.refersTo;
//search for view named summary
associatedModel.filter(options, function(err, expandQ) {
if (err) {
return cb(err);
}
expandQ.prepare();
//append base where statement for this operation
expandQ.where(field.name).in(values);
//final execute query
return expandQ.getItems().then(function(childs) {
var key=null,
attr = (field.property || field.name),
selector = function(x) {
return x[attr]==key;
},
iterator = function(x) {
key =x[mapping.parentField];
x[parentField] = childs.filter(selector);
};
if (util.isArray(result)) {
result.forEach(iterator);
}
else {
key =result[mapping.parentField];
result[parentField] = childs.filter(selector);
}
return cb();
}).catch(function(err) {
return cb(err);
});
});
}
}
else if (mapping.childModel==self.model.name && mapping.associationType=='junction') {
//create a dummy object
var HasParentJunction = require('./has-parent-junction').HasParentJunction;
junction = new HasParentJunction(self.model.convert({}), mapping);
//ensure array of results
arr = util.isArray(result) ? result : [result];
//get array of key values (for childs)
values = arr.filter(function(x) { return (typeof x[mapping.childField]!=='undefined') && (x[mapping.childField]!=null); }).map(function(x) { return x[mapping.childField] });
//query junction model
junction.baseModel.where('valueId').in(values).silent().all(function(err, junctions) {
if (err) { return cb(err); }
//get array of parent key values
values = junctions.map(function(x) { return x['parentId'] });
//get parent model
var parentModel = self.model.context.model(mapping.parentModel);
if (_.isString(options['$select'])) {
if (options['$select'] !== "*") {
var selectOptions = options['$select'].split(",");
if (selectOptions.indexOf(mapping.parentField)<0) {
selectOptions.unshift(mapping.parentField);
options['$select'] = selectOptions.join(",");
}
}
}
//query parent with parent key values
parentModel.filter(options, function(err, expandQ) {
if (err) {
return cb(err);
}
expandQ.prepare();
//append base where statement for this operation
expandQ.where(mapping.parentField).in(values);
//set silent (?)
expandQ.silent();
//and finally query parent
expandQ.getItems().then(function(parents){
//if result contains only one item
if (arr.length == 1) {
arr[0][field.name] = parents;
return cb();
}
//otherwise loop result array
arr.forEach(function(x) {
//get child (key value)
var valueId = x[mapping.childField];
//get parent(s)
var p = junctions.filter(function(y) { return (y.valueId==valueId); }).map(function(r) { return r['parentId']; });
//filter data and set property value (a filtered array of parent objects)
x[field.name] = parents.filter(function(z) { return p.indexOf(z[mapping.parentField])>=0; });
});
return cb();
}).catch(function(err) {
return cb(err);
});
});
});
}
else if (mapping.parentModel==self.model.name && mapping.associationType=='junction') {
//get array of results
arr = util.isArray(result) ? result : [result];
//get array of key values (for parents)
values = arr.filter(function(x) { return (typeof x[mapping.parentField]!=='undefined') && (x[mapping.parentField]!=null); }).map(function(x) { return x[mapping.parentField] });
if (typeof mapping.childModel === 'undefined') {
var DataObjectTag = require('./data-object-tag').DataObjectTag;
junction = new DataObjectTag(self.model.convert({ }), mapping);
return junction.getBaseModel().where("object").in(values).flatten().silent().select("object", "value").all().then(function(items) {
arr.forEach(function(x) {
x[field.name] = items.filter(function(y) {
return y["object"]===x[mapping.parentField];
}).map(function (y) {
return y.value;
});
});
return cb();
}).catch(function (err) {
return cb(err);
});
}
//create a dummy object
var DataObjectJunction = require('./data-object-junction').DataObjectJunction;
junction = new DataObjectJunction(self.model.convert({}), mapping);
//query junction model
junction.baseModel.where('parentId').in(values).flatten().silent().all(function(err, junctions) {
if (err) { cb(err); return; }
//get array of child key values
values = junctions.map(function(x) { return x['valueId'] });
//get child model
var childModel = self.model.context.model(mapping.childModel);
if (_.isString(options['$select'])) {
if (options['$select'] !== "*") {
var selectOptions = options['$select'].split(",");
if (selectOptions.indexOf(mapping.childField)<0) {
selectOptions.unshift(mapping.childField);
options['$select'] = selectOptions.join(",");
}
}
}
childModel.filter(options, function(err, expandQ) {
if (err) {
return cb(err);
}
expandQ.prepare();
//append where statement for this operation
expandQ.where(mapping.childField).in(values);
//set silent (?)
expandQ.silent();
//and finally query childs
expandQ.getItems().then(function(childs) {
//if result contains only one item
if (arr.length == 1) {
arr[0][field.name] = childs;
return cb();
}
//otherwise loop result array
arr.forEach(function(x) {
//get parent (key value)
var parentId = x[mapping.parentField];
//get parent(s)
var p = junctions.filter(function(y) { return (y.parentId==parentId); }).map(function(r) { return r['valueId']; });
//filter data and set property value (a filtered array of parent objects)
x[field.name] = childs.filter(function(z) { return p.indexOf(z[mapping.childField])>=0; });
});
return cb();
}).catch(function(err) {
return cb(err);
});
});
});
}
else {
/**
* @type {DataModel}
* @private
*/
associatedModel = self.model.context.model(mapping.parentModel);
var keyAttr = self.model.field(mapping.childField);
values = [];
keyField = keyAttr.property || keyAttr.name;
if (util.isArray(result)) {
var iterator = function(x) { if (x[keyField]) { if (values.indexOf(x[keyField])==-1) { values.push(x[keyField]); } } };
result.forEach(iterator);
}
else {
if (result[keyField])
values.push(result[keyField]);
}
if (values.length==0) {
return cb();
}
else {
var childField = self.model.field(mapping.childField);
associatedModel.filter(options, function(err, expandQ) {
if (err) {
return cb(err);
}
expandQ.prepare();
//Important Backward compatibility issue (<1.8.0)
//Description: if $levels parameter is not defined then set the default value to 0.
if (typeof expandQ.$levels === 'undefined') {
expandQ.$levels = 0;
}
//append where statement for this operation
expandQ.where(mapping.parentField).in(values);
//set silent (?)
expandQ.silent();
expandQ.getItems().then(function(parents) {
var key=null,
selector = function(x) {
return x[mapping.parentField]==key;
},
iterator = function(x) {
key = x[keyField];
if (childField.property && childField.property!==childField.name) {
x[childField.property] = parents.filter(selector)[0];
delete x[childField.name];
}
else
x[childField.name] = parents.filter(selector)[0];
};
if (util.isArray(result)) {
result.forEach(iterator);
}
else {
key = result[keyField];
if (childField.property && childField.property!==childField.name) {
result[childField.property] = parents.filter(selector)[0];
delete result[childField.name];
}
else
result[childField.name] = parents.filter(selector)[0];
}
return cb();
}).catch(function(err) {
return cb(err);
});
});
}
}
}
else {
return cb(new Error("Not yet implemented"));
}
}
else {
console.log(util.format('Data assocication mapping (%s) for %s cannot be found or the association between these two models defined more than once.', expand, self.model.title));
return cb(null);
}
}, function(err) {
if (err) {
callback(err);
}
else {
toArrayCallback.call(self, result, callback);
}
});
}
else {
toArrayCallback.call(self, result, callback);
}
}
/**
* @private
* @param {Array|*} result
* @param {Function} callback
*/
function toArrayCallback(result, callback) {
try {
var self = this;
if (self.$asArray) {
if (typeof self.query === 'undefined') {
return callback(null, result);
}
var fields = self.query.fields();
if (util.isArray(fields)==false) {
return callback(null, result);
}
if (fields.length==1) {
var arr = [];
result.forEach(function(x) {
if (_.isNil(x))
return;
var key = Object.keys(x)[0];
if (x[key])
arr.push(x[key]);
});
return callback(null, arr);
}
else {
return callback(null, result);
}
}
else {
return callback(null, result);
}
}
catch (e) {
return callback(e);
}
}
/**
* Disables permission listeners and executes the underlying query without applying any permission filters
* @param {Boolean=} value - A boolean which represents the silent flag. If value is missing the default parameter is true.
* @returns {DataQueryable}
* @example
//retrieve user
context.model('User')
.where('name').equal('other@example.com')
.silent()
.first().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.silent = function(value) {
/**
* @type {boolean}
* @private
*/
this.$silent = false;
if (typeof value === 'undefined') {
this.$silent = true;
}
else {
this.$silent = value;
}
return this;
};
/**
* Generates a MD5 hashed string for this DataQueryable instance
* @returns {string}
*/
DataQueryable.prototype.toMD5 = function() {
var q = { query:this.query };
if (typeof this.$expand !== 'undefined') { q.$expand =this.$expand; }
if (typeof this.$levels!== 'undefined') { q.$levels =this.$levels; }
if (typeof this.$flatten!== 'undefined') { q.$flatten =this.$flatten; }
if (typeof this.$silent!== 'undefined') { q.$silent =this.$silent; }
if (typeof this.$asArray!== 'undefined') { q.$asArray =this.$asArray; }
return dataCommon.md5(q);
};
/**
* @param {Boolean=} value
* @returns {DataQueryable}
*/
DataQueryable.prototype.asArray = function(value) {
/**
* @type {boolean}
* @private
*/
this.$asArray = false;
if (typeof value === 'undefined') {
this.$asArray = true;
}
else {
this.$asArray = value;
}
return this;
};
/**
* Gets or sets query data. This data may be used in before and after execute listeners.
* @param {string=} name
* @param {*=} value
* @returns {DataQueryable|*}
*/
DataQueryable.prototype.data = function(name, value) {
this.query.data = this.query.data || {};
if (typeof name === 'undefined') {
return this.query.data;
}
if (typeof value === 'undefined') {
return this.query.data[name];
}
else {
this.query.data[name] = value;
}
return this;
};
/**
* Gets or sets a string which represents the title of this DataQueryable instance. This title may be used in caching operations
* @param {string=} value - The title of this DataQueryable instance
* @returns {string|DataQueryable}
*/
DataQueryable.prototype.title = function(value) {
return this.data('title', value);
};
/**
* Gets or sets a boolean which indicates whether results should be cached or not. This parameter is valid for models which have caching mechanisms.
* @param {string=} value
* @returns {string|DataQueryable}
*/
DataQueryable.prototype.cache = function(value) {
return this.data('cache', value);
};
/**
* Sets an expandable field or collection of fields. An expandable field produces nested objects based on the association between two models.
* @param {...string|*} attr - A param array of strings which represents the field or the array of fields that are going to be expanded.
* If attr is missing then all the previously defined expandable fields will be removed.
* @returns {DataQueryable}
* @example
//retrieve an order and expand customer field
context.model('Order')
//note: the field [orderedItem] is defined as expandable in model definition and it will produce a nested object for each order
.select('id','orderedItem','customer')
.expand('customer')
.where('id').equal(46)
.first().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Result:
{
"id": 46,
"orderedItem": {
"id": 413,
"additionalType": "Product",
"category": "Storage and Networking Gear",
"price": 647.13,
"model": "FY8135",
"releaseDate": "2015-01-15 18:07:42.000+02:00",
"name": "LaCie Blade Runner",
"dateCreated": "2015-11-23 14:53:04.927+02:00",
"dateModified": "2015-11-23 14:53:04.934+02:00"
},
"customer": {
"id": 317,
"additionalType": "Person",
"alternateName": null,
"description": "Nicole Armstrong",
"image": "https://s3.amazonaws.com/uifaces/faces/twitter/zidoway/128.jpg",
"dateCreated": "2015-11-23 14:52:57.886+02:00",
"dateModified": "2015-11-23 14:52:57.917+02:00"
}
}
@example //retrieve an order and do not expand customer field
{
"id": 46,
"orderedItem": {
"id": 413,
"additionalType": "Product",
"category": "Storage and Networking Gear",
"price": 647.13,
"model": "FY8135",
"releaseDate": "2015-01-15 18:07:42.000+02:00",
"name": "LaCie Blade Runner",
"dateCreated": "2015-11-23 14:53:04.927+02:00",
"dateModified": "2015-11-23 14:53:04.934+02:00"
},
"customer": 317
}
*/
DataQueryable.prototype.expand = function(attr) {
var self = this,
arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr;
if (_.isNil(arg)) {
delete self.$expand;
}
else {
if (!util.isArray(this.$expand))
self.$expand=[];
if (util.isArray(arg)) {
arg.forEach(function(x) {
if (_.isNil(x)) {
return;
}
if ((typeof x === 'string')
|| (typeof x === 'object' && x.hasOwnProperty('name'))) {
self.$expand.push(x);
}
else {
throw new Error("Expand option may be a string or a named object.")
}
});
}
else {
self.$expand.push(arg);
}
}
return self;
};
/**
* Disables expandable fields
* @param {boolean=} value - If the value is true the result will contain only flat objects -without any nested associated object-,
* even if model definition contains expandable fields. If value is missing, the default parameter is true
* @returns {DataQueryable}
* @example
//retrieve a list of orders
context.model('Order')
.flatten()
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
id customer orderStatus paymentMethod
-- -------- ----------- -------------
1 299 5 6
2 337 7 5
3 309 3 3
4 257 2 4
5 285 5 2
*/
DataQueryable.prototype.flatten = function(value) {
if (value || (typeof value==='undefined')) {
//delete expandable data (if any)
delete this.$expand;
this.$flatten = true;
}
else {
delete this.$flatten;
}
if (this.$flatten) {
this.$levels = 0;
}
return this;
};
/**
* Prepares an addition (e.g. ([field] + 4))
* @param {number|*} x - The
* @returns {DataQueryable}
* @example
//retrieve a list of products
context.model('Product')
.select('id','name', 'price')
//perform ((ProductData.price + 100)>300)
.where('price').add(100).lowerThan(300)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.add = function(x) {
this.query.add(x); return this;
};
/**
* Prepares a subtraction (e.g. ([field] - 4))
* @param {number|*} x
* @returns {DataQueryable}
//retrieve a list of orders
context.model('Product')
.select('id','name', 'price')
//perform ((ProductData.price - 50)<150)
.where('price').subtract(50).lowerThan(150)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.subtract = function(x) {
this.query.subtract(x); return this;
};
/**
* Prepares a multiplication (e.g. ([field] * 0.2))
* @param {number} x
* @returns {DataQueryable}
@example
//retrieve a list of orders
context.model('Product')
.select('id','name', 'price')
//perform ((ProductData.price * 0.2)<50)
.where('price').multiply(0.2).lowerThan(50)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.multiply = function(x) {
this.query.multiply(x); return this;
};
/**
* Prepares a division (e.g. ([field] / 0.2))
* @param {number} x
* @returns {DataQueryable}
@example
//retrieve a list of orders
context.model('Product')
.select('id','name', 'price')
//perform ((ProductData.price / 0.8)>500)
.where('price').divide(0.8).greaterThan(500)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.divide = function(x) {
this.query.divide(x); return this;
};
/**
* * Prepares a round mathematical expression
* @param {number=} n
* @returns {DataQueryable}
*/
DataQueryable.prototype.round = function(n) {
this.query.round(x); return this;
};
/**
* Prepares a substring comparison
* @param {number} start - The position where to start the extraction. First character is at index 0
* @param {number=} length - The number of characters to extract
* @returns {DataQueryable}
* @example
//retrieve a list of persons
context.model('Person')
.select('givenName')
.where('givenName').substr(0,4).equal('Alex')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
givenName
---------
Alex
Alexis
*/
DataQueryable.prototype.substr = function(start,length) {
this.query.substr(start,length); return this;
};
/**
* Prepares an indexOf comparison
* @param {string} s The string to search for
* @returns {DataQueryable}
* @example
//retrieve a list of persons
context.model('Person')
.select('givenName')
.where('givenName').indexOf('a').equal(1)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
givenName
---------
Daisy
Maxwell
Mackenzie
Zachary
Mason
*/
DataQueryable.prototype.indexOf = function(s) {
this.query.indexOf(s); return this;
};
/**
* Prepares a string concatenation expression
* @param {string} s
* @returns {DataQueryable}
*/
DataQueryable.prototype.concat = function(s) {
this.query.concat(s); return this;
};
/**
* Prepares a string trimming expression
* @returns {DataQueryable}
*/
DataQueryable.prototype.trim = function() {
this.query.trim(); return this;
};
/**
* Prepares a string length expression
* @returns {DataQueryable}
* @example
//retrieve a list of persons
context.model('Person')
.select('givenName')
.where('givenName').length().equal(5)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
givenName
---------
Daisy
Peter
Kylie
Colin
Lydia
*/
DataQueryable.prototype.length = function() {
this.query.length(); return this;
};
/**
* Prepares an expression by getting the date only value of a datetime field
* @returns {DataQueryable}
* @example
//retrieve a list of orders
context.model('Order')
.select('id','paymentDue', 'orderDate')
.where('orderDate').getDate().equal('2015-01-16')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.getDate = function() {
this.query.getDate(); return this;
};
/**
* Prepares an expression by getting the year of a datetime field
* @returns {DataQueryable}
* @example
//retrieve a list of orders made during 2015
context.model('Order')
.where('orderDate').getYear().equal(2015)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.getYear = function() {
this.query.getYear(); return this;
};
/**
* Prepares an expression by getting the year of a datetime field
* @returns {DataQueryable}
* @example
//retrieve a list of orders made during 2015
context.model('Order')
.where('orderDate').getYear().equal(2015)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.getFullYear = function() {
this.query.getYear(); return this;
};
/**
* Prepares an expression by getting the month (from 1 to 12) of a datetime field.
* @returns {DataQueryable}
* @example
//retrieve a list of orders made during October 2015
context.model('Order')
.where('orderDate').getYear().equal(2015)
.and('orderDate').getMonth().equal(10)
.take(5).list().then(function(result) {
console.table(result.records);
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.getMonth = function() {
this.query.getMonth(); return this;
};
/**
* Prepares an expression by getting the day of the month of a datetime field
* @returns {DataQueryable}
* @example
//retrieve a list of orders
context.model('Order')
.where('orderDate').getYear().equal(2015)
.and('orderDate').getMonth().equal(1)
.and('orderDate').getDay().equal(16)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.getDay = function() {
this.query.getDay(); return this;
};
/**
* Prepares an expression by getting the hours (from 0 to 23) a datetime field
* @returns {DataQueryable}
*/
DataQueryable.prototype.getHours = function() {
this.query.getHours(); return this;
};
/**
* Prepares an expression by getting the minutes (from 0 to 59) a datetime field
* @returns {DataQueryable}
*/
DataQueryable.prototype.getMinutes = function() {
this.query.getMinutes(); return this;
};
/**
* Prepares an expression by getting the seconds (from 0 to 59) a datetime field
* @returns {DataQueryable}
*/
DataQueryable.prototype.getSeconds = function() {
this.query.getSeconds(); return this;
};
/**
* Prepares a floor mathematical expression
* @returns {DataQueryable}
*/
DataQueryable.prototype.floor = function() {
this.query.floor(); return this;
};
/**
* Prepares a ceil mathematical expression
* @returns {DataQueryable}
*/
DataQueryable.prototype.ceil = function() {
this.query.ceil(); return this;
};
/**
* Prepares a lower case string comparison
* @returns {DataQueryable}
*/
DataQueryable.prototype.toLocaleLowerCase = function() {
this.query.toLocaleLowerCase(); return this;
};
/**
* Prepares a lower case string comparison
* @returns {DataQueryable}
* @example
//retrieve a list of persons
context.model('Person')
.where('givenName').toLocaleLowerCase().equal('alexis')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.toLowerCase = function() {
return this.toLocaleLowerCase();
};
/**
* Prepares an upper case string comparison
* @returns {DataQueryable}
*/
DataQueryable.prototype.toLocaleUpperCase = function() {
this.query.toLocaleUpperCase(); return this;
};
/**
* Prepares an upper case string comparison
* @returns {DataQueryable}
*/
DataQueryable.prototype.toUpperCase = function() {
return this.toLocaleUpperCase();
};
/**
* @private
* @param {Function} callback
*/
function valueInternal(callback) {
if (_.isNil(this.query.$select)) {
this.select(this.model.primaryKey);
}
firstInternal.call(this, function(err, result) {
if (err) { return callback(err); }
if (_.isNil(result)) { return callback(); }
var key = Object.keys(result)[0];
if (typeof key === 'undefined') { return callback(); }
callback(null, result[key]);
});
}
/**
* Executes the underlying query and a single value.
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result.
* @returns {Deferred|*}
* @example
//retrieve the full name (description) of a person
context.model('Person')
.where('user').equal(330)
.select('description')
.value().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.value = function(callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
valueInternal.call(this, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return valueInternal.call(this, callback);
}
};
/**
* Sets the number of levels of the expandable attributes.
* The default value is 1 which means that any expandable attribute will be flat (without any other nested attribute).
* If the value is greater than 1 then the nested objects may contain other nested objects and so on.
* @param {Number=} value - A number which represents the number of levels which are going to be used in expandable attributes.
* @returns {DataQueryable}
* @example
//get orders, expand customer and get customer's nested objects if any.
context.model('Order')
.orderByDescending('dateCreated)
.expand('customer')
.levels(2)
.getItems().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.levels = function(value) {
/**
* @type {number}
* @private
*/
this.$levels = 1;
if (typeof value === 'undefined') {
this.$levels = 1;
}
else if (typeof value === 'number') {
this.$levels = parseInt(value);
}
//set flatten property (backward compatibility issue)
this.$flatten = (this.$levels<1);
return this;
};
/**
* Gets the number of levels of the expandable objects
* @returns {number}
*/
DataQueryable.prototype.getLevels = function() {
if (typeof this.$levels === 'number') {
return this.$levels;
}
return 1;
};
/**
* Converts a DataQueryable instance to an object which is going to be used as parameter in DataQueryable.expand() method
* @param {String} attr - A string which represents the attribute of a model which is going to be expanded with the options specified in this instance of DataQueryable.
*
* @example
//get customer and customer orders with options (e.g. select specific attributes and sort orders by order date)
context.model("Person")
.search("Daisy")
.expand(context.model("Order").select("id", "customer","orderStatus", "orderDate", "orderedItem").levels(2).orderByDescending("orderDate").take(10).toExpand("orders"))
.take(3)
.getItems()
.then(function (result) {
console.log(JSON.stringify(result));
done();
}).catch(function (err) {
done(err);
});
*
*/
DataQueryable.prototype.toExpand = function(attr) {
if ((typeof attr === 'string') && (attr.length>0)) {
return {
name: attr,
options: this
}
}
throw new Error("Invalid parameter. Expected not empty string.")
};
/**
* Executes the specified query against the underlying model and returns the first item.
* @returns {Promise|*}
*/
DataQueryable.prototype.getItem = function() {
var self = this, d = Q.defer();
process.nextTick(function() {
self.first().then(function (result) {
return d.resolve(result);
}).catch(function(err) {
return d.reject(err);
});
});
return d.promise;
};
/**
* Gets an instance of DataObject by executing the defined query.
* @returns {Promise|*}
*/
DataQueryable.prototype.getTypedItem = function() {
var self = this, d = Q.defer();
process.nextTick(function() {
self.first().then(function (result) {
return d.resolve(self.model.convert(result));
}).catch(function(err) {
return d.reject(err);
});
});
return d.promise;
};
/**
* Gets a collection of DataObject instances by executing the defined query.
* @returns {Promise|*}
*/
DataQueryable.prototype.getTypedItems = function() {
var self = this, d = Q.defer();
process.nextTick(function() {
self.getItems().then(function (result) {
return d.resolve(self.model.convert(result));
}).catch(function(err) {
return d.reject(err);
});
});
return d.promise;
};
/**
* Gets a result set that contains a collection of DataObject instances by executing the defined query.
* @returns {Promise|*}
*/
DataQueryable.prototype.getTypedList = function() {
var self = this, d = Q.defer();
process.nextTick(function() {
self.list().then(function (result) {
result.records = self.model.convert(result.records.slice(0));
return d.resolve(result);
}).catch(function(err) {
return d.reject(err);
});
});
return d.promise;
};
if (typeof exports !== 'undefined')
{
module.exports = {
DataQueryable:DataQueryable,
DataAttributeResolver:DataAttributeResolver
};
}