@themost/data
Version:
MOST Web Framework 2.0 - ORM module
1,285 lines (1,218 loc) • 117 kB
JavaScript
/**
* @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 _ = require("lodash");
var sprintf = require('sprintf').sprintf;
var Symbol = require('symbol');
var path = require("path");
var pluralize = require("pluralize");
var async = require('async');
var QueryUtils = require('@themost/query/utils').QueryUtils;
var OpenDataParser = require('@themost/query/odata').OpenDataParser;
var types = require('./types');
var DataAssociationMapping = require('./types').DataAssociationMapping;
var dataListeners = require('./data-listeners');
var validators = require('./data-validator');
var dataAssociations = require('./data-associations');
var DataNestedObjectListener = require("./data-nested-object-listener").DataNestedObjectListener;
var DataReferencedObjectListener = require("./data-ref-object-listener").DataReferencedObjectListener;
var DataQueryable = require('./data-queryable').DataQueryable;
var DataAttributeResolver = require('./data-queryable').DataAttributeResolver;
var DataObjectAssociationListener = dataAssociations.DataObjectAssociationListener;
var DataModelView = require('./data-model-view').DataModelView;
var DataFilterResolver = require('./data-filter-resolver').DataFilterResolver;
var Q = require("q");
var SequentialEventEmitter = require("@themost/common/emitter").SequentialEventEmitter;
var LangUtils = require("@themost/common/utils").LangUtils;
var TraceUtils = require("@themost/common/utils").TraceUtils;
var DataError = require("@themost/common/errors").DataError;
var DataConfigurationStrategy = require('./data-configuration').DataConfigurationStrategy;
var ModelClassLoaderStrategy = require('./data-configuration').ModelClassLoaderStrategy;
var ModuleLoader = require('@themost/common/config').ModuleLoaderStrategy;
var mappingsProperty = Symbol('mappings');
var DataPermissionEventListener = require('./data-permission').DataPermissionEventListener;
var DataField = require('./types').DataField;
/**
* @this DataModel
* @param {DataField} field
* @private
*/
function inferTagMapping_(field) {
/**
* @type {DataModel|*}
*/
var self = this;
//validate field argument
if (_.isNil(field)) {
return;
}
//validate DataField.many attribute
if (!(field.hasOwnProperty('many') && field.many === true)) {
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 model name
var name = self.name.concat(_.upperFirst(field.name));
var primaryKey = self.key();
return new DataAssociationMapping({
"associationType": "junction",
"associationAdapter": name,
"cascade": "delete",
"parentModel": self.name,
"parentField": primaryKey.name,
"refersTo": field.name,
"privileges": field.mapping && field.mapping.privileges
});
}
/**
* @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;
if (_.isNil(context_)) {
unregisterContextListeners.bind(this)();
}
else {
registerContextListeners.bind(this)();
}
}, 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
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.find(function (constraint) {
return constraint.fields && constraint.fields.length === 1 && constraint.fields.indeOf(x.name) === 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
if (baseModel && !x.primary) {
//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;
}
}
//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() {
//
};
/**
* 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) {
var result = new DataModel(this);
if (context)
result.context = context;
return result;
};
/**
* @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('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;
var DataStateValidatorListener = require('./data-state-validator').DataStateValidatorListener;
//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);
}
//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);
//get module loader
/**
* @type {ModuleLoader|*}
*/
var moduleLoader = this.context.getConfiguration().getStrategy(ModuleLoader);
//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 DataEventListener
*/
var dataEventListener;
if (/^@themost\/data\//i.test(listener.type)) {
dataEventListener = moduleLoader.require(listener.type);
//dataEventListener = require(listener.type.replace(/^@themost\/data\//,'./'));
}
else {
dataEventListener = moduleLoader.require(listener.type);
}
//if listener exports beforeSave function then register this as before.save event listener
if (typeof dataEventListener.beforeSave === 'function')
this.on('before.save', dataEventListener.beforeSave);
//if listener exports afterSave then register this as after.save event listener
if (typeof dataEventListener.afterSave === 'function')
this.on('after.save', dataEventListener.afterSave);
//if listener exports beforeRemove then register this as before.remove event listener
if (typeof dataEventListener.beforeRemove === 'function')
this.on('before.remove', dataEventListener.beforeRemove);
//if listener exports afterRemove then register this as after.remove event listener
if (typeof dataEventListener.afterRemove === 'function')
this.on('after.remove', dataEventListener.afterRemove);
//if listener exports beforeExecute then register this as before.execute event listener
if (typeof dataEventListener.beforeExecute === 'function')
this.on('before.execute', dataEventListener.beforeExecute);
//if listener exports afterExecute then register this as after.execute event listener
if (typeof dataEventListener.afterExecute === 'function')
this.on('after.execute', dataEventListener.afterExecute);
//if listener exports afterUpgrade then register this as after.upgrade event listener
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
*/
DataModel.prototype.where = function(attr) {
var result = new DataQueryable(this);
return result.where(attr);
};
/**
* 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(), $joinExpressions = [], view;
if (typeof params !== 'undefined' && params !== null && typeof params.$select === 'string') {
//split select
var arr = params.$select.split(',');
if (arr.length===1) {
//try to get data view
view = self.dataviews(arr[0]);
}
}
parser.resolveMember = function(member, cb) {
if (view) {
var field = view.fields.find(function(x) { return x.property === member });
if (field) { member = field.name; }
}
var attr = self.field(member);
if (attr)
member = 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 = expr1.$expand;
//replace member expression
member = expr1.$select.$name.replace(/\./g,"/");
}
else {
expr = DataAttributeResolver.prototype.resolveNestedAttributeJoin.call(self, member);
}
if (expr) {
var arrExpr = [];
if (_.isArray(expr))
arrExpr.push.apply(arrExpr, expr);
else
arrExpr.push(expr);
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 {
parser.parse(filter, function(err, query) {
if (err) {
callback(err);
}
else {
//create a DataQueryable instance
var q = new DataQueryable(self);
q.query.$where = query;
if ($joinExpressions.length>0)
q.query.$expand = $joinExpressions;
//prepare
q.query.prepare();
if (typeof params === 'object') {
//apply query parameters
var select = params.$select,
skip = params.$skip || 0,
orderBy = params.$orderby || params.$order,
groupBy = params.$groupby || params.$group,
expand = params.$expand,
levels = parseInt(params.$levels),
top = params.$top || params.$take;
//select fields
if (typeof select === 'string') {
q.select.apply(q, select.split(',').map(function(x) {
return x.replace(/^\s+|\s+$/g, '');
}));
}
//apply group by fields
if (typeof groupBy === 'string') {
q.groupBy.apply(q, groupBy.split(',').map(function(x) {
return x.replace(/^\s+|\s+$/g, '');
}));
}
if ((typeof levels === 'number') && !isNaN(levels)) {
//set expand levels
q.levels(levels);
}
//set $skip
q.skip(skip);
if (top)
q.query.take(top);
//set caching
if (params.$cache && self.caching === 'conditional') {
q.cache(true);
}
//set $orderby
if (orderBy) {
orderBy.split(',').map(function(x) {
return x.replace(/^\s+|\s+$/g, '');
}).forEach(function(x) {
if (/\s+desc$/i.test(x)) {
q.orderByDescending(x.replace(/\s+desc$/i, ''));
}
else if (/\s+asc/i.test(x)) {
q.orderBy(x.replace(/\s+asc/i, ''));
}
else {
q.orderBy(x);
}
});
}
if (expand) {
var resolver = require("./data-expand-resolver");
var matches = resolver.testExpandExpression(expand);
if (matches && matches.length>0) {
q.expand.apply(q, matches);
}
}
//return
callback(null, q);
}
else {
//and finally return DataQueryable instance
callback(null, q);
}
}
});
}
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);
}
};
/**
* 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 (obj.hasOwnProperty(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 (obj.hasOwnProperty(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 (obj.hasOwnProperty(x)) {
find[x] = obj[x];
}
});
}
result = new DataQueryable(this);
findSet = false;
//enumerate properties and build query
for(var key in find) {
if (find.hasOwnProperty(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 {string|*} attr - A string that is going to be used in this expression.
* @returns DataQueryable
* @example
context.model('Person').orderBy('givenName').list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataModel.prototype.orderBy = function(attr) {
var result = new DataQueryable(this);
return result.orderBy(attr);
};
/**
* 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 missing then returns a Promise object.
* @deprecated Use DataModel.asQueryable().list().
* @example
context.model('User').list(function(err, result) {
if (err) { return done(err); }
return done(null, result);
});
*/
DataModel.prototype.list = function(callback) {
var result = new DataQueryable(this);
return result.list(callback);
};
/**
* @returns {Promise|*}
*/
DataModel.prototype.getList = function() {
var result = new DataQueryable(this);
return result.list();
};
/**
* Returns the first item 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 missing then returns a Promise object.
* @deprecated Use DataModel.asQueryable().first().
* @example
context.model('User').first(function(err, result) {
if (err) { return done(err); }
return done(null, result);
});
*/
DataModel.prototype.first = function(callback) {
var result = new DataQueryable(this);
return result.select.apply(result,this.attributeNames).first(callback);
};
/**
* A helper function for getting an object based on the given primary key value
* @param {String|*} key - The primary key value to search for.
* @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, if any.
* @returns {Deferred|*} If callback parameter is missing then returns a Deferred object.
* @example
context.model('User').get(1).then(function(result) {
return done(null, result);
}).catch(function(err) {
return done(err);
});
*/
DataModel.prototype.get = function(key, callback) {
var result = new DataQueryable(this);
return result.where(this.primaryKey).equal(key).first(callback);
};
/**
* Returns the last item of the current model based.
* @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 missing then returns a Promise object.
* @example
context.model('User').last(function(err, result) {
if (err) { return done(err); }
return done(null, result);
});
*/
DataModel.prototype.last = function(callback) {
var result = new DataQueryable(this);
return result.orderByDescending(this.primaryKey).select.apply(result,this.attributeNames).first(callback);
};
/**
* Returns all data items.
* @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, if any.
*/
DataModel.prototype.all = function(callback) {
var result = new DataQueryable(this);
return result.select.apply(result, this.attributeNames).all(callback);
};
/**
* Bypasses a number of items based on the given parameter. This method is used in data paging operations.
* @param {Number} n - The number of items to skip.
* @returns DataQueryable
*/
DataModel.prototype.skip = function(n) {
var result = new DataQueryable(this);
return result.skip(n);
};
/**
* Prepares an descending order by expression and returns an instance of DataQueryable class.
* @param {string|*} attr - A string that is going to be used in this expression.
* @returns DataQueryable
* @example
context.model('Person').orderByDescending('givenName').list().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataModel.prototype.orderByDescending = function(attr) {
var result = new DataQueryable(this);
return result.orderBy(attr);
};
/**
* Returns the maximum value for a field.
* @param {string} attr - A string that represents the name of the field.
* @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 missing then returns a Promise object.
*/
DataModel.prototype.max = function(attr, callback) {
var result = new DataQueryable(this);
return result.max(attr, callback);
};
/**
* Returns the minimum value for a field.
* @param {string} attr - A string that represents the name of the field.
* @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 missing then returns a Promise object.
*/
DataModel.prototype.min = function(attr, callback) {
var result = new DataQueryable(this);
return result.min(attr, callback);
};
/**
* Gets a DataModel instance which represents the inherited data model of this item, if any.
* @returns {DataModel}
*/
DataModel.prototype.base = function()
{
if (_.isNil(this.inherits))
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.inherits);
};
/**
* @this DataModel
* @private
* @param {*} obj
*/
function convertInternal_(obj) {
var self = this;
//get type parsers (or default type parsers)
var parsers = self.parsers || types.parsers, parser, value;
self.attributes.forEach(function(x) {
value = obj[x.name];
if (value) {
//get parser for this type
parser = parsers['parse'.concat(x.type)];
//if a parser exists
if (typeof parser === 'function')
//parse value
obj[x.name] = parser(value);
else {
//get mapping
var mapping = self.inferMapping(x.name);
if (mapping) {
if ((mapping.associationType==='association') && (mapping.childModel===self.name)) {
var associatedModel = self.context.model(mapping.parentModel);
if (associatedModel) {
if (typeof value === 'object') {
//set associated key value (e.g. primary key value)
convertInternal_.call(associatedModel, value);
}
else {
var field = associatedModel.field(mapping.parentField);
if (field) {
//parse raw value
parser = parsers['parse'.concat(field.type)];
if (typeof parser === 'function')
obj[x.name] = parser(value);
}
}
}
}
}
}
}
});
}
function dasherize(data) {
if (typeof data === 'string')
{
return data.replace(/(^\s*|\s*$)/g, '').replace(/[_\s]+/g, '-').replace(/([A-Z])/g, '-$1').replace(/-+/g, '-').replace(/^-/,'').toLowerCase();
}
}
/**
* @this DataModel
* @returns {*}
* @constructor
* @private
*/
function getDataObjectClass_() {
var self = this;
var DataObjectClass = self['DataObjectClass'];
if (typeof DataObjectClass === 'undefined')
{
if (typeof self.classPath === 'string') {
DataObjectClass = require(self.classPath);
}
else {
//try to find class file with data model's name in lower case
// e.g. OrderDetail -> orderdetail-model.js (backward compatibility naming convention)
var classPath = path.join(process.cwd(),'app','models',self.name.toLowerCase().concat('-model.js'));
try {
DataObjectClass = require(classPath);
}
catch(e) {
if (e.code === 'MODULE_NOT_FOUND') {
try {
//if the specified class file was not found try to dasherize model name
// e.g. OrderDetail -> order-detail-model.js
classPath = path.join(process.cwd(),'app','models',dasherize(self.name).concat('-model.js'));
DataObjectClass = require(classPath);
}
catch(e) {
if (e.code === 'MODULE_NOT_FOUND') {
if (_.isNil(self.inherits)) {
//if , finally, we are unable to find class file, load default DataObject class
DataObjectClass = require('./data-object').DataObject;
}
else {
DataObjectClass = getDataObjectClass_.call(self.base());
}
}
else {
throw e;
}
}
}
else {
throw e;
}
}
}
//cache DataObject class property
/**
* @type {DataConfigurationStrategy}
*/
var strategy = self.context.getConfiguration().getStrategy(DataConfigurationStrategy);
var modelDefinition = strategy.getModelDefinition(self.name);
modelDefinition['DataObjectClass'] = self['DataObjectClass'] = DataObjectClass;
}
return DataObjectClass;
}
/**
* Converts an object or a collection of objects to the corresponding data object instance
* @param {Array|*} obj
* @param {boolean=} typeConvert - Forces property value conversion for each proper