UNPKG

@themost/data

Version:

MOST Web Framework Codename Blueshift - Data module

1,481 lines (1,429 loc) 102 kB
// 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