UNPKG

@themost/data

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