UNPKG

@themost/data

Version:

MOST Web Framework Codename Blueshift - Data module

815 lines (764 loc) 28.9 kB
// MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved const _ = require('lodash'); const {SequentialEventEmitter, LangUtils, AbstractClassError, AbstractMethodError} = require('@themost/common'); const {defer, Observable, shareReplay, switchMap} = require('rxjs'); const {UserService} = require('./UserService'); /** * @classdesc Represents an abstract data connector to a database * @class * @constructor * @param {*} options - The database connection options * @abstract * @property {*} rawConnection - Gets or sets the native database connection * @property {*} options - Gets or sets the database connection options */ function DataAdapter(options) { if (this.constructor === DataAdapter.prototype.constructor) { throw new AbstractClassError(); } this.rawConnection=null; this.options = options; } // noinspection JSUnusedLocalSymbols /** * Opens the underlying database connection * @param {Function} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.open = function(callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Closes the underlying database connection * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.close = function(callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Executes the given query against the underlying database. * @param {string|*} query - A string or a query expression to execute. * @param {*} values - An object which represents the named parameters that are going to be used during query parsing * @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. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.execute = function(query, values, callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Produces a new identity value for the given entity and attribute. * @param {string} entity - A string that represents the target entity name * @param {string} attribute - A string that represents the target attribute name * @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. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.selectIdentity = function(entity, attribute , callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Begins a transactional operation and executes the given function * @param {Function} fn - The function to execute * @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. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.executeInTransaction = function(fn, callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * A helper method for creating a database view if the current data adapter supports views * @param {string} name - A string that represents the name of the view to be created * @param {import('@themost/query').QueryExpression|*} query - A query expression that represents the database view * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.createView = function(name, query, callback) { throw new AbstractMethodError(); }; /** * @classdesc Represents the event arguments of a data model listener. * @class * @constructor * @property {DataModel|*} model - Represents the underlying model. * @property {DataObject|*} target - Represents the underlying data object. * @property {Number|*} state - Represents the operation state (Update, Insert, Delete). * @property {DataQueryable|*} emitter - Represents the event emitter, normally a DataQueryable object instance. * @property {*} query - Represents the underlying query expression. This property may be null. * @property {DataObject|*} previous - Represents the underlying data object. */ function DataEventArgs() { // } /** * @classdesc Represents the main data context. * @class * @inherits SequentialEventEmitter * @constructor * @abstract */ function DataContext() { DataContext.super_.bind(this)(); //throw abstract class error if (this.constructor === DataContext.prototype.constructor) { throw new AbstractClassError(); } const user$ = defer(() => this.getUser()); Object.defineProperty(this, 'user$', { configurable: true, enumerable: false, value: user$ }); const interactiveUser$ = defer(() => this.getInteractiveUser()); Object.defineProperty(this, 'interactiveUser$', { configurable: true, enumerable: false, value: interactiveUser$ }); const anonymousUser$ = defer(() => this.getAnonymousUser()); Object.defineProperty(this, 'anonymousUser$', { configurable: true, enumerable: false, value: anonymousUser$ }); /** * @property db * @description Gets the current database adapter * @type {DataAdapter} * @memberOf DataContext# */ Object.defineProperty(this, 'db', { get : function() { return null; }, configurable : true, enumerable:false }); Object.defineProperty(this, 'configuration', { get : function() { return this.getConfiguration(); }, configurable : true, enumerable:false }); } // noinspection JSUnusedLocalSymbols /** * Gets a data model based on the given data context * @param name {string} A string that represents the model to be loaded. * @returns {DataModel} * @abstract */ // eslint-disable-next-line no-unused-vars DataContext.prototype.model = function(name) { throw new AbstractMethodError(); }; /** * Gets an instance of DataConfiguration class which is associated with this data context * @returns {import('@themost/common').ConfigurationBase} * @abstract */ DataContext.prototype.getConfiguration = function() { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * @param {Function} callback * @abstract */ // eslint-disable-next-line no-unused-vars DataContext.prototype.finalize = function(callback) { return callback(); }; /** * Finalizes data context * @returns {Promise<void>} */ DataContext.prototype.finalizeAsync = function() { const self = this; return new Promise(function(resolve, reject) { return self.finalize(function(err) { if (err) { return reject(err); } return resolve(); }); }); } /** * * @param {function():Promise<void>} func * @returns {Promise<void>} */ DataContext.prototype.executeInTransactionAsync = function(func) { const self = this; return new Promise((resolve, reject) => { // start transaction return self.db.executeInTransaction(function(cb) { try { func().then(function() { // commit return cb(); }).catch( function(err) { // rollback return cb(err); }); } catch (err) { return cb(err); } }, function(err) { if (err) { return reject(err); } // end transaction return resolve(); }); }); } DataContext.prototype.getUser = function() { return new Promise((resolve, reject) => { if ((this.user && this.user.name) == null) { return resolve(null); } // get current application const application = this.getApplication(); if (application && typeof application.getService === 'function') { // get user service const userService = application.getService(UserService); // check if user service is available if (userService != null) { // get user return userService.getUser(this, this.user.name).then((result) => { return resolve(result); }).catch((err) => { return reject(err); }); } } return new UserService(application).getUser(this, this.user.name).then((result) => { return resolve(result); }).catch((err) => { return reject(err); }); }); }; DataContext.prototype.setUser = function(user) { this.user = user; }; DataContext.prototype.getAnonymousUser = function() { return new Promise((resolve, reject) => { // get current application const application = this.getApplication(); if (application && typeof application.getService === 'function') { // get user service const userService = application.getService(UserService); // check if user service is available if (userService != null) { // get user return userService.getAnonymousUser(this).then((result) => { return resolve(result); }).catch((err) => { return reject(err); }); } } return new UserService(application).getAnonymousUser(this).then((result) => { return resolve(result); }).catch((err) => { return reject(err); }); }); }; DataContext.prototype.getInteractiveUser = function() { return new Promise((resolve, reject) => { if ((this.interactiveUser && this.interactiveUser.name) == null) { return resolve(null); } // get current application const application = this.getApplication(); if (application && typeof application.getService === 'function') { // get user service const userService = application.getService(UserService); // check if user service is available if (userService != null) { // get user return userService.getUser(this, this.interactiveUser.name).then((result) => { return resolve(result); }).catch((err) => { return reject(err); }); } } return new UserService(application).getUser(this, this.interactiveUser.name).then((result) => { return resolve(result); }).catch((err) => { return reject(err); }); }); }; DataContext.prototype.setInteractiveUser = function(user) { this.interactiveUser = user; }; /** * Sets the application that is associated with this data context * @param {import('@themost/common').ApplicationBase} application */ DataContext.prototype.setApplication = function(application) { Object.defineProperty(this, 'application', { get: function() { return application; }, configurable: true, enumerable: false }); }; /** * Returns the application that is associated with this data context * @returns {import('@themost/common').ApplicationBase} */ DataContext.prototype.getApplication = function() { return this.application; }; LangUtils.inherits(DataContext, SequentialEventEmitter); /** * @classdesc Represents a data model's listener * @class * @constructor * @abstract */ function DataEventListener() { //do nothing } /** * Occurs before executing a data operation. The event arguments contain the query that is going to be executed. * @param {DataEventArgs} e - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.beforeExecute = function(e, cb) { return cb(); }; /** * Occurs after executing a data operation. The event arguments contain the executed query. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterExecute = function(event, cb) { return cb(); }; /** * Occurs before creating or updating a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.beforeSave = function(event, cb) { return cb(); }; /** * Occurs after creating or updating a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterSave = function(event, cb) { return cb(); }; /** * Occurs before removing a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. * @returns {DataEventListener} */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.beforeRemove = function(event, cb) { return cb(); }; /** * Occurs after removing a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterRemove = function(event, cb) { return cb(); }; /** * Occurs after upgrading a data model. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterUpgrade = function(event, cb) { return cb(); }; var DateTimeRegex = /^(\d{4})(?:-?W(\d+)(?:-?(\d+)D?)?|(?:-(\d+))?-(\d+))(?:[T ](\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?)?(?:Z(-?\d*))?$/g; var BooleanTrueRegex = /^true$/ig; var BooleanFalseRegex = /^false$/ig; /* var NullRegex = /^null$/ig; var UndefinedRegex = /^undefined$/ig; */ var IntegerRegex =/^[-+]?\d+$/g; var FloatRegex =/^[+-]?\d+(\.\d+)?$/g; /** * Represents a model migration scheme against data adapters * @class * @constructor * @ignore */ function DataModelMigration() { /** * Gets an array that contains the definition of fields that are going to be added * @type {Array} */ this.add = []; /** * Gets an array that contains a collection of constraints which are going to be added * @type {Array} */ this.constraints = []; /** * Gets an array that contains a collection of indexes which are going to be added or updated * @type {Array} */ this.indexes = []; /** * Gets an array that contains the definition of fields that are going to be deleted * @type {Array} */ this.remove = []; /** * Gets an array that contains the definition of fields that are going to be changed * @type {Array} */ this.change = []; /** * Gets or sets a string that contains the internal version of this migration. This property cannot be null. * @type {string} */ this.version = '0.0'; /** * Gets or sets a string that represents a short description of this migration * @type {string|null} */ this.description = null; /** * Gets or sets a string that represents the adapter that is going to be migrated through this operation. * This property cannot be null. */ this.appliesTo = null; /** * Gets or sets a string that represents the model that is going to be migrated through this operation. * This property may be null. */ this.model = null; } /** * @classdesc DataAssociationMapping class describes the association between two models. * @class * @property {string} associationAdapter - Gets or sets the association database object * @property {string} parentModel - Gets or sets the parent model name * @property {string} childModel - Gets or sets the child model name * @property {string} parentField - Gets or sets the parent field name * @property {string} childField - Gets or sets the child field name * @property {string} associationObjectField - Gets or sets the name of the parent field as it is defined in association adapter. This attribute is optional, but it is required for many-to-many associations where parent and child model are the same. * @property {string} associationValueField - Gets or sets the name of the child field as it is defined in association adapter. This attribute is optional, but it is required for many-to-many associations where parent and child model are the same. * @property {string} refersTo - Gets or sets the parent property where this association refers to * @property {string} parentLabel - Gets or sets the parent field that is going to be used as label for this association * @property {string} cascade - Gets or sets the action that occurs when parent item is going to be deleted (all|none|null|delete). The default value is 'none'. * @property {string} associationType - Gets or sets the type of this association (junction|association). The default value is 'association'. * @property {Array<DataModelPrivilege>} privilege - Gets or sets a collection of privileges which are going to be attached in a many-to-many association * @property {string[]} select - Gets or sets an array of fields to select from associated model. If this property is empty then all associated model fields will be selected. * @property {*} options - Gets or sets a set of default options which are going to be used while expanding results based on this data association. * @param {*=} obj - An object that contains relation mapping attributes * @constructor */ function DataAssociationMapping(obj) { this.cascade = 'none'; this.associationType = 'association'; //this.select = []; if (typeof obj === 'object') { _.assign(this, obj); } } /** * @class * @constructor * @property {string} name - Gets or sets the internal name of this field. * @property {string} property - Gets or sets the property name for this field. * @property {string} title - Gets or sets the title of this field. * @property {boolean} nullable - Gets or sets a boolean that indicates whether field is nullable or not. * @property {string} type - Gets or sets the type of this field. * @property {boolean} primary - Gets or sets a boolean that indicates whether field is primary key or not. * @property {boolean} many - Gets or sets a boolean that indicates whether field defines an one-to-many relationship between models. * @property {boolean} model - Gets or sets the parent model of this field. * @property {*} value - Gets or sets the default value of this field. * @property {*} calculation - Gets or sets the calculated value of this field. * @property {boolean} readonly - Gets or sets a boolean which indicates whether a field is readonly. * @property {boolean} editable - Gets or sets a boolean which indicates whether a field is available for edit. The default value is true. * @property {DataAssociationMapping} mapping - Get or sets a relation mapping for this field. * @property {string} coltype - Gets or sets a string that indicates the data field's column type. This attribute is used in data view definition * @property {boolean} expandable - Get or sets whether the current field defines an association mapping and the associated data object(s) must be included while getting data. * @property {string} section - Gets or sets the section where the field belongs. * @property {boolean} nested - Gets or sets a boolean which indicates whether this field allows object(s) to be nested and updatable during an insert or update operation * @property {string} description - Gets or sets a short description for this field. * @property {string} help - Gets or sets a short help for this field. * @property {string} appearance - Gets or sets the appearance template of this field, if any. * @property {{type:string,custom:string,minValue:*,maxValue:*,minLength:number,maxLength:number,pattern:string,patternMessage:string}|*} validation - Gets or sets data validation attributes. * @property {*} options - Gets or sets the available options for this field. * @property {boolean} virtual - Gets or sets a boolean that indicates whether this field is a view only field or not. * @property {boolean} indexed - Gets or sets a boolean which indicates whether this field will be indexed for searching items. The default value is false. */ function DataField() { this.nullable = true; this.primary = false; this.indexed = false; this.readonly = false; this.expandable = false; this.virtual = false; this.editable = true; } // noinspection JSUnusedGlobalSymbols DataField.prototype.getName = function() { return this.property || this.name; }; /** * @class * @constructor * @property {string} name - Gets or sets a short description for this listener * @property {string} type - Gets or sets a string which is the path of the module that exports this listener. * @property {boolean} disabled - Gets or sets a boolean value that indicates whether this listener is disabled or not. The default value is false. */ function DataModelEventListener() { } /** * An enumeration of tha available privilege types * @enum */ var PrivilegeType = { /** * Self Privilege (self). * @type {string} */ Self: 'self', /** * Parent Privilege (parent) * @type {string} */ Parent: 'parent', /** * Item Privilege (child) * @type {string} */ Item: 'item', /** * Global Privilege (global) * @type {string} */ Global: 'global' }; /** * @classdesc Represents a privilege which is defined in a data model, and it may be given in users and groups * @class * @constructor * @property {PermissionMask} mask - Gets or sets the set of permissions which may be given with this privilege. * @property {PrivilegeType|string} type - Gets or sets the type of this privilege (global|parent|item|self). * @property {string} filter - Gets or sets a filter expression which is going to be used for self privileges. * The defined set of permissions are automatically assigned if the requested objects fulfill filter criteria. * (e.g. read-write permissions for a user's associated person through the following expression:"user eq me()") * @property {string} account - Gets or sets a wildcard (*) expression for global privileges only. * The defined set of permissions are automatically assigned to all users (e.g. read permissions for all users) */ function DataModelPrivilege() { } /** * Represents a query result when this query uses paging parameters. * @class * @property {number} total - The total number of records * @property {number} skip - The number of skipped records * @property {Array} value - An array of objects which represents the query results. * @constructor */ function DataResultSet() { this.total = 0; this.skip = 0; this.value = []; } /** * @abstract * @constructor * @ignore */ function DataContextEmitter() { if (this.constructor === DataContextEmitter.prototype.constructor) { throw new AbstractClassError(); } } /** * @abstract */ DataContextEmitter.prototype.ensureContext = function() { throw new AbstractMethodError(); }; /** * An enumeration of the available data object states * @enum {number} */ var DataObjectState = { /** * Insert State (1) */ Insert:1, /** * Update State (2) */ Update:2, /** * Delete State (4) */ Delete:4, /** * Delete State (4) */ Execute:16 }; /** * An enumeration of the available data caching types * @enum {string} */ var DataCachingType = { /** * Data will never be cached (none) */ None: 'none', /** * Data will always be cached (always) */ Always: 'always', /** * Data will conditionally be cached (conditional) */ Conditional: 'conditional' }; class TypeParser { static parseInteger(val) { if (_.isNil(val)) return 0; else if (typeof val === 'number') return val; else if (typeof val === 'string') { if (val.match(IntegerRegex) || val.match(FloatRegex)) { return parseInt(val, 10); } else if (val.match(BooleanTrueRegex)) return 1; else if (val.match(BooleanFalseRegex)) return 0; } else if (typeof val === 'boolean') return val===true ? 1 : 0; else { return parseInt(val, 10) || 0; } } static parseCounter(val) { return TypeParser.parseInteger(val); } static parseFloat(val) { if (_.isNil(val)) return 0; else if (typeof val === 'number') return val; else if (typeof val === 'string') { if (val.match(IntegerRegex) || val.match(FloatRegex)) { return parseFloat(val); } else if (val.match(BooleanTrueRegex)) return 1; } else if (typeof val === 'boolean') return val===true ? 1 : 0; else { return parseFloat(val); } } static parseNumber(val) { return TypeParser.parseFloat(val); } static parseDateTime(val) { if (_.isNil(val)) return null; if (val instanceof Date) return val; if (typeof val === 'string') { if (val.match(DateTimeRegex)) return new Date(Date.parse(val)); } else if (typeof val === 'number') { return new Date(val); } return null; } static parseDate(val) { var res = this.parseDateTime(val); if (res instanceof Date) { res.setHours(0,0,0,0); return res; } return res; } static parseBoolean(val) { return (TypeParser.parseInteger(val)!==0); } static parseText(val) { if (_.isNil(val)) return val; else if (typeof val === 'string') { return val; } else { return val.toString(); } } static hasParser(type) { if (typeof type !== 'string') { return; } var descriptor = Object.getOwnPropertyDescriptor(TypeParser, 'parse' + type); if (descriptor == null) { return; } if (typeof descriptor.value === 'function') { return descriptor.value; } } } // backward compatibility issue (this constant should be removed in next version) var parsers = TypeParser; module.exports = { TypeParser, parsers, PrivilegeType, DataObjectState, DataCachingType, DataAdapter, DataContext, DataContextEmitter, DataEventArgs, DataEventListener, DataModelMigration, DataAssociationMapping, DataField, DataResultSet, DataModelEventListener, DataModelPrivilege };