@themost/data
Version:
MOST Web Framework Codename Blueshift - Data module
1,481 lines (1,429 loc) • 102 kB
JavaScript
// MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved
var async = require('async');
var {sprintf} = require('sprintf-js');
var _ = require('lodash');
var {TextUtils} = require('@themost/common');
var {DataMappingExtender} = require('./data-mapping-extensions');
var {DataAssociationMapping} = require('./types');
var {DataError, Args} = require('@themost/common');
var {QueryField, Expression, MethodCallExpression} = require('@themost/query');
var {QueryEntity} = require('@themost/query');
var {QueryUtils} = require('@themost/query');
var Q = require('q');
var {hasOwnProperty} = require('./has-own-property');
var { DataAttributeResolver } = require('./data-attribute-resolver');
var { DataExpandResolver } = require('./data-expand-resolver');
var {instanceOf} = require('./instance-of');
var { DataValueResolver } = require('./data-value-resolver');
/**
* @param {DataQueryable} target
*/
function resolveJoinMember(target) {
return function onResolvingJoinMember(event) {
/**
* @type {Array}
*/
var fullyQualifiedMember = event.fullyQualifiedMember.split('.');
// validate first member
const attribute = target.model.getAttribute(fullyQualifiedMember[0]);
var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(target, fullyQualifiedMember.join('/'));
if (attribute && attribute.type === 'Json') {
Args.check(expr.$value != null, 'Invalid expression. Expected a JSON expression.');
var [method] = Object.keys(expr.$value); // get method name
var methodWithoutSign = method.replace(/\$/g, '');
var { [method]: args } = expr.$value;
Object.assign(event, {
member: new MethodCallExpression(methodWithoutSign, args)
});
return;
}
if (instanceOf(expr, QueryField)) {
var member = expr.$name.split('.');
Object.assign(event, {
object: member[0],
member: member[1]
})
}
if (expr instanceof Expression) {
Object.assign(event, {
member: expr
})
}
}
}
// eslint-disable-next-line no-unused-vars
function resolveZeroOrOneJoinMember(target) {
/**
* This method tries to resolve a join member e.g. product.productDimensions
* when this member defines a zero-or-one association
*/
return function onResolvingZeroOrOneJoinMember(event) {
/**
* @type {Array<string>}
*/
// eslint-disable-next-line no-unused-vars
var fullyQualifiedMember = event.fullyQualifiedMember.split('.');
}
}
/**
* @param {DataQueryable} target
*/
function resolveMember(target) {
/**
* @param {member:string} event
*/
return function onResolvingMember(event) {
var collection = target.model.viewAdapter;
var member = event.member.replace(new RegExp('^' + collection + '.'), '');
/**
* @type {import('./types').DataAssociationMapping}
*/
var mapping = target.model.inferMapping(member);
if (mapping == null) {
return;
}
/**
* @type {import('./types').DataField}
*/
var attribute = target.model.getAttribute(member);
if (attribute.multiplicity === 'ZeroOrOne') {
var resolveMember = null;
if (mapping.associationType === 'junction' && mapping.parentModel === self.name) {
// expand child field
resolveMember = attribute.name.concat('/', mapping.childField);
} else if (mapping.associationType === 'junction' && mapping.childModel === self.name) {
// expand parent field
resolveMember = attribute.name.concat('/', mapping.parentField);
} else if (mapping.associationType === 'association' && mapping.parentModel === target.model.name) {
var associatedModel = target.model.context.model(mapping.childModel);
resolveMember = attribute.name.concat('/', associatedModel.primaryKey);
}
if (resolveMember) {
// resolve attribute
var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(target, resolveMember);
if (instanceOf(expr, QueryField)) {
event.member = expr.$name;
}
}
}
}
}
/**
* @classdesc Represents a dynamic query helper for filtering, paging, grouping and sorting data associated with an instance of DataModel class.
* @class
* @property {DataModel|*} model - Gets or sets the underlying data model
* @constructor
* @param model {DataModel|*}
* @augments DataContextEmitter
*/
function DataQueryable(model) {
/**
* @property DataQueryable#query
* @type {import('@themost/query').QueryExpression}
*/
/**
* @type {QueryExpression}
* @private
*/
var q = null;
/**
* Gets or sets an array of expandable models
* @type {Array}
* @private
*/
this.$expand = undefined;
/**
* @type {Boolean}
* @private
*/
this.$flatten = undefined;
/**
* @type {DataModel}
* @private
*/
var m = model;
Object.defineProperty(this, 'query', { get: function() {
if (!q) {
if (!m) {
return null;
}
q = QueryUtils.query(m.viewAdapter);
}
return q;
}, configurable:false, enumerable:false});
this.query
Object.defineProperty(this, 'model', { get: function() {
return m;
}, configurable:false, enumerable:false});
//get silent property
if (m)
this.silent(m.$silent);
}
/**
* Clones the current DataQueryable instance.
* @returns {DataQueryable|*} - The cloned object.
*/
DataQueryable.prototype.clone = function() {
var result = new DataQueryable(this.model);
//set view if any
result.$view = this.$view;
//set silent property
result.$silent = this.$silent;
//set silent property
result.$levels = this.$levels;
//set flatten property
result.$flatten = this.$flatten;
//set expand property
result.$expand = this.$expand;
//set query
_.assign(result.query, this.query);
return result;
};
/**
* Ensures data queryable context and returns the current data context. This function may be overriden.
* @returns {DataContext}
* @ignore
*/
DataQueryable.prototype.ensureContext = function() {
if (this.model!==null)
if (this.model.context!==null)
return this.model.context;
return null;
};
/**
* Serializes the underlying query and clears current filter expression for further filter processing. This operation may be used in complex filtering.
* @param {Boolean=} useOr - Indicates whether an or statement will be used in the resulted statement.
* @returns {DataQueryable}
* @example
//retrieve a list of order
context.model('Order')
.where('orderStatus').equal(1).and('paymentMethod').equal(2)
.prepare().where('orderStatus').equal(2).and('paymentMethod').equal(2)
.prepare(true)
//(((OrderData.orderStatus=1) AND (OrderData.paymentMethod=2)) OR ((OrderData.orderStatus=2) AND (OrderData.paymentMethod=2)))
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.prepare = function(useOr) {
this.query.prepare(useOr);
return this;
};
/**
* Initializes a where expression
* @param attr {string|*} - A string which represents the field name that is going to be used as the left operand of this expression
* @returns this
*/
DataQueryable.prototype.where = function(attr) {
// get arguments as array
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
try {
query.where.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
}
return this;
}
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr));
return this;
}
// check if attribute defines a many-to-many association
var mapping = this.model.inferMapping(attr);
if (mapping && mapping.associationType === 'junction') {
// append mapping id e.g. groups -> groups/id or members -> members/id etc
let attrId = attr + '/' + mapping.parentField;
if (mapping.parentModel === this.model.name) {
attrId = attr + '/' + mapping.childField;
}
this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attrId));
return this;
}
this.query.where(this.fieldOf(attr));
return this;
};
/**
* Initializes a full-text search expression
* @param {string} text - A string which represents the text we want to search for
* @returns {DataQueryable}
* @example
context.model('Person')
.search('Peter')
.select('description')
.take(25).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.search = function(text) {
var self = this;
// eslint-disable-next-line no-unused-vars
var options = { multiword:true };
var terms = [];
if (typeof text !== 'string') { return self; }
var re = /("(.*?)")|([^\s]+)/g;
var match = re.exec(text);
while(match) {
if (match[2]) {
terms.push(match[2]);
}
else {
terms.push(match[0]);
}
match = re.exec(text);
}
if (terms.length===0) {
return self;
}
self.prepare();
var stringTypes = [ 'Text', 'URL', 'Note' ];
self.model.attributes.forEach(function(x) {
if (x.many) { return; }
var mapping = self.model.inferMapping(x.name);
if (mapping) {
if ((mapping.associationType === 'association') && (mapping.childModel===self.model.name)) {
var parentModel = self.model.context.model(mapping.parentModel);
if (parentModel) {
parentModel.attributes.forEach(function(z) {
if (stringTypes.indexOf(z.type)>=0) {
terms.forEach(function (w) {
if (!/^\s+$/.test(w))
self.or(x.name + '/' + z.name).contains(w);
});
}
});
}
}
}
if (stringTypes.indexOf(x.type)>=0) {
terms.forEach(function (y) {
if (!/^\s+$/.test(y))
self.or(x.name).contains(y);
});
}
});
self.prepare();
return self;
};
DataQueryable.prototype.join = function(model)
{
var self = this;
if (_.isNil(model))
return this;
/**
* @type {DataModel}
*/
var joinModel = self.model.context.model(model);
//validate joined model
if (_.isNil(joinModel))
throw new Error(sprintf('The %s model cannot be found', model));
var arr = self.model.attributes.filter(function(x) { return x.type===joinModel.name; });
if (arr.length===0)
throw new Error(sprintf('An internal error occurred. The association between %s and %s cannot be found', this.model.name ,model));
var mapping = self.model.inferMapping(arr[0].name);
var expr = QueryUtils.query();
expr.where(self.fieldOf(mapping.childField)).equal(joinModel.fieldOf(mapping.parentField));
/**
* @type DataAssociationMapping
*/
var entity = new QueryEntity(joinModel.viewAdapter).left();
//set join entity (without alias and join type)
self.select().query.join(entity).with(expr);
return self;
};
/**
* Prepares a logical AND expression
* @param attr {string} - The name of field that is going to be used in this expression
* @returns {DataQueryable}
* @example
context.model('Order').where('customer').equal(298)
.and('orderStatus').equal(1)
.list().then(function(result) {
//SQL: WHERE ((OrderData.customer=298) AND (OrderData.orderStatus=1)
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.and = function(attr) {
if (typeof attr === 'string' && /\//.test(attr)) {
this.query.and(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr));
return this;
}
// check if attribute defines a many-to-many association
var mapping = this.model.inferMapping(attr);
if (mapping && mapping.associationType === 'junction') {
// append mapping id e.g. groups -> groups/id or members -> members/id etc
let attrId = attr + '/' + mapping.parentField;
if (mapping.parentModel === this.model.name) {
attrId = attr + '/' + mapping.childField;
}
this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attrId));
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;
}
// check if attribute defines a many-to-many association
var mapping = this.model.inferMapping(attr);
if (mapping && mapping.associationType === 'junction') {
// append mapping id e.g. groups -> groups/id or members -> members/id etc
let attrId = attr + '/' + mapping.parentField;
if (mapping.parentModel === this.model.name) {
attrId = attr + '/' + mapping.childField;
}
this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attrId));
return this;
}
this.query.or(this.fieldOf(attr));
return this;
};
/**
* @private
* @this DataQueryable
* @memberof DataQueryable#
* @param {*} obj
* @returns {*}
*/
function resolveValue(obj) {
var self = this;
if (typeof obj === 'string' && /^\$it\//.test(obj)) {
var attr = obj.replace(/^\$it\//,'');
if (DataAttributeResolver.prototype.testNestedAttribute(attr)) {
return DataAttributeResolver.prototype.resolveNestedAttribute.call(self, attr);
}
else {
attr = DataAttributeResolver.prototype.testAttribute(attr);
if (attr) {
return self.fieldOf(attr.name);
}
}
}
return obj;
}
/**
* Performs an equality comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders with order status equal to 1
context.model('Order').where('orderStatus').equal(1)
.list().then(function(result) {
//WHERE (OrderData.orderStatus=1)
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.equal = function(obj) {
// check if the given object is an array
if (Array.isArray(obj)) {
var resolver = new DataValueResolver(this);
// and resolve each value separately
this.query.equal(obj.map(function(value) {
return resolver.resolve(value);
}));
return this;
}
this.query.equal(new DataValueResolver(this).resolve(obj));
return this;
};
/**
* Performs an equality comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a person with id equal to 299
context.model('Person').where('id').is(299)
.first().then(function(result) {
//WHERE (PersonData.id=299)
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.is = function(obj) {
return this.equal(obj);
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares a not equal comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders with order status different than 1
context.model('Order')
.where('orderStatus').notEqual(1)
.orderByDescending('orderDate')
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.notEqual = function(obj) {
// check if the given object is an array
if (Array.isArray(obj)) {
var resolver = new DataValueResolver(this);
// and resolve each value separately
this.query.notEqual(obj.map(function(value) {
return resolver.resolve(value);
}));
return this;
}
// otherwise resolve the value
this.query.notEqual(new DataValueResolver(this).resolve(obj));
return this;
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares a greater than comparison.
* @param obj {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve a list of orders where product price is greater than 800
context.model('Order')
.where('orderedItem/price').greaterThan(800)
.orderByDescending('orderDate')
.select('id','orderedItem/name as productName', 'orderedItem/price as productPrice', 'orderDate')
.take(5)
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
id productName productPrice orderDate
--- -------------------------------------------- ------------ -----------------------------
304 Apple iMac (27-Inch, 2013 Version) 1336.27 2015-11-27 23:49:17.000+02:00
322 Dell B1163w Mono Laser Multifunction Printer 842.86 2015-11-27 20:16:52.000+02:00
167 Razer Blade (2013) 1553.43 2015-11-27 04:17:08.000+02:00
336 Apple iMac (27-Inch, 2013 Version) 1336.27 2015-11-26 07:25:35.000+02:00
89 Nvidia GeForce GTX 650 Ti Boost 1625.49 2015-11-21 17:29:21.000+02:00
*/
DataQueryable.prototype.greaterThan = function(obj) {
this.query.greaterThan(new DataValueResolver(this).resolve(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(new DataValueResolver(this).resolve(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(new DataValueResolver(this).resolve(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(new DataValueResolver(this).resolve(obj));
return this;
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares an ends with comparison
* @param obj {*} - The string to be searched for at the end of a field.
* @returns {DataQueryable}
* @example
//retrieve people whose given name starts with 'D'
context.model('Person')
.where('givenName').startsWith('D')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results:
id givenName familyName
--- --------- ----------
257 Daisy Lambert
275 Dustin Brooks
333 Dakota Gallagher
*/
DataQueryable.prototype.startsWith = function(obj) {
this.query.startsWith(obj);
return this;
};
// noinspection JSUnusedGlobalSymbols
/**
* Prepares an ends with comparison
* @param obj {*} - The string to be searched for at the end of a field.
* @returns {DataQueryable}
* @example
//retrieve people whose given name ends with 'y'
context.model('Person')
.where('givenName').endsWith('y')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //Results
id givenName familyName
--- --------- ----------
257 Daisy Lambert
287 Zachary Field
295 Anthony Berry
339 Brittney Hunt
341 Kimberly Wheeler
*/
DataQueryable.prototype.endsWith = function(obj) {
this.query.endsWith(obj);
return this;
};
/**
* Prepares a typical IN comparison.
* @param objs {Array} - An array of values which represents the values to be used in expression
* @returns {DataQueryable}
* @example
//retrieve orders with order status 1 or 2
context.model('Order').where('orderStatus').in([1,2])
.list().then(function(result) {
//WHERE (OrderData.orderStatus IN (1, 2))
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.in = function(objs) {
this.query.in(objs);
return this;
};
/**
* Prepares a typical NOT IN comparison.
* @param objs {Array} - An array of values which represents the values to be used in expression
* @returns {DataQueryable}
* @example
//retrieve orders with order status 1 or 2
context.model('Order').where('orderStatus').notIn([1,2])
.list().then(function(result) {
//WHERE (NOT OrderData.orderStatus IN (1, 2))
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataQueryable.prototype.notIn = function(objs) {
this.query.notIn(objs);
return this;
};
/**
* Prepares a modular arithmetic operation
* @param {*} obj The value to be compared
* @param {Number} result The result of modular expression
* @returns {DataQueryable}
*/
DataQueryable.prototype.mod = function(obj, result) {
this.query.mod(obj, result);
return this;
};
/**
* Prepares a contains comparison (e.g. a string contains another string).
* @param value {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve person where the given name contains
context.model('Person').select(['id','givenName','familyName'])
.where('givenName').contains('ex')
.list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id givenName familyName
--- --------- ----------
297 Alex Miles
353 Alexis Rees
*/
DataQueryable.prototype.contains = function(value) {
this.query.contains(value);
return this;
};
/**
* Prepares a not contains comparison (e.g. a string contains another string).
* @param value {*} - The right operand of the expression
* @returns {DataQueryable}
* @example
//retrieve persons where the given name not contains 'ar'
context.model('Person').select(['id','givenName','familyName'])
.where('givenName').notContains('ar')
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id givenName familyName
--- --------- ----------
257 Daisy Lambert
259 Peter French
261 Kylie Jordan
263 Maxwell Hall
265 Christian Marshall
*/
DataQueryable.prototype.notContains = function(value) {
this.query.notContains(value);
return this;
};
/**
* Prepares a comparison where the left operand is between two values
* @param {*} value1 - The minimum value
* @param {*} value2 - The maximum value
* @returns {DataQueryable}
* @example
//retrieve products where price is between 150 and 250
context.model('Product')
.where('price').between(150,250)
.take(5).list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
@example //The result set of this example may be:
id name model price
--- ------------------------------------------ ------ ------
367 Asus Transformer Book T100 HD2895 224.52
380 Zotac Zbox Nano XS AD13 Plus WC5547 228.05
384 Apple iPad Air ZE6015 177.44
401 Intel Core i7-4960X Extreme Edition SM5853 194.61
440 Bose SoundLink Bluetooth Mobile Speaker II HS5288 155.27
*/
DataQueryable.prototype.between = function(value1, value2) {
const resolver = new DataValueResolver(this);
this.query.between(resolver.resolve(value1), resolver.resolve(value2));
return this;
};
/**
* @this DataQueryable
* @memberOf DataQueryable#
* @param arg
* @returns {*}
* @private
*/
function select_(arg) {
var self = this;
if (typeof arg === 'string' && arg.length===0) {
return;
}
var a = DataAttributeResolver.prototype.testAggregatedNestedAttribute.call(self,arg);
if (a) {
return DataAttributeResolver.prototype.selectAggregatedAttribute.call(self, a.aggr , a.name, a.property);
}
else {
a = DataAttributeResolver.prototype.testNestedAttribute.call(self,arg);
if (a) {
return DataAttributeResolver.prototype.selectNestedAttribute.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 {...*} attr An array of fields, a field or a view name
* @returns {DataQueryable}
*/
DataQueryable.prototype.select = function(attr) {
var self = this;
var arr;
var expr;
var arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr;
// get arguments as array
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
var onResolvingMember = resolveMember(this);
query.resolvingMember.subscribe(onResolvingMember);
try {
query.select.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
query.resolvingMember.unsubscribe(onResolvingMember);
}
return this;
}
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, x.property));
}
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.selectNestedAttribute.call(self, b.name, x.property);
if (expr) { arr.push(expr); }
}
else {
b = DataAttributeResolver.prototype.testAttribute.call(self,name);
if (b) {
arr.push(self.fieldOf(b.name, x.property));
}
else if (/\./g.test(name)) {
name = name.split('.')[0];
arr.push(self.fieldOf(name));
}
else
{
arr.push(self.fieldOf(name));
}
}
}
}
});
}
//select a field from a joined entity
else {
expr = select_.call(self, arg);
if (expr) {
arr = arr || [];
arr.push(expr);
}
}
}
if (_.isArray(arr)) {
if (arr.length===0)
arr = null;
}
}
else {
//get array of attributes
if (_.isArray(arg)) {
arr = [];
//check if field is a data view
if (arg.length === 1 && typeof arg[0] === 'string') {
if (self.model.dataviews(arg[0])) {
return self.select(arg[0]);
}
}
arg.forEach(function(x) {
if (typeof x === 'string') {
field = self.model.field(x);
if (field) {
if (field.many || (field.mapping && field.mapping.associationType === 'junction')) {
self.expand({
'name':field.name,
'options':field.options
});
}
else {
arr.push(self.fieldOf(field.name));
}
}
//test nested attribute and simple attribute expression
else {
expr = select_.call(self, x);
if (expr) {
arr = arr || [];
arr.push(expr);
}
}
}
else {
//validate if x is an object (QueryField)
arr.push(x);
}
});
}
}
if (_.isNil(arr)) {
if (!self.query.hasFields()) {
// //enumerate fields
var fields = self.model.attributes.filter(function (x) {
return (x.many === false) || (_.isNil(x.many)) || ((x.expandable === true) && (self.getLevels()>0));
}).map(function(x) {
return x.property || x.name;
});
if (fields.length === 0) {
return this;
}
return self.select.apply(self, fields);
}
}
else {
self.query.select(arr);
}
return this;
};
// noinspection JSUnusedGlobalSymbols
DataQueryable.prototype.dateOf = function(attr) {
if (typeof attr ==='undefined' || attr === null)
return attr;
if (typeof attr !=='string')
return attr;
return this.fieldOf('date(' + attr + ')');
};
/**
* @param attr {string|*}
* @param alias {string=}
* @returns {DataQueryable|QueryField|*}
*/
DataQueryable.prototype.fieldOf = function(attr, alias) {
if (typeof attr ==='undefined' || attr === null)
return attr;
if (typeof attr !=='string')
return attr;
var matches = /(count|avg|sum|min|max)\((.*?)\)/i.exec(attr), res, field, aggr, prop;
if (matches) {
//get field
field = this.model.field(matches[2]);
//get aggregate function
aggr = matches[1].toLowerCase();
//test nested attribute aggregation
if (_.isNil(field) && /\//.test(matches[2])) {
//resolve nested attribute
var nestedAttr = DataAttributeResolver.prototype.resolveNestedAttribute.call(this, matches[2]);
//if nested attribute exists
if (nestedAttr) {
if (_.isNil(alias)) {
var nestedMatches = /as\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(attr);
alias = _.isNil(nestedMatches) ? aggr.concat('Of_', matches[2].replace(/\//g, '_')) : nestedMatches[1];
}
/**
* @type {Function}
*/
var fn = QueryField[aggr];
//return query field
return fn(nestedAttr.$name).as(alias);
}
}
if (typeof field === 'undefined' || field === null)
throw new Error(sprintf('The specified field %s cannot be found in target model.', matches[2]));
if (_.isNil(alias)) {
matches = /as\s([\u0020-\u007F\u0080-\uFFFF]+)$/i.exec(attr);
if (matches) {
alias = matches[1];
}
else {
alias = aggr.concat('Of', field.name);
}
}
if (aggr==='count')
return QueryField.count(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr==='avg')
return QueryField.average(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr==='sum')
return QueryField.sum(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr==='min')
return QueryField.min(field.name).from(this.model.viewAdapter).as(alias);
else if (aggr==='max')
return QueryField.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(sprintf('The specified field %s cannot be found in target model.', matches[2]));
if (_.isNil(alias)) {
matches = /as\s([\u0021-\u007F\u0080-\uFFFF]+)$/i.exec(attr);
if (matches) {
alias = matches[1];
}
}
prop = alias || field.property || field.name;
res[prop] = { }; res[prop]['$' + aggr] = [ QueryField.select(field.name).from(this.model.viewAdapter) ];
return res;
}
else {
//matches 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(sprintf('The specified field %s cannot be found in target model.', attr));
alias = matches[2];
prop = alias || field.property || field.name;
return QueryField.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(sprintf('The specified field %s cannot be found in target model.', attr));
var f = QueryField.select(field.name).from(this.model.viewAdapter);
if (alias) {
return f.as(alias);
}
else 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) {
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
try {
query.orderBy.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
}
return this;
}
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 {...*} attr - A param array of string that represents the attributes which are going to be used in group by expression
* @returns {DataQueryable}
*/
DataQueryable.prototype.groupBy = function(attr) {
var arr = [],
arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr;
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
try {
query.groupBy.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
}
return this;
}
if (_.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) {
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
try {
query.thenBy.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
}
return this;
}
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) {
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
try {
query.orderByDescending.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
}
return this;
}
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|*} attr The field name to use for sorting results
* @returns {DataQueryable}
*/
DataQueryable.prototype.thenByDescending = function(attr) {
var args = Array.from(arguments);
if (typeof args[0] === 'function') {
/**
* @type {import("@themost/query").QueryExpression}
*/
var query = this.query;
var onResolvingJoinMember = resolveJoinMember(this);
query.resolvingJoinMember.subscribe(onResolvingJoinMember);
try {
query.thenByDescending.apply(query, args);
} finally {
query.resolvingJoinMember.unsubscribe(onResolvingJoinMember);
}
return this;
}
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 occurred, or null otherwise. The second argument will contain the result.
* @returns {Promise|*}
* @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);
}
};
/**
* @this DataQueryable
* @private
* @memberOf DataQueryable#
* @param {Function} 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);
}
});
}
/**
* Executes the specified query and returns all objects which satisfy the specified criteria.
* @param {Function=} callback
* @returns {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);
}
};
/**
* @this DataQueryable
* @private
* @memberOf DataQueryable#
* @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;
};
/**
* @this DataQueryable
* @memberOf DataQueryable#
* @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 occurred, 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 o