@themost/data
Version:
MOST Web Framework Codename Blueshift - Data module
1,199 lines (1,149 loc) • 131 kB
JavaScript
// 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