/**
* MOST Web Framework
* A JavaScript Web Framework
* http://themost.io
* Created by Kyriakos Barbounakis<k.barbounakis@gmail.com> on 2014-10-13.
*
* Copyright (c) 2014, Kyriakos Barbounakis k.barbounakis@gmail.com
Anthi Oikonomou anthioikonomou@gmail.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MOST Web Framework nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @private
*/
var _ = require("lodash"),
util = require('util'),
path = require("path"),
fs = require("fs"),
async = require('async'),
events = require('events'),
qry = require('most-query'),
types = require('./types'),
functions = require('./functions'),
dataCommon = require('./data-common'),
dataListeners = require('./data-listeners'),
validators = require('./data-validator'),
dataAssociations = require('./data-associations'),
DataNestedObjectListener = require("./data-nested-object-listener").DataNestedObjectListener,
DataQueryable = require('./data-queryable').DataQueryable,
DataAttributeResolver = require('./data-queryable').DataAttributeResolver,
DataObjectAssociationListener = dataAssociations.DataObjectAssociationListener,
DataModelView = require('./data-model-view').DataModelView,
DataFilterResolver = require('./data-filter-resolver').DataFilterResolver,
Q = require("q");
/**
* @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 conf = self.context.getConfiguration(), dataType = conf.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 types.DataAssociationMapping({
"associationType": "junction",
"associationAdapter": name,
"cascade": "delete",
"parentModel": self.name,
"parentField": primaryKey.name,
"refersTo": field.name
});
}
/**
* @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 {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 {DataContext|*} - Gets or sets the data context of this model.
* @property {DataField[]} attributes - Gets an array of DataField objects which represents the collection of model fields (including fields which are inherited from the base 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 EventEmitter2
* @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')
util._extend(this, obj);
}
/**
* Gets or sets the underlying data adapter
* @type {DataContext}
* @private
*/
var context_ = null;
var self = this;
Object.defineProperty(this, 'context', { get: function() {
return context_;
}, set: function(value) {
context_ = value;
}, enumerable: false, configurable: false});
Object.defineProperty(this, 'sourceAdapter', { get: function() {
return dataCommon.isDefined(self.source) ? self.source : self.name.concat('Base');
}, enumerable: false, configurable: false});
Object.defineProperty(this, 'viewAdapter', { get: function() {
return dataCommon.isDefined(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});
var pluralExpression = /([a-zA-Z]+?)([e']s|[^aiou]s)$/;
/**
* @type {Array}
*/
var attributes;
/**
* @private
*/
this._clearAttributes = function() {
attributes = null;
};
/**
* Gets an array of objects that represents the collection of fields for this model.
* This collection contains the fields defined in the current model and its parent.
* @type {Array}
*
*/
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;
//enumerate fields
self.fields.forEach(function(x) {
if (typeof x.many === 'undefined') {
if (typeof self.context.getConfiguration().dataTypes[x.type] === 'undefined')
//set one-to-many attribute (based on a naming convention)
x.many = pluralExpression.test(x.name) || (x.mapping && x.mapping.associationType === 'junction');
else
//otherwise set one-to-many attribute to false
x.many = false;
}
//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
util._extend(clone, field);
//get all overriden properties
util._extend(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);
}
});
}
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.fields.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 (util.isArray(self.constraints)) {
//apend 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 (util.isArray(baseArr)) {
//apend to collection
baseArr.forEach(function(x) {
arr.push(x);
});
}
}
return arr;
}, enumerable: false, configurable: false});
//register listeners
registerListeners_.call(this);
//call initialize method
if (typeof this.initialize === 'function')
this.initialize();
}
util.inherits(DataModel, types.EventEmitter2);
/**
* 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;
};
/**
* @private
*/
function registerListeners_() {
//change: 2015-01-19
//description: change default max listeners (10) to 32 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(32);
}
var CalculatedValueListener = dataListeners.CalculatedValueListener,
DefaultValueListener = dataListeners.DefaultValueListener,
DataCachingListener = dataListeners.DataCachingListener,
DataModelCreateViewListener = dataListeners.DataModelCreateViewListener,
DataModelSeedListener = dataListeners.DataModelSeedListener,
DataModelSubTypesListener = dataListeners.DataModelSubTypesListener,
DataStateValidatorListener = require('./data-state-validator').DataStateValidatorListener;
//register system 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');
//0. Permission Event Listener
var perms = require('./data-permission');
//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);
/**
* change:8-Jun 2015
* description: Set lookup default listeners as obsolete.
*/
////register lookup model listeners
//if (this.type === 'lookup') {
// //after save (clear lookup caching)
// this.on('after.save', DataModelLookupCachingListener.afterSave);
// //after remove (clear lookup caching)
// this.on('after.remove', DataModelLookupCachingListener.afterRemove);
//}
//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)
{
/**
* Load event listener from the defined type
* @type DataEventListener
*/
var m = listener.type.indexOf('/')==0 ? require(path.join(process.cwd(), listener.type)) : require(listener.type);
//if listener exports beforeSave function then register this as before.save event listener
if (typeof m.beforeSave == 'function')
this.on('before.save', m.beforeSave);
//if listener exports afterSave then register this as after.save event listener
if (typeof m.afterSave == 'function')
this.on('after.save', m.afterSave);
//if listener exports beforeRemove then register this as before.remove event listener
if (typeof m.beforeRemove == 'function')
this.on('before.remove', m.beforeRemove);
//if listener exports afterRemove then register this as after.remove event listener
if (typeof m.afterRemove == 'function')
this.on('after.remove', m.afterRemove);
//if listener exports beforeExecute then register this as before.execute event listener
if (typeof m.beforeExecute == 'function')
this.on('before.execute', m.beforeExecute);
//if listener exports afterExecute then register this as after.execute event listener
if (typeof m.afterExecute == 'function')
this.on('after.execute', m.afterExecute);
//if listener exports afterUpgrade then register this as after.upgrade event listener
if (typeof m.afterUpgrade == 'function')
this.on('after.upgrade', m.afterUpgrade);
}
}
}
//before execute
this.on('before.execute', perms.DataPermissionEventListener.prototype.beforeExecute);
//before save (validate permissions)
this.on('before.save', perms.DataPermissionEventListener.prototype.beforeSave);
//before remove (validate permissions)
this.on('before.remove', perms.DataPermissionEventListener.prototype.beforeRemove);
}
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);
};
/**
* 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(Error=,DataQueryable=)} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain an instance of DataQueryable class.
* @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) {
var self = this;
var parser = qry.openData.createParser(), $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 (util.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);
util._extend(q, params);
util._extend(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 $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);
}
};
/**
* 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;
}
//cast object
var find = {};
self.attributeNames.forEach(function(x)
{
if (obj.hasOwnProperty(x))
{
find[x] = obj[x];
}
});
if (find.hasOwnProperty(self.primaryKey)) {
result = new DataQueryable(this);
return result.where(self.primaryKey).equal(find[self.primaryKey]);
}
else {
result = new DataQueryable(this);
var bQueried = false;
//enumerate properties and build query
for(var key in find) {
if (find.hasOwnProperty(key)) {
if (!bQueried) {
result.where(key).equal(find[key]);
bQueried = true;
}
else
result.and(key).equal(find[key]);
}
}
if (!bQueried) {
//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}
*/
DataModel.prototype.select = function(attr) {
var result = new DataQueryable(this);
return result.select.apply(result, 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 occured, 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;
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 occured, 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 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 occured, 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(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 occured, 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 occured, 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(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 occured, 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(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 occured, 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 occured, 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 (typeof this.inherits === 'undefined' || this.inherits == null)
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);
};
/**
* @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);
}
}
}
}
}
}
}
});
}
/**
* @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',dataCommon.dasherize(self.name).concat('-model.js'));
DataObjectClass = require(classPath);
}
catch(e) {
if (e.code === 'MODULE_NOT_FOUND') {
if (typeof self.inherits === 'undefined' || self.inherits == null) {
//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
self.context.getConfiguration().models[self.name]['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 property based on field type.
* @returns {DataObject|Array|*} - Returns an instance of DataObject (or an array of DataObject instances)
*<p>
This conversion of an anonymous object through DataModel.convert() may be overriden by subclassing DataObject
and place this class in app/models folder of a MOST Data Appllication:
</p>
<pre class="prettyprint"><code>
/
+ app
+ models
+ user-model.js
</code></pre>
<p>
An example of user model subclassing (user-model.js):
</p>
<pre class="prettyprint"><code>
var util = require('util'),
md = require('most-data'),
web = require('most-web');
function UserModel(obj) {
UserModel.super_.call(this, 'User', obj);
}
util.inherits(UserModel, md.classes.DataObject);
UserModel.prototype.person = function (callback) {
var self = this, context = self.context;
try {
//search person by user name
return context.model('Person').where('user/name').equal(self.name).first(callback);
}
catch (err) {
callback(err);
}
};
if (typeof module !== 'undefined') module.exports = UserModel;
</code></pre>
@example
//get User model
var users = context.model('User');
users.where('name').equal(context.user.name).first().then(function(result) {
if (md.common.isNullOrUndefined(result)) {
return done(new Error('User cannot be found'));
}
//convert result
var user = users.convert(result);
//get user's person
user.person(function(err, result) {
if (err) { return done(err); }
if (md.common.isNullOrUndefined(result)) {
return done(new Error('Person cannot be found'));
}
console.log('Person: ' + JSON.stringify(result));
done(null, result);
});
}).catch(function(err) {
done(err);
});
*/
DataModel.prototype.convert = function(obj, typeConvert)
{
var self = this;
if (_.isNil(obj))
return obj;
/**
* @constructor
* @augments DataObject
* @ignore
*/
var DataObjectClass = getDataObjectClass_.call(self);
if (util.isArray(obj)) {
var arr = [], src;
obj.forEach(function(x) {
if (typeof x !== 'undefined' && x!=null) {
var o = new DataObjectClass();
if (typeof x === 'object') {
util._extend(o, x);
}
else {
src = {}; src[self.primaryKey] = x;
util._extend(o, src);
}
if (typeConvert)
convertInternal_.call(self, o);
o.context = self.context;
o.$$type = self.name;
arr.push(o);
}
});
return arr;
}
else {
var result = new DataObjectClass();
if (typeof obj === 'object') {
util._extend(result, obj);
}
else {
src = {}; src[self.primaryKey] = obj;
util._extend(result, src);
}
if (typeConvert)
convertInternal_.call(self, result);
result.context = self.context;
result.$$type = self.name;
return result;
}
};
/**
* Extracts an identifier from the given parameter.
* If the parameter is an object then gets the identifier property, otherwise tries to convert the given parameter to an identifier
* suitable for this model.
* @param {*} obj
* @returns {*|undefined}
* @example
var id = context.model('User').idOf({ id:1, "name":"anonymous"});
*/
DataModel.prototype.idOf = function(obj) {
if (typeof obj === 'undefined')
return;
if (obj===null)
return;
if (typeof this.primaryKey === 'undefined' || this.primaryKey == null)
return;
if (typeof obj === 'object')
return obj[this.primaryKey];
return obj;
};
/**
* Casts the given object and returns an object that is going to be used against the underlying database.
* @param {*} obj - The source object which is going to be cast
* @param {number=} state - The state of the source object.
* @returns {*} - Returns an object which is going to be against the underlying database.
*/
DataModel.prototype.cast = function(obj, state)
{
return cast_.call(this, obj, state);
};
/**
* @param {*} obj
* @param {number=} state
* @returns {*}
* @private
*/
function cast_(obj, state) {
var self = this;
if (obj==null)
return {};
if (typeof obj === 'object' && obj instanceof Array)
{
return obj.map(function(x) {
return cast_.call(self, x, state);
});
}
else
{
//ensure state (set default state to Insert=1)
state = _.isNil(state) ? (_.isNil(obj.$state) ? 1 : obj.$state) : state;
var result = {}, name;
self.attributes.filter(function(x) {
if (x.model!==self.name) { return false; }
return (!x.readonly) ||
(x.readonly && (typeof x.calculation!=='undefined') && state==2) ||
(x.readonly && (typeof x.value!=='undefined') && state==1) ||
(x.readonly && (typeof x.calculation!=='undefined') && state==1);
}).filter(function(y) {
/*
change: 2016-02-27
author:k.barbounakis@gmail.com
description:exclude non editable attributes on update operation
*/
return (y.state==2) ? (y.hasOwnProperty("editable") ? y.editable : true) : true;
}).forEach(function(x) {
name = obj.hasOwnProperty(x.property) ? x.property : x.name;
if (obj.hasOwnProperty(name))
{
var mapping = self.inferMapping(name);
if (_.isNil(mapping))
result[x.name] = obj[name];
else if ((mapping.associationType==='association') && (mapping.childModel===self.name)) {
if ((typeof obj[name] === 'object') && (obj[name] != null))
//set associated key value (e.g. primary key value)
result[x.name] = obj[name][mapping.parentField];
else
//set raw value
result[x.name] = obj[name];
}
}
});
return result;
}
}
/**
* @param {*} obj
* @param {number=} state
* @returns {*}
* @private
*/
function castForValidation_(obj, state) {
var self = this;
if (obj==null)
return {};
if (typeof obj === 'object' && obj instanceof Array)
{
return obj.map(function(x) {
return castForValidation_.call(self, x, state);
});
}
else
{
//ensure state (set default state to Insert=1)
state = _.isNil(state) ? (_.isNil(obj.$state) ? 1 : obj.$state) : state;
var result = {}, name;
self.attributes.filter(function(x) {
if (x.model!==self.name) {
if (types.parsers.parseBoolean(x.cloned) == false)
return false;
}
return (!x.readonly) ||
(x.readonly && (typeof x.calculation!=='undefined') && state==2) ||
(x.readonly && (typeof x.value!=='undefined') && state==1) ||
(x.readonly && (typeof x.calculation!=='undefined') && state==1);
}).filter(function(y) {
/*
change: 2016-02-27
author:k.barbounakis@gmail.com
description:exclude non editable attributes on update operation
*/
return (y.state==2) ? (y.hasOwnProperty("editable") ? y.editable : true) : true;
}).forEach(function(x) {
name = obj.hasOwnProperty(x.property) ? x.property : x.name;
if (obj.hasOwnProperty(name))
{
var mapping = self.inferMapping(name);
if (_.isNil(mapping))
result[x.name] = obj[name];
else if ((mapping.associationType==='association') && (mapping.childModel===self.name)) {
if ((typeof obj[name] === 'object') && (obj[name] != null))
//set associated key value (e.g. primary key value)
result[x.name] = obj[name][mapping.parentField];
else
//set raw value
result[x.name] = obj[name];
}
}
});
return result;
}
}
/**
* Casts the given source object and returns a data object based on the current model.
* @param {*} dest - The destination object
* @param {*} src - The source object
* @param {function(Error=)} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
*/
DataModel.prototype.recast = function(dest, src, callback)
{
callback = callback || function() {};
var self = this;
if (_.isNil(src)) {
callback();
return;
}
if (_.isNil(dest)) {
dest = { };
}
async.eachSeries(self.fields, function(field, cb) {
try {
if (src.hasOwnProperty(field.name)) {
//ensure db property removal
if (field.property && field.property!==field.name)
delete dest[field.name];
var mapping = self.inferMapping(field.name), name = field.property || field.name;
if (_.isNil(mapping)) {
//set destination property
dest[name] = src[field.name];
cb(null);
}
else if (mapping.associationType==='association') {
if (typeof dest[name] === 'object' && dest[name] ) {
//check associated object
if (dest[name][mapping.parentField]===src[field.name]) {
//return
cb(null);
}
else {
//load associated item
var associatedModel = self.context.model(mapping.parentModel);
associatedModel.where(mapping.parentField).equal(src[field.name]).silent().first(function(err, result) {
if (err) {
cb(err);
}
else {
dest[name] = result;
//return
cb(null);
}
});
}
}
else {
//set destination property
dest[name] = src[field.name];
cb(null);
}
}
}
else {
cb(null);
}
}
catch (e) {
cb(e);
}
}, function(err) {
callback(err);
});
};
/**
* Casts the given object and returns an object that was prepared for insert.
* @param obj {*} - The object to be cast
* @returns {*}
*/
DataModel.prototype.new = function(obj)
{
return this.cast(obj);
};
/**
*
* @param {*|Array} obj
* @param {Function} callback
* @private
*/
function save_(obj, callback) {
var self = this;
if (typeof obj=='undefined' || obj == null) {
callback.call(self, null);
return;
}
//ensure migration
self.migrate(function(err) {
if (err) { callback(err); return; }
//do save
var arr = [];
if (util.isArray(obj)) {
for (var i = 0; i < obj.length; i++)
arr.push(obj[i]);
}
else
arr.push(obj);
var db = self.context.db;
var res = [];
db.executeInTransaction(function(cb) {
async.eachSeries(arr, function(item, saveCallback) {
saveSingleObject_.call(self, item, function(err, result) {
if (err) {
saveCallback.call(self, err);
return;
}
res.push(result.insertedId);
saveCallback.call(self, null);
});
}, function(err) {
if (err) {
res = null;
cb(err);
return;
}
cb(null);
});
}, function(err) {
callback.call(self, err, res);
});
});
}
/**
* Saves the given object or array of objects
* @param obj {*|Array}
* @param callback {Function=} - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
* @returns {Promise<T>|*} - If callback parameter is missing then returns a Promise object.
* @example
//save a new group (Sales)
var group = { "description":"Sales Users", "name":"Sales" };
context.model("Group").save(group).then(function() {
console.log('A new group was created with ID ' + group.id);
done();
}).catch(function(err) {
done(err);
});
*/
DataModel.prototype.save = function(obj, callback)
{
if (typeof callback !== 'function') {
var d = Q.defer();
save_.call(this, obj, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return save_.call(this, obj, callback);
}
};
/**
* Infers the state of the given object.
* @param {DataObject|*} obj - The source object
* @param {function(Error=,DataObjectState=)} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result.
* @see DataObjectState
*/
DataModel.prototype.inferState = function(obj, callback) {
var self = this,
DataStateValidatorListener = require('./data-state-validator').DataStateValidatorListener;
var e = { model:self, target:obj };
DataStateValidatorListener.prototype.beforeSave(e, function(err) {
//if error return error
if (err) { return callback(err); }
//otherwise return the calucated state
callback(null, e.state);
});
};
/**
* @param {*} obj
* @param {Function} callback
* @private
*/
function saveBaseObject_(obj, callback) {
//ensure callback
callback = callback || function() {};
var self = this, base = self.base();
//if obj is an array of objects throw exception (invoke callback with error)
if (util.isArray(obj)) {
callback.call(self, new Error('Invalid argument. Base object cannot be an array.'));
return 0;
}
//if current model does not have a base model
if (base==null) {
//exit operation
callback.call(self, null);
}
else {
base.silent();
//perform operation
saveSingleObject_.call(base, obj, function(err, result) {
callback.call(self, err, result);
});
}
}
/**
* @param {*} obj
* @param {Function} callback
* @private
*/
function saveSingleObject_(obj, callback) {
var self = this,
NotNullConstraintListener = dataListeners.NotNullConstraintListener,
DataValidatorListener = validators.DataValidatorListener,
UniqueContraintListener = dataListeners.UniqueContraintListener;
callback = callback || function() {};
if (obj==null) {
callback.call(self);
return;
}
if (util.isArray(obj)) {
callback.call(self, new Error('Invalid argument. Source object cannot be an array.'));
return 0;
}
if (obj.$state == 4) {
return removeSingleObject_.call(self, obj, callback);
}
//get object state before any other operation
var state = obj.$state ? obj.$state : (obj[self.primaryKey]!=null ? 2 : 1);
var e = {
model: self,
target: obj,
state:state
};
//register nested objects listener (before save)
self.once('before.save', DataNestedObjectListener.prototype.beforeSave);
//register data association listener (before save)
self.once('before.save', DataObjectAssociationListener.prototype.beforeSave);
//register data association listener
self.once('after.save', DataObjectAssociationListener.prototype.afterSave);
//register unique constraint listener at the end of listeners collection (before emit)
self.once('before.save', UniqueContraintListener.prototype.beforeSave);
//register data validators at the end of listeners collection (before emit)
self.once('before.save', DataValidatorListener.prototype.beforeSave);
//register not null listener at the end of listeners collection (before emit)
self.once('before.save', NotNullConstraintListener.prototype.beforeSave);
//execute before update events
self.emit('before.save', e, function(err) {
//if an error occured
if (err) {
//invoke callback with error
callback.call(self, err);
}
//otherwise execute save operation
else {
//save base object if any
saveBaseObject_.call(self, e.target, function(err, result) {
if (err) {
callback.call(self, err);
return;
}
//if result is defined
if (result!==undefined)
//sync original object
util._extend(e.target, result);
//get db context
var db = self.context.db;
//create insert query
var target = self.cast(e.target, e.state);
var q = null, key = target[self.primaryKey];
if (e.state==1)
//create insert statement
q = qry.insert(target).into(self.sourceAdapter);
else
{
//create update statement
if (key)
delete target[self.primaryKey];
if (Object.keys(target).length>0)
q = qry.update(self.sourceAdapter).set(target).where(self.primaryKey).equal(e.target[self.primaryKey]);
else
//object does not have any properties other than primary key. do nothing
q = new EmptyQueryExpression();
}
if (q instanceof EmptyQueryExpression) {
if (key)
target[self.primaryKey] = key;
//get updated object
self.recast(e.target, target, function(err) {
if (err) {
//and return error
callback.call(self, err);
}
else {
//execute after update events
self.emit('after.save',e, function(err) {
//and return
return callback.call(self, err, e.target);
});
}
});
}
else {
var pm = e.model.field(self.primaryKey), nextIdentity, adapter = e.model.sourceAdapter;
//search if adapter has a nextIdentity function (also primary key must be a counter and state equal to insert)
if (pm.type === 'Counter' && typeof db.nextIdentity === 'function' && e.state==1) {
nextIdentity = db.nextIdentity;
}
else {
//otherwise use a dummy nextIdentity function
nextIdentity = function(a, b, callback) { return callback(); }
}
nextIdentity.call(db, adapter, pm.name, function(err, insertedId) {
if (err) { return callback.call(self, err); }
if (insertedId) {
//get object to insert
if (q.$insert) {
var o = q.$insert[adapter];
if (o) {
//set the generated primary key
o[pm.name] = insertedId;
}
}
}
db.execute(q, null, function(err, result) {
if (err) {
callback.call(self, err);
}
else {
if (key)
target[self.primaryKey] = key;
//get updated object
self.recast(e.target, target, function(err) {
if (err) {
callback.call(self, err);
}
else {
if (pm.type==='Counter' && typeof db.nextIdentity !== 'function' && e.state==1) {
//if data adapter contains lastIdentity function
var lastIdentity = db.lastIdentity || function(lastCallback) {
if (_.isNil(result))
lastCallback(null, { insertId: null});
lastCallback(null, result);
};
lastIdentity.call(db, function(err, lastResult) {
if (lastResult)
if (lastResult.insertId)
e.target[self.primaryKey] = lastResult.insertId;
//raise after save listeners
self.emit('after.save',e, function(err) {
//invoke callback
callback.call(self, err, e.target);
});
});
}
else {
//raise after save listeners
self.emit('after.save',e, function(err) {
//invoke callback
callback.call(self, err, e.target);
});
}
}
});
}
});
});
}
});
}
});
}
/**
* Gets an array of strings which contains the super types of this model e.g. User model may have ['Account','Thing'] as super types
* @returns {Array}
*/
DataModel.prototype.getSuperTypes = function() {
var result=[];
var baseModel = this.base();
while(baseModel!=null) {
result.unshift(baseModel.name);
baseModel = baseModel.base();
}
return result;
};
/**
* @param {*|Array} obj
* @param {Function} callback
* @private
*/
function update_(obj, callback) {
var self = this;
//ensure callback
callback = callback || function() {};
if ((obj==null) || obj === undefined) {
callback.call(self, null);
}
//set state
if (util.isArray(obj)) {
obj.forEach(function(x) {x['$state'] = 2; })
}
else {
obj['$state'] = 2;
}
self.save(obj, callback);
}
/**
* Updates an item or an array of items
* @param obj {*|Array} - The item or the array of items to update
* @param callback {Function=} - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
* @returns {Promise<T>|*} - If callback parameter is missing then returns a Promise object.
*/
DataModel.prototype.update = function(obj, callback)
{
if (typeof callback !== 'function') {
var d = Q.defer();
update_.call(this, obj, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return update_.call(this, obj, callback);
}
};
/**
* @param {*|Array} obj
* @param {Function} callback
* @private
*/
function insert_(obj, callback) {
var self = this;
//ensure callback
callback = callback || function() {};
if ((obj==null) || obj === undefined) {
callback.call(self, null);
}
//set state
if (util.isArray(obj)) {
obj.forEach(function(x) {x['$state'] = 1; })
}
else {
obj['$state'] = 1;
}
self.save(obj, callback);
}
/**
* Inserts an item or an array of items
* @param obj {*|Array} - The item or the array of items to update
* @param callback {Function=} - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
* @returns {Promise<T>|*} - If callback parameter is missing then returns a Promise object.
*/
DataModel.prototype.insert = function(obj, callback)
{
if (typeof callback !== 'function') {
var d = Q.defer();
insert_.call(this, obj, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return insert_.call(this, obj, callback);
}
};
/**
*
* @param {*|Array} obj
* @param {Function} callback
* @private
*/
function remove_(obj, callback) {
var self = this;
if (obj==null)
{
callback.call(self, null);
return;
}
self.migrate(function(err) {
if (err) { callback(err); return; }
var arr = [];
if (util.isArray(obj)) {
for (var i = 0; i < obj.length; i++)
arr.push(obj[i]);
}
else
arr.push(obj);
//delete objects
var db = self.context.db;
db.executeInTransaction(function(cb) {
async.eachSeries(arr, function(item, removeCallback) {
removeSingleObject_.call(self, item, function(err) {
if (err) {
removeCallback.call(self, err);
return;
}
removeCallback.call(self, null);
});
}, function(err) {
if (err) {
cb(err);
return;
}
cb(null);
});
}, function(err) {
callback.call(self, err);
});
});
}
/**
* Deletes the given object or array of objects
* @param obj {*|Array} The item or the array of items to delete
* @param callback {Function=} - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
* @returns {Promise<T>|*} - If callback parameter is missing then returns a Promise object.
* @example
//remove group (Sales)
var group = { "name":"Sales" };
context.model("Group").remove(group).then(function() {
done();
}).catch(function(err) {
done(err);
});
*/
DataModel.prototype.remove = function(obj, callback)
{
if (typeof callback !== 'function') {
var d = Q.defer();
remove_.call(this, obj, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return remove_.call(this, obj, callback);
}
};
/**
* @param {Object} obj
* @param {Function} callback
* @private
*/
function removeSingleObject_(obj, callback) {
var self = this;
callback = callback || function() {};
if (obj==null) {
callback.call(self);
return;
}
if (util.isArray(obj)) {
callback.call(self, new Error('Invalid argument. Object cannot be an array.'));
return 0;
}
var e = {
model: self,
target: obj,
state: 4
};
//register nested objects listener
self.once('before.remove', DataNestedObjectListener.prototype.beforeRemove);
//register data association listener
self.once('before.remove', DataObjectAssociationListener.prototype.afterSave);
//execute before update events
self.emit('before.remove', e, function(err) {
//if an error occurred
if (err) {
//invoke callback with error
return callback(err);
}
//get db context
var db = self.context.db;
//create delete query
var q = qry.deleteFrom(self.sourceAdapter).where(self.primaryKey).equal(obj[self.primaryKey]);
//execute delete query
db.execute(q, null, function(err) {
if (err) {
return callback(err);
}
//remove base object
removeBaseObject_.call(self, e.target, function(err, result) {
if (err) {
return callback(err);
}
if (typeof result !== 'undefined' && result != null) {
util._extend(e.target, result);
}
//execute after remove events
self.emit('after.remove',e, function(err) {
//invoke callback
return callback(err, e.target);
});
});
});
});
}
/**
* @param {*} obj
* @param {Function} callback
* @private
*/
function removeBaseObject_(obj, callback) {
//ensure callback
callback = callback || function() {};
var self = this, base = self.base();
//if obj is an array of objects throw exception (invoke callback with error)
if (util.isArray(obj)) {
callback.call(self, new Error('Invalid argument. Object cannot be an array.'));
return 0;
}
//if current model does not have a base model
if (_.isNil(base)) {
//exit operation
callback.call(self, null);
}
else {
base.silent();
//perform operation
removeSingleObject_.call(base, obj, function(err, result) {
callback.call(self, err, result);
});
}
}
/**
* Validates that the given string is plural or not.
* @param s {string}
* @returns {boolean}
* @private
*/
DataModel.PluralExpression = /([a-zA-Z]+?)([e']s|[^aiou]s)$/;
/**
* Ensures model data adapter.
* @param callback
* @private
*/
DataModel.prototype.ensureModel = function(callback) {
var self = this;
if (self.name=='Migration') {
//do nothing
return callback();
}
//get migration model
var migrationModel = self.context.model("migration");
//ensure migration
var version = dataCommon.isDefined(self.version) ? self.version : '0.0';
migrationModel.where('appliesTo').equal(self.sourceAdapter).and('version').equal(version).count(function(err, result) {
if (err) { return callback(err); }
if (result>0) { return callback(); }
self.migrate(callback);
});
};
/**
* Performing an automatic migration of current data model based on the current model's definition.
* @param {Function} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise. The second argument will contain the result.
*/
DataModel.prototype.migrate = function(callback)
{
var self = this;
//cache: data model migration
//prepare migration cache
var conf = self.context.getConfiguration();
conf.cache = conf.cache || {};
conf.cache[self.name] = conf.cache[self.name] || { };
if (conf.cache[self.name].version==self.version) {
//model has already been migrated, so do nothing
return callback();
}
//do not migrate sealed models
if (self.sealed) {
return callback();
}
var context = self.context;
//do migration
var fields = self.attributes.filter(function(x) {
return (self.name == x.model) && (!x.many);
});
if ((fields==null) || (fields.length==0))
throw new Error("Migration is not valid for this model. The model has no fields.");
var migration = new types.DataModelMigration();
migration.add = fields;
migration.version = self.version!=null ? self.version : '0.0';
migration.appliesTo = self.sourceAdapter;
migration.model = self.name;
migration.description = util.format('%s migration (version %s)', this.title, migration.version);
if (context==null)
throw new Error("The underlying data context cannot be empty.");
//get all related models
var models = [];
// self.fields.filter(function(x) {
// return (!conf.dataTypes[x.type] && (self.name!=x.type));
// }).forEach(function(x) {
// var m = context.model(x.type);
// if (m) {
// var m1 = models.find(function(y) {
// return y.name == m.name;
// });
// }
// });
var db = context.db;
var baseModel = self.base();
if (baseModel!=null) {
models.push(baseModel);
}
//validate associated models
fields.forEach(function(x) {
//validate mapping
var mapping = self.inferMapping(x.name);
if (mapping && mapping.associationType === 'association') {
if (mapping.childModel === self.name) {
//get parent model
var parentModel = self.context.model(mapping.parentModel),
attr = parentModel.getAttribute(mapping.parentField);
if (attr) {
x.type = attr.type;
}
}
migration.indexes.push({
name: "INDEX_" + migration.appliesTo.toUpperCase() + "_" + x.name.toUpperCase(),
columns: [ x.name ]
});
}
else if (x.indexed === true) {
migration.indexes.push({
name: "INDEX_" + migration.appliesTo.toUpperCase() + "_" + x.name.toUpperCase(),
columns: [ x.name ]
});
}
});
//execute transaction
db.executeInTransaction(function(tr) {
if (models.length==0) {
self.emit('before.upgrade', { model:self }, function(err) {
if (err) { return tr(err); }
db.migrate(migration, function(err) {
if (err) { return tr(err); }
if (migration['updated']) {
return tr();
}
//execute after migrate events
self.emit('after.upgrade', { model:self }, function(err) {
return tr(err);
});
});
});
}
else {
async.eachSeries(models,function(m, cb)
{
if (m) {
m.migrate(cb);
}
else {
return cb();
}
}, function(err) {
if (err) { return tr(err); }
self.emit('before.upgrade', { model:self }, function(err) {
if (err) { return tr(err); }
db.migrate(migration, function(err) {
if (err) { return tr(err); }
if (migration['updated']) {
return tr();
}
//execute after migrate events
self.emit('after.upgrade', { model:self }, function(err) {
return tr(err);
});
});
});
});
}
}, function(err) {
if (!err) {
//set migration info to configuration cache (conf.cache.model.version=[current version])
//cache: data model migration
conf.cache[self.name].version = self.version;
}
callback(err);
});
};
/**
* Gets an instance of DataField class which represents the primary key of this model.
* @returns {DataField|*}
*/
DataModel.prototype.key = function()
{
return this.attributes.find(function(x) { return x.primary==true; });
};
/**
* Gets an instance of DataField class based on the given name.
* @param {String} name - The name of the field.
* @return {DataField|*} - Returns a data field if exists. Otherwise returns null.
*/
DataModel.prototype.field = function(name)
{
if (typeof name !== 'string')
return null;
return this.attributes.find(function(x) { return (x.name==name) || (x.property==name); });
};
/**
*
* @param {string|*} attr
* @param {string=} alias
* @returns {DataQueryable|QueryField|*}
*/
DataModel.prototype.fieldOf = function(attr, alias) {
var q = new DataQueryable(this);
return q.fieldOf(attr, alias);
};
/**
* Gets an instance of DataModelView class which represents a model view with the given name.
* @param {string} name - A string that represents the name of the view.
* @returns {DataModelView|undefined}
*@example
var view = context.model('Person').dataviews('summary');
*
*/
DataModel.prototype.dataviews = function(name) {
var self = this;
var re = new RegExp('^' + name.replace('*','\\*').replace('$','\\$') + '$', 'ig');
var view = self.views.filter(function(x) { return re.test(x.name);})[0];
if (_.isNil(view))
return;
return util._extend(new DataModelView(self), view);
};
/**
* Gets an instance of DataModelView class which represents a model view with the given name.
* @param {string} name - A string that represents the name of the view.
* @returns {DataModelView|undefined}
*@example
var view = context.model('Person').getDataView('summary');
*
*/
DataModel.prototype.getDataView = function(name) {
var self = this;
var re = new RegExp('^' + name.replace('$','\$') + '$', 'ig');
var view = self.views.filter(function(x) { return re.test(x.name);})[0];
if (_.isNil(view))
{
return util._extend(new DataModelView(self), {
"name":"default",
"title":"Default View",
"fields": self.attributes.map(function(x) {
return { "name":x.name }
})
});
}
return util._extend(new DataModelView(self), view);
};
/**
* @param {DataField|*} field
* @param {DataAssociationMapping|*} mapping
* @private
*/
function cacheMapping_(field, mapping) {
if (_.isNil(field))
return;
//cache mapping
var cachedModel = this.getConfiguration().models[this.name];
if (cachedModel) {
var cachedField = cachedModel.fields.find(function(x) { return x.name === field.name });
if (typeof cachedField === 'undefined') {
//search in attributes
cachedField = this.attributes.find(function(x) { return x.name === field.name });
if (cachedField) {
//add overriden field
cachedModel.fields.push(util._extend({ }, cachedField));
cachedField = cachedModel.fields[cachedModel.fields.length-1];
//clear attributes
this._clearAttributes();
}
}
if (cachedField)
//add mapping
cachedField.mapping = mapping;
}
}
/**
* Gets a field association mapping based on field attributes, if any. Otherwise returns null.
* @param {string} name - The name of the field
* @returns {DataAssociationMapping|undefined}
*/
DataModel.prototype.inferMapping = function(name) {
var self = this;
//ensure model cached mappings
var conf = self.context.getConfiguration().model(self.name);
if (typeof conf === "undefined" || conf == null) {
return;
}
if (typeof conf.mappings_ === 'undefined') {
conf.mappings_ = { };
}
if (typeof conf.mappings_[name] !== 'undefined') {
if (conf.mappings_[name] instanceof types.DataAssociationMapping)
return conf.mappings_[name];
else
return new types.DataAssociationMapping(conf.mappings_[name]);
}
var field = self.field(name), result;
if (!field)
return null;
if (field.mapping) {
//if field model is different than the current model
if (field.model !== self.name) {
//if field mapping is already associated with the current model
// (child or parent model is equal to the current model)
if ((field.mapping.childModel===self.name) || (field.mapping.parentModel===self.name)) {
//cache mapping
conf.mappings_[name] = new types.DataAssociationMapping(field.mapping);
//do nothing and return field mapping
return conf.mappings_[name];
}
//get super types
var superTypes = self.getSuperTypes();
//map an inherited association
//1. super model has a foreign key association with another model
//(where super model is the child or the parent model)
if (field.mapping.associationType === 'association') {
//create a new cloned association
result = new types.DataAssociationMapping(field.mapping);
//check super types
if (superTypes.indexOf(field.mapping.childModel)>=0) {
//set child model equal to current model
result.childModel = self.name;
}
else if (superTypes.indexOf(field.mapping.parentModel)>=0) {
//set child model equal to current model
result.childModel = self.name;
}
else {
//this is an exception
throw new types.DataException("EMAP","An inherited data association cannot be mapped.");
}
//cache mapping
conf.mappings_[name] = result;
//and finally return the newly created DataAssociationMapping object
return result;
}
//2. super model has a junction (many-to-many association) with another model
//(where super model is the child or the parent model)
else if (field.mapping.associationType === 'junction') {
//create a new cloned association
result = new types.DataAssociationMapping(field.mapping);
if (superTypes.indexOf(field.mapping.childModel)>=0) {
//set child model equal to current model
result.childModel = self.name;
}
else if (superTypes.indexOf(field.mapping.parentModel)>=0) {
//set parent model equal to current model
result.parentModel = self.name;
}
else {
//this is an exception
throw new types.DataException("EMAP","An inherited data association cannot be mapped.");
}
//cache mapping
conf.mappings_[name] = result;
//and finally return the newly created DataAssociationMapping object
return result;
}
}
//in any other case return the assocation mapping object
if (field.mapping instanceof types.DataAssociationMapping) {
//cache mapping
conf.mappings_[name] = field.mapping;
//and return
return field.mapping;
}
result = util._extend(new types.DataAssociationMapping(), field.mapping);
//cache mapping
conf.mappings_[name] = result;
//and return
return result;
}
else {
//get field model type
var associatedModel = self.context.model(field.type);
if ((typeof associatedModel === 'undefined') || (associatedModel == null))
{
if (typeof field.many === 'boolean' && field.many) {
//validate primitive type mapping
var tagMapping = inferTagMapping_.call(self, field);
if (tagMapping) {
//apply data association mapping to definition
var definitionField = conf.fields.find(function(x) {
return x.name === field.name;
});
definitionField.mapping = field.mapping = tagMapping;
return new types.DataAssociationMapping(definitionField.mapping);
}
}
return null;
}
//in this case we have two possible associations. Junction or Foreign Key association
//try to find a field that belongs to the associated model and holds the foreign key of this model.
var associatedField = associatedModel.attributes.find(function(x) {
return x.type === self.name;
});
if (associatedField)
{
if (associatedField.many)
{
//return a data relation (parent model is the associated model)
result = new types.DataAssociationMapping({
parentModel:associatedModel.name,
parentField:associatedModel.primaryKey,
childModel:self.name,
childField:field.name,
associationType:'association',
cascade:'null'
});
//cache mapping
conf.mappings_[name] = result;
//and finally return mapping
return result;
}
else
{
//return a data relation (parent model is the current model)
result = new types.DataAssociationMapping({
parentModel:self.name,
parentField:self.primaryKey,
childModel:associatedModel.name,
childField:associatedField.name,
associationType:'association',
cascade:'null',
refersTo:field.property || field.name
});
//cache mapping
conf.mappings_[name] = result;
//and finally return mapping
return result;
}
}
else {
//validate pluralize
var re = new RegExp(DataModel.PluralExpression.source);
if (re.test(field.name) || field.many) {
//return a data junction
result = new types.DataAssociationMapping({
associationAdapter: self.name.concat(_.upperFirst(field.name)),
parentModel: self.name, parentField: self.primaryKey,
childModel: associatedModel.name,
childField: associatedModel.primaryKey,
associationType: 'junction',
cascade: 'delete'
});
//cache mapping
conf.mappings_[name] = result;
//and finally return mapping
return result;
}
else {
result = new types.DataAssociationMapping({
parentModel: associatedModel.name,
parentField: associatedModel.primaryKey,
childModel: self.name,
childField: field.name,
associationType: 'association',
cascade: 'null'
});
//cache mapping
conf.mappings_[name] = result;
//and finally return mapping
return result;
}
}
}
};
/**
*
* @param {*} obj
* @param {number} state
* @param {Function} callback
* @private
*/
function validate_(obj, state, callback) {
/**
* @type {DataModel|*}
*/
var self = this;
if (_.isNil(obj)) {
return callback();
}
//get object copy (based on the defined state)
var objCopy = castForValidation_.call (self, obj, state);
var attributes = self.attributes.filter(function(x) {
if (x.model!==self.name) {
if (!x.cloned)
return false;
}
return (!x.readonly) ||
(x.readonly && (typeof x.calculation!=='undefined') && state==2) ||
(x.readonly && (typeof x.value!=='undefined') && state==1) ||
(x.readonly && (typeof x.calculation!=='undefined') && state==1);
}).filter(function(y) {
return (state==2) ? (y.hasOwnProperty("editable") ? y.editable : true) : true;
});
async.eachSeries(attributes, function(attr, cb) {
var validator, validationResult;
//get value
var value = objCopy[attr.name];
//build validators array
var arrValidators=[];
//-- RequiredValidator
if (attr.hasOwnProperty('nullable') && !attr.nullable)
{
if (state==1 && !attr.primary) {
arrValidators.push(new validators.RequiredValidator());
}
else if (state==2 && !attr.primary && objCopy.hasOwnProperty(attr.name)) {
arrValidators.push(new validators.RequiredValidator());
}
}
//-- MaxLengthValidator
if (attr.hasOwnProperty('size') && objCopy.hasOwnProperty(attr.name)) {
if (!(attr.validation && attr.validation.maxLength))
arrValidators.push(new validators.MaxLengthValidator(attr.size));
}
//-- CustomValidator
if (attr.validation && attr.validation['validator'] && objCopy.hasOwnProperty(attr.name)) {
var validatorModule;
try {
if (/^\./ig.test(attr.validation['validator'])) {
var modulePath = path.resolve(process.cwd(), attr.validation['validator']);
validatorModule = require(modulePath);
}
else {
validatorModule = require(attr.validation['validator']);
}
}
catch (e) {
dataCommon.debug(util.format("Data validator module (%s) cannot be loaded", attr.validation.type));
dataCommon.debug(e);
return cb(e);
}
if (typeof validatorModule.createInstance !== 'function') {
dataCommon.debug(util.format("Data validator module (%s) does not export createInstance() method.", attr.validation.type));
return cb(new Error("Invalid data validator type."));
}
arrValidators.push(validatorModule.createInstance(attr));
}
//-- DataTypeValidator #1
if (attr.validation && objCopy.hasOwnProperty(attr.name)) {
if (typeof attr.validation.type === 'string') {
arrValidators.push(new validators.DataTypeValidator(attr.validation.type));
}
else {
//convert validation data to pseudo type declaration
var validationProperties = {
properties:attr.validation
};
arrValidators.push(new validators.DataTypeValidator(validationProperties));
}
}
//-- DataTypeValidator #2
if (attr.type && objCopy.hasOwnProperty(attr.name)) {
arrValidators.push(new validators.DataTypeValidator(attr.type));
}
if (arrValidators.length == 0) {
return cb();
}
//do validation
async.eachSeries(arrValidators, function(validator, cb) {
//set context
if (typeof validator.setContext === 'function') {
validator.setContext(self.context);
}
//set target
validator.target = obj;
if (typeof validator.validateSync === 'function') {
validationResult = validator.validateSync(value);
if (validationResult) {
return cb(new types.DataException(validationResult.code || "EVALIDATE",validationResult.message, validationResult.innerMessage, self.name, attr.name));
}
else {
return cb();
}
}
else if (typeof validator.validate === 'function') {
return validator.validate(value, function(err, validationResult) {
if (err) {
return cb(err);
}
if (validationResult) {
return cb(new types.DataException(validationResult.code || "EVALIDATE",validationResult.message, validationResult.innerMessage, self.name, attr.name));
}
return cb();
});
}
else {
dataCommon.debug(util.format("Data validator (%s) does not have either validate() or validateSync() methods.", attr.validation.type));
return cb(new Error("Invalid data validator type."));
}
}, function(err) {
return cb(err);
});
}, function(err) {
return callback(err);
});
}
/**
* Validates the given object against validation rules which are defined either by the data type or the definition of each attribute
<p>Read more about data validation <a href="DataValidatorListener.html">here</a>.</p>
* @param {*} obj - The data object which is going to be validated
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
* @returns {Promise|*} - If callback parameter is missing then returns a Promise object.
*/
DataModel.prototype.validateForUpdate = function(obj, callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
validate_.call(this, obj, 2, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return validate_.call(this, obj, callback);
}
};
/**
* Validates the given object against validation rules which are defined either by the data type or the definition of each attribute
<p>Read more about data validation <a href="DataValidatorListener.html">here</a>.</p>
* @param {*} obj - The data object which is going to be validated
* @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
* @returns {Promise|*} - If callback parameter is missing then returns a Promise object.
<p>Read more about data validation <a href="DataValidationListener.html">here</a></p>
*/
DataModel.prototype.validateForInsert = function(obj, callback) {
if (typeof callback !== 'function') {
var d = Q.defer();
validate_.call(this, obj, 1, function(err, result) {
if (err) { return d.reject(err); }
d.resolve(result);
});
return d.promise;
}
else {
return validate_.call(this, obj, callback);
}
};
/**
* Sets the number of levels of the expandable attributes.
* The default value is 1 which means that any expandable attribute will be flat (without any other nested attribute).
* If the value is greater than 1 then the nested objects may contain other nested objects and so on.
* @param {Number=} value - A number which represents the number of levels which are going to be used in expandable attributes.
* @returns {DataQueryable}
* @example
//get orders, expand customer and get customer's nested objects if any.
context.model('Order')
.levels(2)
.orderByDescending('dateCreated)
.expand('customer')
.getItems().then(function(result) {
done(null, result);
}).catch(function(err) {
done(err);
});
*/
DataModel.prototype.levels = function(value) {
var result = new DataQueryable(this);
return result.levels(value);
};
/**
* Gets an array of active models which are derived from this model.
* @returns {Promise|*}
* @example
* context.model("Thing").getSubTypes().then(function(result) {
console.log(JSON.stringify(result,null,4));
return done();
}).catch(function(err) {
return done(err);
});
*/
DataModel.prototype.getSubTypes = function () {
var self = this;
var d = Q.defer();
process.nextTick(function() {
var migrations = self.context.model("Migration");
if (_.isNil(migrations)) {
return d.resolve([]);
}
migrations.silent()
.select("model")
.groupBy("model")
.all().then(function(result) {
var conf = self.context.getConfiguration(), arr = [];
result.forEach(function(x) {
var m = conf.getModelDefinition(x.model);
if (m && m.inherits === self.name) {
arr.push(m.name);
}
});
return d.resolve(arr);
}).catch(function(err) {
return d.reject(err)
});
});
return d.promise;
};
/**
* Gets an attribute of this data model.
* @param {string} name
*/
DataModel.prototype.getAttribute = function (name) {
if (_.isNil(name)) { return; }
if (typeof name !== 'string') { return; }
return this.attributes.find(function(x) { return x.name === name; });
};
/**
* Gets a collection of DataObject instances by executing the defined query.
* @returns {Promise|*}
*/
DataModel.prototype.getTypedItems = function() {
var self = this,
d = Q.defer();
process.nextTick(function() {
var q = new DataQueryable(self);
q.getTypedItems().then(function (result) {
return d.resolve(result);
}).catch(function(err) {
return d.reject(err);
});
});
return d.promise;
};
/**
* Gets a result set that contains a collection of DataObject instances by executing the defined query.
* @returns {Promise|*}
*/
DataModel.prototype.getTypedList = function() {
var self = this,
d = Q.defer();
process.nextTick(function() {
var q = new DataQueryable(self);
q.getTypedList().then(function (result) {
return d.resolve(result);
}).catch(function(err) {
return d.reject(err);
});
});
return d.promise;
};
if (typeof exports !== 'undefined') {
module.exports.DataModel = DataModel;
}