UNPKG

@themost/data

Version:
1,285 lines (1,218 loc) 117 kB
/** * @license * MOST Web Framework 2.0 Codename Blueshift * Copyright (c) 2017, THEMOST LP All rights reserved * * Use of this source code is governed by an BSD-3-Clause license that can be * found in the LICENSE file at https://themost.io/license */ /// var _ = 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