UNPKG

@themost/data

Version:

MOST Web Framework Codename Blueshift - Data module

1,199 lines (1,149 loc) 131 kB
// MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved var _ = require('lodash'); var {cloneDeep} = require('lodash'); var {sprintf} = require('sprintf-js'); var Symbol = require('symbol'); var pluralize = require('pluralize'); var async = require('async'); var {QueryUtils, MethodCallExpression, ObjectNameValidator} = require('@themost/query'); var {OpenDataParser} = require('@themost/query'); var types = require('./types'); var {DataAssociationMapping} = require('./types'); var dataListeners = require('./data-listeners'); var validators = require('./data-validator'); var dataAssociations = require('./data-associations'); var {DataNestedObjectListener} = require('./data-nested-object-listener'); var {DataReferencedObjectListener} = require('./data-ref-object-listener'); var {DataQueryable} = require('./data-queryable'); var {DataAttributeResolver} = require('./data-attribute-resolver'); var DataObjectAssociationListener = dataAssociations.DataObjectAssociationListener; var {DataModelView} = require('./data-model-view'); var {DataFilterResolver} = require('./data-filter-resolver'); var Q = require('q'); var {SequentialEventEmitter,Args} = require('@themost/common'); var {LangUtils} = require('@themost/common'); var {TraceUtils} = require('@themost/common'); var {DataError} = require('@themost/common'); var {DataConfigurationStrategy} = require('./data-configuration'); var {ModelClassLoaderStrategy} = require('./data-configuration'); var {ModuleLoaderStrategy} = require('@themost/common'); var mappingsProperty = Symbol('mappings'); var {DataPermissionEventListener} = require('./data-permission'); // eslint-disable-next-line no-unused-vars var {DataField} = require('./types'); var {ZeroOrOneMultiplicityListener} = require('./zero-or-one-multiplicity'); var {OnNestedQueryListener} = require('./OnNestedQueryListener'); var {OnExecuteNestedQueryable} = require('./OnExecuteNestedQueryable'); var {OnNestedQueryOptionsListener} = require('./OnNestedQueryOptionsListener'); var {hasOwnProperty} = require('./has-own-property'); var { SyncSeriesEventEmitter } = require('@themost/events'); require('@themost/promise-sequence'); var DataObjectState = types.DataObjectState; var { OnJsonAttribute } = require('./OnJsonAttribute'); var { isObjectDeep } = require('./is-object'); var { DataStateValidatorListener } = require('./data-state-validator'); var resolver = require('./data-expand-resolver'); var isArrayLikeObject = require('lodash/isArrayLikeObject'); /** * @this DataModel * @param {DataField} field * @private */ function inferTagMapping(field) { /** * @type {DataModel|*} */ var self = this; //validate field argument if (_.isNil(field)) { return; } var hasManyAttribute = Object.prototype.hasOwnProperty.call(field, 'many'); // if field does not have attribute 'many' if (hasManyAttribute === false) { // do nothing return; } // if field has attribute 'many' but it's false if (hasManyAttribute === true && field.many === false) { return; } //check if the type of the given field is a primitive data type //(a data type that is defined in the collection of data types) var dataType = self.context.getConfiguration().getStrategy(DataConfigurationStrategy).dataTypes[field.type]; if (_.isNil(dataType)) { return; } // get associated adapter name var associationAdapter = self.name.concat(_.upperFirst(field.name)); // get parent field var parentField = self.primaryKey; // mapping attributes var mapping = _.assign({}, { 'associationType': 'junction', 'associationAdapter': associationAdapter, 'cascade': 'delete', 'parentModel': self.name, 'parentField': parentField, 'refersTo': field.name }, field.mapping); // and return return new DataAssociationMapping(mapping); } /** * @this DataModel * @returns {*} */ function getImplementedModel() { if (_.isNil(this['implements'])) { return null; } if (typeof this.context === 'undefined' || this.context === null) throw new Error('The underlying data context cannot be empty.'); return this.context.model(this['implements']); } /** * @ignore * @class * @constructor * @augments QueryExpression */ function EmptyQueryExpression() { // } /** * @classdesc DataModel class extends a JSON data model and performs all data operations (select, insert, update and delete) in MOST Data Applications. <p> These JSON schemas are in config/models folder: </p> <pre class="prettyprint"><code> / + config + models - User.json - Group.json - Account.json ... </code></pre> <p class="pln"> The following JSON schema presents a typical User model with fields, views, privileges, constraints, listeners, and seeding: </p> <pre class="prettyprint"><code> { "name": "User", "id": 90, "title": "Application Users", "inherits": "Account", "hidden": false, "sealed": false, "abstract": false, "version": "1.4", "fields": [ { "name": "id", "title": "Id", "description": "The identifier of the item.", "type": "Integer", "nullable": false, "primary": true }, { "name": "accountType", "title": "Account Type", "description": "Contains a set of flags that define the type and scope of an account object.", "type": "Integer", "readonly":true, "value":"javascript:return 0;" }, { "name": "lockoutTime", "title": "Lockout Time", "description": "The date and time that this account was locked out.", "type": "DateTime", "readonly": true }, { "name": "logonCount", "title": "Logon Count", "description": "The number of times the account has successfully logged on.", "type": "Integer", "value": "javascript:return 0;", "readonly": true }, { "name": "enabled", "title": "Enabled", "description": "Indicates whether a user is enabled or not.", "type": "Boolean", "nullable": false, "value": "javascript:return true;" }, { "name": "lastLogon", "title": "Last Logon", "description": "The last time and date the user logged on.", "type": "DateTime", "readonly": true }, { "name": "groups", "title": "User Groups", "description": "A collection of groups where user belongs.", "type": "Group", "expandable": true, "mapping": { "associationAdapter": "GroupMembers", "parentModel": "Group", "parentField": "id", "childModel": "User", "childField": "id", "associationType": "junction", "cascade": "delete", "select": [ "id", "name", "alternateName" ] } }, { "name": "additionalType", "value":"javascript:return this.model.name;", "readonly":true }, { "name": "accountType", "value": "javascript:return 0;" } ], "privileges":[ { "mask":1, "type":"self", "filter":"id eq me()" }, { "mask":15, "type":"global", "account":"*" } ], "constraints":[ { "description": "User name must be unique across different records.", "type":"unique", "fields": [ "name" ] } ], "views": [ { "name":"list", "title":"Users", "fields":[ { "name":"id", "hidden":true }, { "name":"description" }, { "name":"name" }, { "name":"enabled" , "format":"yesno" }, { "name":"dateCreated", "format":"moment : 'LLL'" }, { "name":"dateModified", "format":"moment : 'LLL'" } ], "order":"dateModified desc" } ], "eventListeners": [ { "name":"New User Credentials Provider", "type":"/app/controllers/user-credentials-listener" } ], "seed":[ { "name":"anonymous", "description":"Anonymous User", "groups":[ { "name":"Guests" } ] }, { "name":"admin@example.com", "description":"Site Administrator", "groups":[ { "name":"Administrators" } ] } ] } </code></pre> * * @class * @property {string} classPath - Gets or sets a string which represents the path of the DataObject subclass associated with this model. * @property {string} name - Gets or sets a string that represents the name of the model. * @property {number} id - Gets or sets an integer that represents the internal identifier of the model. * @property {boolean} hidden - Gets or sets a boolean that indicates whether the current model is hidden or not. The default value is false. * @property {string} title - Gets or sets a title for this data model. * @property {boolean} sealed - Gets or sets a boolean that indicates whether current model is sealed or not. A sealed model cannot be migrated. * @property {boolean} abstract - Gets or sets a boolean that indicates whether current model is an abstract model or not. * @property {string} version - Gets or sets the version of this data model. * @property {string} type - Gets or sets an internal type for this model. * @property {DataCachingType|string} caching - Gets or sets a string that indicates the caching type for this model. The default value is none. * @property {string} inherits - Gets or sets a string that contains the model that is inherited by the current model. * @property {string} implements - Gets or sets a string that contains the model that is implemented by the current model. * @property {DataField[]} fields - Gets or sets an array that represents the collection of model fields. * @property {DataModelEventListener[]} eventListeners - Gets or sets an array that represents the collection of model listeners. * @property {Array} constraints - Gets or sets the array of constraints which are defined for this model * @property {DataModelView[]} views - Gets or sets the array of views which are defined for this model * @property {DataModelPrivilege[]} privileges - Gets or sets the array of privileges which are defined for this model * @property {string} source - Gets or sets a string which represents the source database object for this model. * @property {string} view - Gets or sets a string which represents the view database object for this model. * @property {Array} seed - An array of objects which represents a collection of items to be seeded when the model is being generated for the first time * @constructor * @augments SequentialEventEmitter * @param {*=} obj An object instance that holds data model attributes. This parameter is optional. */ function DataModel(obj) { this.hidden = false; this.sealed = false; this.abstract = false; this.version = '0.1'; //this.type = 'data'; this.caching = 'none'; this.fields = []; this.eventListeners = []; this.constraints = []; this.views = []; this.privileges = []; //extend model if obj parameter is defined if (obj) { if (typeof obj === 'object') _.assign(this, obj); } /** * Gets or sets the underlying data adapter * @type {DataContext} * @private */ var context = null; var self = this; /** * @name DataModel#context * @type {DataContext|*} */ Object.defineProperty(this, 'context', { get: function () { return context; }, set: function (value) { context = value; // unregister listeners unregisterContextListeners.call(self); if (context != null) { registerContextListeners.call(self); } }, enumerable: false, configurable: false }); /** * @description Gets the database object associated with this data model * @name DataModel#sourceAdapter * @type {string} */ Object.defineProperty(this, 'sourceAdapter', { get: function() { return _.isString(self.source) ? self.source : self.name.concat('Base'); }, enumerable: false, configurable: false}); /** * @description Gets the database object associated with this data model view * @name DataModel#viewAdapter * @type {string} */ Object.defineProperty(this, 'viewAdapter', { get: function() { return _.isString(self.view) ? self.view : self.name.concat('Data'); }, enumerable: false, configurable: false}); var silent_ = false; /** * Prepares a silent data operation (for query, update, insert, delete etc). * In a silent execution, permission check will be omitted. * Any other listeners which are prepared for using silent execution will use this parameter. * @param {Boolean=} value * @returns DataModel */ this.silent = function(value) { if (typeof value === 'undefined') silent_ = true; else silent_ = !!value; return this; }; Object.defineProperty(this, '$silent', { get: function() { return silent_; }, enumerable: false, configurable: false}); /** * @type {Array} */ var attributes; /** * @description Gets an array of DataField objects which represents the collection of model fields (including fields which are inherited from the base model). * @name DataModel#attributes * @type {Array.<DataField>} */ Object.defineProperty(this, 'attributes', { get: function() { //validate self field collection if (typeof attributes !== 'undefined' && attributes !== null) return attributes; //init attributes collection attributes = []; //get base model (if any) var baseModel = self.base(), field; var implementedModel = getImplementedModel.bind(self)(); //enumerate fields var strategy = self.context.getConfiguration().getStrategy(DataConfigurationStrategy); self.fields.forEach(function(x) { if (typeof x.many === 'undefined') { if (typeof strategy.dataTypes[x.type] === 'undefined') //set one-to-many attribute (based on a naming convention) x.many = pluralize.isPlural(x.name) || (x.mapping && x.mapping.associationType === 'junction'); else //otherwise set one-to-many attribute to false x.many = false; } // define virtual attribute if (x.many) { // set multiplicity property EdmMultiplicity.Many if (Object.prototype.hasOwnProperty.call(x, 'multiplicity') === false) { x.multiplicity = 'Many'; } } if (x.nested) { // try to find if current field defines one-to-one association var mapping = x.mapping; if (mapping && mapping.associationType === 'association' && mapping.parentModel === self.name) { /** * get child model * @type {DataModel} */ var childModel = (mapping.childModel === self.name) ? self : self.context.model(mapping.childModel); // check child model constraints for one-to-one parent to child association if (childModel && childModel.constraints && childModel.constraints.length && childModel.constraints.find(function (constraint) { return constraint.type === 'unique' && constraint.fields && constraint.fields.length === 1 && constraint.fields.indexOf(mapping.childField) === 0; })) { // backward compatibility issue // set [many] attribute to true because is being used by query processing x.many = true; // set multiplicity property EdmMultiplicity.ZeroOrOne or EdmMultiplicity.One if (typeof x.nullable === 'boolean') { x.multiplicity = x.nullable ? 'ZeroOrOne' : 'One'; } else { x.multiplicity = 'ZeroOrOne'; } } } } //re-define field model attribute if (typeof x.model === 'undefined') x.model = self.name; var clone = x; // if base model exists and current field is not primary key field var isPrimary = !!x.primary; if (baseModel != null && isPrimary === false) { // get base field field = baseModel.field(x.name); if (field) { //clone field clone = { }; //get all inherited properties _.assign(clone, field); //get all overridden properties _.assign(clone, x); //set field model clone.model = field.model; //set cloned attribute clone.cloned = true; } } if (clone.insertable === false && clone.editable === false && clone.model === self.name) { clone.readonly = true; } //finally push field attributes.push(clone); }); if (baseModel) { baseModel.attributes.forEach(function(x) { if (!x.primary) { //check if member is overridden by the current model field = self.fields.find(function(y) { return y.name === x.name; }); if (typeof field === 'undefined') attributes.push(x); } else { //try to find primary key in fields collection var primaryKey = _.find(self.fields, function(y) { return y.name === x.name; }); if (typeof primaryKey === 'undefined') { //add primary key field primaryKey = _.assign({}, x, { 'type': x.type === 'Counter' ? 'Integer' : x.type, 'model': self.name, 'indexed': true, 'value': null, 'calculation': null }); delete primaryKey.value; delete primaryKey.calculation; attributes.push(primaryKey); } } }); } if (implementedModel) { implementedModel.attributes.forEach(function(x) { field = _.find(self.fields, function(y) { return y.name === x.name; }); if (_.isNil(field)) { attributes.push(_.assign({}, x, { model:self.name })); } }); } return attributes; }, enumerable: false, configurable: false}); /** * Gets the primary key name * @type String */ this.primaryKey = undefined; //local variable for DateModel.primaryKey var primaryKey_; Object.defineProperty(this, 'primaryKey' , { get: function() { return self.getPrimaryKey(); }, enumerable: false, configurable: false}); this.getPrimaryKey = function() { if (typeof primaryKey_ !== 'undefined') { return primaryKey_; } var p = self.attributes.find(function(x) { return x.primary===true; }); if (p) { primaryKey_ = p.name; return primaryKey_; } }; /** * Gets an array that contains model attribute names * @type Array */ this.attributeNames = undefined; Object.defineProperty(this, 'attributeNames' , { get: function() { return self.attributes.map(function(x) { return x.name; }); }, enumerable: false, configurable: false}); Object.defineProperty(this, 'constraintCollection' , { get: function() { var arr = []; if (_.isArray(self.constraints)) { //append constraints to collection self.constraints.forEach(function(x) { arr.push(x); }); } //get base model var baseModel = self.base(); if (baseModel) { //get base model constraints var baseArr = baseModel.constraintCollection; if (_.isArray(baseArr)) { //append to collection baseArr.forEach(function(x) { arr.push(x); }); } } return arr; }, enumerable: false, configurable: false}); //call initialize method if (typeof this.initialize === 'function') this.initialize(); } LangUtils.inherits(DataModel, SequentialEventEmitter); /** * Gets a boolean which indicates whether data model is in silent mode or not */ DataModel.prototype.isSilent = function() { return this.$silent; }; /** * @returns {Function} */ DataModel.prototype.getDataObjectType = function() { return this.context.getConfiguration().getStrategy(ModelClassLoaderStrategy).resolve(this); }; /** * Initializes the current data model. This method is used for extending the behaviour of an install of DataModel class. */ DataModel.prototype.initialize = function() { DataModel.load.emit({ target: this }); }; /** * Clones the current data model * @param {DataContext=} context - An instance of DataContext class which represents the current data context. * @returns {DataModel} Returns a new DataModel instance */ DataModel.prototype.clone = function(context) { // create new instance var cloned = new DataModel(cloneDeep(this)).silent(this.isSilent()); // set context or this model context cloned.context = context || this.context; return cloned; }; /** * @this DataModel * @private */ function unregisterContextListeners() { //unregister event listeners this.removeAllListeners('before.save'); this.removeAllListeners('after.save'); this.removeAllListeners('before.remove'); this.removeAllListeners('after.remove'); this.removeAllListeners('before.execute'); this.removeAllListeners('after.execute'); this.removeAllListeners('before.upgrade'); this.removeAllListeners('after.upgrade'); } /** * @this DataModel * @private */ function registerContextListeners() { //description: change default max listeners (10) to 64 in order to avoid node.js message // for reaching the maximum number of listeners //author: k.barbounakis@gmail.com if (typeof this.setMaxListeners === 'function') { this.setMaxListeners(64); } var CalculatedValueListener = dataListeners.CalculatedValueListener; var DefaultValueListener = dataListeners.DefaultValueListener; var DataCachingListener = dataListeners.DataCachingListener; var DataModelCreateViewListener = dataListeners.DataModelCreateViewListener; var DataModelSeedListener = dataListeners.DataModelSeedListener; //1. State validator listener this.on('before.save', DataStateValidatorListener.prototype.beforeSave); this.on('before.remove', DataStateValidatorListener.prototype.beforeRemove); //2. Default values Listener this.on('before.save', DefaultValueListener.prototype.beforeSave); //3. Calculated values listener this.on('before.save', CalculatedValueListener.prototype.beforeSave); //register before execute caching if (this.caching==='always' || this.caching==='conditional') { this.on('before.execute', DataCachingListener.prototype.beforeExecute); } this.on('before.execute', OnExecuteNestedQueryable.prototype.beforeExecute); this.on('before.execute', OnNestedQueryOptionsListener.prototype.beforeExecute); this.on('before.execute', OnNestedQueryListener.prototype.beforeExecute); //register after execute caching if (this.caching==='always' || this.caching==='conditional') { this.on('after.execute', DataCachingListener.prototype.afterExecute); } //migration listeners this.on('after.upgrade',DataModelCreateViewListener.prototype.afterUpgrade); this.on('after.upgrade',DataModelSeedListener.prototype.afterUpgrade); // json listener this.on('after.save', OnJsonAttribute.prototype.afterSave); this.on('after.execute', OnJsonAttribute.prototype.afterExecute); this.on('before.save', OnJsonAttribute.prototype.beforeSave); //get module loader /** * @type {ModuleLoader|*} */ var moduleLoader = this.context.getConfiguration().getStrategy(ModuleLoaderStrategy); //register configuration listeners if (this.eventListeners) { for (var i = 0; i < this.eventListeners.length; i++) { var listener = this.eventListeners[i]; //get listener type (e.g. type: require('./custom-listener.js')) if (listener.type && !listener.disabled) { /** * @type {{beforeSave?:function,afterSave?:function,beforeRemove?:function,afterRemove?:function,beforeExecute?:function,afterExecute?:function,beforeUpgrade?:function,afterUpgrade?:function}} */ var dataEventListener = moduleLoader.require(listener.type); if (typeof dataEventListener.beforeUpgrade === 'function') this.on('before.upgrade', dataEventListener.beforeUpgrade); if (typeof dataEventListener.beforeSave === 'function') this.on('before.save', dataEventListener.beforeSave); if (typeof dataEventListener.afterSave === 'function') this.on('after.save', dataEventListener.afterSave); if (typeof dataEventListener.beforeRemove === 'function') this.on('before.remove', dataEventListener.beforeRemove); if (typeof dataEventListener.afterRemove === 'function') this.on('after.remove', dataEventListener.afterRemove); if (typeof dataEventListener.beforeExecute === 'function') this.on('before.execute', dataEventListener.beforeExecute); if (typeof dataEventListener.afterExecute === 'function') this.on('after.execute', dataEventListener.afterExecute); if (typeof dataEventListener.afterUpgrade === 'function') this.on('after.upgrade', dataEventListener.afterUpgrade); } } } //before execute this.on('before.execute', DataPermissionEventListener.prototype.beforeExecute); } DataModel.prototype.join = function(model) { var result = new DataQueryable(this); return result.join(model); }; /** * Initializes a where statement and returns an instance of DataQueryable class. * @param {String|*} attr - A string that represents the name of a field * @returns DataQueryable */ // eslint-disable-next-line no-unused-vars DataModel.prototype.where = function(attr) { var result = new DataQueryable(this); return result.where.apply(result, Array.from(arguments)); }; /** * Initializes a full-text search statement and returns an instance of DataQueryable class. * @param {String} text - A string that represents the text to search for * @returns DataQueryable */ DataModel.prototype.search = function(text) { var result = new DataQueryable(this); return result.search(text); }; /** * Returns a DataQueryable instance of the current model * @returns {DataQueryable} */ DataModel.prototype.asQueryable = function() { return new DataQueryable(this); }; /** * @private * @this DataModel * @param {*} params * @param {Function} callback * @returns {*} */ function filterInternal(params, callback) { var self = this; var parser = OpenDataParser.create() var $joinExpressions = []; // issue #226: enable getting distinct values var $distinct = false; var view; var selectAs = []; parser.resolveMember = function(member, cb) { // resolve view var attr = self.field(member); if (attr) { member = attr.name; if (attr.multiplicity === 'ZeroOrOne') { var mapping1 = self.inferMapping(member); if (mapping1 && mapping1.associationType === 'junction' && mapping1.parentModel === self.name) { member = attr.name.concat('/', mapping1.childField); selectAs.push({ member: attr.name.concat('.', mapping1.childField), alias: attr.name }); } else if (mapping1 && mapping1.associationType === 'junction' && mapping1.childModel === self.name) { member = attr.name.concat('/', mapping1.parentField); selectAs.push({ member: attr.name.concat('.', mapping1.parentField), alias: attr.name }); } else if (mapping1 && mapping1.associationType === 'association' && mapping1.parentModel === self.name) { var associatedModel = self.context.model(mapping1.childModel); const primaryKey = associatedModel.attributes.find((x) => x.primary === true); member = attr.name.concat('/', primaryKey.name); selectAs.push({ member: attr.name.concat('.', primaryKey.name), alias: attr.name }); } } } if (DataAttributeResolver.prototype.testNestedAttribute.call(self,member)) { try { var member1 = member.split('/'), mapping = self.inferMapping(member1[0]), expr; if (mapping && mapping.associationType === 'junction') { var expr1 = DataAttributeResolver.prototype.resolveJunctionAttributeJoin.call(self, member); expr = { $expand: expr1.$expand }; if (expr1.$distinct) { $distinct = true; } //replace member expression member = expr1.$select.$name.replace(/\./g,'/'); } else { expr = DataAttributeResolver.prototype.resolveNestedAttributeJoin.call(self, member); if (expr.$distinct) { $distinct = true; } // get member expression if (expr && expr.$select && Object.prototype.hasOwnProperty.call(expr.$select, '$value')) { // get value var {$value} = expr.$select; // get first property var [property] = Object.keys($value); // check if property starts with $ (e.g. $concat, $jsonGet etc) if (property && property.startsWith('$')) { // get arguments var {[property]: args} = $value; // create method call expression member = new MethodCallExpression(property.substring(1), args); } else { return cb(new Error('Invalid member expression. Expected a method call expression.')); } } else if (expr && expr.$select) { member = expr.$select.$name.replace(/\./g, '/'); } } if (expr && expr.$expand) { var arrExpr = []; if (_.isArray(expr.$expand)) { arrExpr.push.apply(arrExpr, expr.$expand); } else { arrExpr.push(expr.$expand); } arrExpr.forEach(function(y) { var joinExpr = $joinExpressions.find(function(x) { if (x.$entity && x.$entity.$as) { return (x.$entity.$as === y.$entity.$as); } return false; }); if (_.isNil(joinExpr)) $joinExpressions.push(y); }); } } catch (err) { cb(err); return; } } if (typeof self.resolveMember === 'function') { self.resolveMember.call(self, member, cb); } else { DataFilterResolver.prototype.resolveMember.call(self, member, cb); } }; parser.resolveMethod = function(name, args, cb) { if (typeof self.resolveMethod === 'function') { self.resolveMethod.call(self, name, args, cb); } else { DataFilterResolver.prototype.resolveMethod.call(self, name, args, cb); } }; var filter; if ((params instanceof DataQueryable) && (self.name === params.model.name)) { var q = new DataQueryable(self); _.assign(q, params); _.assign(q.query, params.query); return callback(null, q); } if (typeof params === 'string') { filter = params; } else if (typeof params === 'object') { filter = params.$filter; } try { var top = parseInt(params.$top || params.$take, 10); var skip = parseInt(params.$skip, 10); var levels = parseInt(params.$levels, 10) var queryOptions = { $filter: filter, $select: params.$select, $orderBy: params.$orderby || params.$orderBy || params.$order, $groupBy: params.$groupby || params.$groupBy || params.$group, $top: isNaN(top) ? 0 : top, $skip: isNaN(skip) ? 0 : skip, $levels: isNaN(levels) ? -1 : levels }; void parser.parseQueryOptions(queryOptions, /** * @param {Error=} err * @param {{$where?:*,$order?:*,$select?:*,$group?:*}} query * @returns {void} */ function(err, query) { try { if (err) { callback(err); } else { // create an instance of data queryable var q = new DataQueryable(self); if (query.$select) { if (q.query.$select == null) { q.query.$select = {}; } var collection = q.query.$collection; // validate the usage of a data view if (Array.isArray(query.$select) && query.$select.length === 1) { var reTrimCollection = new RegExp('^' + collection + '.', 'ig'); for (let index = 0; index < query.$select.length; index++) { var element = query.$select[index]; if (Object.prototype.hasOwnProperty.call(element, '$name')) { // get attribute name if (typeof element.$name === 'string') { view = self.dataviews(element.$name.replace(reTrimCollection, '')); if (view != null) { break; } } } } } // resolve a backward compatibility issue // convert select attributes which define an association to expandable attributes if (Array.isArray(query.$select)) { var removeCollectionRegex = new RegExp('^' + collection + '.', 'ig'); for (var index = 0; index < query.$select.length; index++) { var selectElement = query.$select[index]; if (Object.prototype.hasOwnProperty.call(selectElement, '$name')) { // get attribute name if (typeof selectElement.$name === 'string') { var selectAttributeName= selectElement.$name.replace(removeCollectionRegex, ''); var selectAttribute = self.getAttribute(selectAttributeName); if (selectAttribute && selectAttribute.many) { // expand attribute q.expand(selectAttributeName); // and query.$select.splice(index, 1); index -= 1; } } } } } if (view != null) { // select view q.select(view.name) } else { if (Array.isArray(query.$select)) { // validate aliases found by resolveMember if (selectAs.length > 0) { for (let index = 0; index < query.$select.length; index++) { var element1 = query.$select[index]; if (Object.prototype.hasOwnProperty.call(element1, '$name')) { if (typeof element1.$name === 'string') { var item = selectAs.find(function(x) { return x.member === element1.$name; }); if (item != null) { // add original name as alias Object.defineProperty(element1, item.alias, { configurable: true, enumerable: true, value: { $name: element1.$name } }); // and delete $name property delete element1.$name; } } } } } } // otherwise, format $select attribute Object.defineProperty(q.query.$select, collection, { configurable: true, enumerable: true, writable: true, value: query.$select }); } } if (query.$where) { q.query.$where = query.$where; } if (query.$order) { q.query.$order = query.$order; } if (query.$group) { q.query.$group = query.$group; } else { // issue #226: enable getting distinct values if ($distinct) { q.distinct(); } } // assign join expressions if ($joinExpressions.length>0) { // concat expand expressions if there are any var queryExpand = []; if (Array.isArray(q.query.$expand)) { queryExpand = q.query.$expand.slice(); } else if (typeof q.query.$expand === 'object') { queryExpand = [].concat(q.query.$expand) } // enumerate already defined expand expressions // this operation is very important when selecting items from a view queryExpand.forEach(function(expandExpr) { // find join expression by entity alias var joinExpr = $joinExpressions.find(function(x) { if (x.$entity && x.$entity.$as) { return (x.$entity.$as === expandExpr.$entity.$as); } return false; }); // if join expression is not defined then add it if (joinExpr == null) { $joinExpressions.push(expandExpr) } }); // finally assign join expressions q.query.$expand = $joinExpressions; } // prepare query q.query.prepare(); // set levels if (queryOptions.$levels >= 0) { q.levels(queryOptions.$levels); } if (queryOptions.$top > 0) { q.take(queryOptions.$top); } if (queryOptions.$skip > 0) { q.skip(queryOptions.$skip); } // set caching if (typeof params === 'object' && params.$cache === true && self.caching === 'conditional') { q.cache(true); } // set expand if (typeof params === 'object' && params.$expand != null) { var matches = resolver.testExpandExpression(params.$expand); if (matches && matches.length>0) { q.expand.apply(q, matches); } } return callback(null, q); } } catch (error) { return callback(error); } }); } catch(e) { return callback(e); } } /** * Applies open data filter, ordering, grouping and paging params and returns a data queryable object * @param {String|{$filter:string=, $skip:number=, $levels:number=, $top:number=, $take:number=, $order:string=, $inlinecount:string=, $expand:string=,$select:string=, $orderby:string=, $group:string=, $groupby:string=}} params - A string that represents an open data filter or an object with open data parameters * @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 an instance of DataQueryable class. * @returns Promise<DataQueryable>|* * @example context.model('Order').filter(context.params, function(err,q) { if (err) { return callback(err); } q.take(10, function(err, result) { if (err) { return callback(err); } callback(null, result); }); }); */ DataModel.prototype.filter = function(params, callback) { if (typeof callback === 'function') { return filterInternal.bind(this)(params, callback); } else { return Q.nbind(filterInternal, this)(params); } }; DataModel.prototype.filterAsync = function(params) { return this.filter(params); }; /** * Prepares a data query with the given object as parameters and returns the equivalent DataQueryable instance * @param {*} obj - An object which represents the query parameters * @returns DataQueryable - An instance of DataQueryable class that represents a data query based on the given parameters. * @example context.model('Order').find({ "paymentMethod":1 }).orderBy('dateCreated').take(10, function(err,result) { if (err) { return callback(err); } return callback(null, result); }); */ DataModel.prototype.find = function(obj) { var self = this, result; if (_.isNil(obj)) { result = new DataQueryable(this); result.where(self.primaryKey).equal(null); return result; } var find = { }, findSet = false; if (_.isObject(obj)) { if (hasOwnProperty(obj, self.primaryKey)) { find[self.primaryKey] = obj[self.primaryKey]; findSet = true; } else { //get unique constraint var constraint = _.find(self.constraints, function(x) { return x.type === 'unique'; }); //find by constraint if (_.isObject(constraint) && _.isArray(constraint.fields)) { //search for all constrained fields var findAttrs = {}, constrained = true; _.forEach(constraint.fields, function(x) { if (hasOwnProperty(obj, x)) { findAttrs[x] = obj[x]; } else { constrained = false; } }); if (constrained) { _.assign(find, findAttrs); findSet = true; } } } } else { find[self.primaryKey] = obj; findSet = true; } if (!findSet) { _.forEach(self.attributeNames, function(x) { if (hasOwnProperty(obj, x)) { find[x] = obj[x]; } }); } result = new DataQueryable(this); findSet = false; //enumerate properties and build query for(var key in find) { if (hasOwnProperty(find, key)) { if (!findSet) { result.where(key).equal(find[key]); findSet = true; } else result.and(key).equal(find[key]); } } if (!findSet) { //there is no query defined a dummy one (e.g. primary key is null) result.where(self.primaryKey).equal(null); } return result; }; /** * Selects the given attribute or attributes and return an instance of DataQueryable class * @param {...string} attr - An array of fields, a field or a view name * @returns {DataQueryable} */ // eslint-disable-next-line no-unused-vars DataModel.prototype.select = function(attr) { var result = new DataQueryable(this); return result.select.apply(result, Array.prototype.slice.call(arguments)); }; /** * Prepares an ascending order by expression and returns an instance of DataQueryable class. * @param {*} attr - A string that is going to be used in this expression. * @returns DataQueryable }); */ // eslint-disable-next-line no-unused-vars DataModel.prototype.orderBy = function(attr) { var result = new DataQueryable(this); return result.orderBy.apply(result, Array.from(arguments)); }; /** * Takes an array of maximum [n] items. * @param {Number} n - The maximum number of items that is going to be retrieved * @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|undefined If callback parameter is missing then returns a DataQueryable object. */ DataModel.prototype.take = function(n, callback) { n = n || 25; var result = new DataQueryable(this); if (typeof callback === 'undefined') return result.take(n); result.take(n, callback); }; /** * Returns an instance of DataResultSet of the current model. * @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<T>|*} If callback parameter is missin