From 33171e29317b5a8fbc8f637989148c947c35deb5 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Wed, 26 Feb 2025 10:52:54 +0200 Subject: [PATCH 1/4] convert DefaultDataContext to ES2015 class --- data-context.d.ts | 20 +- data-context.js | 560 ++++++++++++++++++++-------------------------- package-lock.json | 16 ++ package.json | 1 + types.d.ts | 10 +- types.js | 13 +- 6 files changed, 282 insertions(+), 338 deletions(-) diff --git a/data-context.d.ts b/data-context.d.ts index bac9b69b..bed9f423 100644 --- a/data-context.d.ts +++ b/data-context.d.ts @@ -1,18 +1,26 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved -import {DataAdapter, DataContext} from "./types"; -import { DataAdapterBase } from '@themost/common'; +import { Observable } from 'rxjs'; +import {DataContext} from "./types"; +import { ConfigurationBase, DataAdapterBase } from '@themost/common'; +import { DataModel } from 'data-model'; export declare class DefaultDataContext extends DataContext { + model(name: any): DataModel; + getConfiguration(): ConfigurationBase; + finalize(callback?: (err?: Error) => void): void; constructor(); readonly name: string; - getDb(): DataAdapterBase; - setDb(db: DataAdapterBase): void; + user$: Observable; + interactiveUser$: Observable; } export declare class NamedDataContext extends DataContext { + model(name: any): DataModel; + getConfiguration(): ConfigurationBase; + finalize(callback?: (err?: Error) => void): void; constructor(name: string); readonly name: string; getName(): string - getDb(): DataAdapterBase; - setDb(db: DataAdapterBase): void; + user$: Observable; + interactiveUser$: Observable; } diff --git a/data-context.js b/data-context.js index 1700133a..f05ada96 100644 --- a/data-context.js +++ b/data-context.js @@ -1,386 +1,316 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved -var _ = require('lodash'); -var {TraceUtils} = require('@themost/common'); -var {LangUtils} = require('@themost/common'); -var {DataContext} = require('./types'); -var {DataConfigurationStrategy} = require('./data-configuration'); -var cfg = require('./data-configuration'); -var Symbol = require('symbol'); -var nameProperty = Symbol('name'); -var { DataModel } = require('./data-model'); +/*eslint no-var: "error"*/ +const {DataError} = require('@themost/common'); +const {DataContext} = require('./types'); +const {DataConfigurationStrategy} = require('./data-configuration'); +const cfg = require('./data-configuration'); +const { DataModel } = require('./data-model'); +const { shareReplay, Observable } = require('rxjs'); + +class DefaultDataContext extends DataContext { + constructor() { + super(); + Object.defineProperty(this, '_db', { + writable: true, + enumerable: false, + configurable: true, + value: null + }); + Object.defineProperty(this, 'name', { + writable: true, + enumerable: false, + configurable: true, + value: 'default' + }); + + this.user$ = new Observable((observer) => { + void this.model('User').where( + (x, name) => x.name === name, this.user && this.user.name + ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); + }).pipe(shareReplay()); + + this.interactiveUser$ = new Observable((observer) => { + void this.model('User').where( + (x, name) => x.name === name, this.interactiveUser && this.interactiveUser.name + ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); + }).pipe(shareReplay()); -/** - * @classdesc Represents the default data context of MOST Data Applications. - * The default data context uses the adapter which is registered as the default adapter in application configuration. - * @description - ``` - adapters: [ - ... - { "name":"development", "invariantName":"...", "default":false, - "options": { - "server":"localhost", - "user":"user", - "password":"password", - "database":"test" - } -}, - { "name":"development_with_pool", "invariantName":"pool", "default":true, - "options": { - "adapter":"development" } -} - ... - ] - ``` - * @class - * @constructor - * @augments {DataContext} - * @property {DataAdapter} db - Gets a data adapter based on the current configuration settings. - */ -function DefaultDataContext() -{ - /** - * @type {import('./types').DataAdapter} - */ - var _db= null; - /** - * @name DataAdapter#hasConfiguration - * @type {Function} - * @param {Function} getConfigurationFunc - */ + /** - * @private + * Gets the current data adapter instance + * @returns {import('@themost/common').DataAdapterBase} */ - this._finalize = function(cb) { - if (_db) { - return _db.close(function(err) { - // destroy db context - _db = null; - return cb(err); - }); + get db() { + if (this._db) { + return this._db; } - return cb(); - }; - var self = this; - // set data context name with respect to DataContext implementation - var _name = 'default'; - Object.defineProperty(this, 'name', { - enumerable: false, - configurable: true, - get: function() { - return _name; - } - }); - - self.getDb = function() { - - if (_db) - return _db; - var er; - //otherwise load database options from configuration - var strategy = self.getConfiguration().getStrategy(DataConfigurationStrategy); - var adapter = _.find(strategy.adapters, function(x) { - return x['default']; + // otherwise, load database options from configuration + const strategy = this.getConfiguration().getStrategy(DataConfigurationStrategy); + const adapter = strategy.adapters.find((x) => { + return x.default === true; }); - if (_.isNil(adapter)) { - er = new Error('The default data adapter is missing.'); er.code = 'EADAPTER'; - throw er; + if (adapter == null) { + throw new DataError('ERR_ADAPTER', 'The default data adapter is missing.'); } /** * @type {*} */ - var adapterType = strategy.adapterTypes.get(adapter.invariantName); + const adapterType = strategy.adapterTypes.get(adapter.invariantName); //validate data adapter type if (adapterType == null) { - er = new Error('Invalid adapter type.'); er.code = 'ERR_ADAPTER_TYPE'; - throw er; + throw new DataError('ERR_ADAPTER_TYPE', 'Invalid adapter type.'); } // get data adapter instance - var createInstance = adapterType.createInstance; + const createInstance = adapterType.createInstance; // get adapter type constructor if any - var AdapterTypeCtor = adapterType.type; + const AdapterTypeCtor = adapterType.type; // create adapter instance - if (typeof AdapterTypeCtor === 'function') { - _db = new AdapterTypeCtor(adapter.options); + if (typeof AdapterTypeCtor === 'function') { + this._db = new AdapterTypeCtor(adapter.options); } else if (typeof createInstance === 'function') { - _db = createInstance(adapter.options); + this._db = createInstance(adapter.options); } else { // throw error - var err = new Error('The given adapter type is invalid. Adapter type constructor is undefined.'); - err.code = 'ERR_ADAPTER_TYPE'; - throw err; + throw new DataError('ERR_ADAPTER_TYPE', 'The given adapter type is invalid. Adapter type constructor is undefined.'); } - if (typeof _db.hasConfiguration === 'function') { - _db.hasConfiguration(function() { - return self.getConfiguration(); + if (typeof this._db.hasConfiguration === 'function') { + this._db.hasConfiguration(() => { + return this.getConfiguration(); }); } - return _db; - }; + return this._db; + } - self.setDb = function(value) { - /** - * @type {DataAdapter|*} - */ - _db = value; - if (_db) { - if (typeof _db.hasConfiguration === 'function') { - _db.hasConfiguration(function() { - return self.getConfiguration(); + /** + * Sets a data adapter instance + * @param {import('@themost/common').DataAdapterBase} value + */ + set db(value) { + this._db = value; + if (this._db) { + if (typeof this._db.hasConfiguration === 'function') { + this._db.hasConfiguration(() => { + return this.getConfiguration(); }); } } - }; - - delete self.db; - - Object.defineProperty(self, 'db', { - get: function() { - return self.getDb(); - }, - set: function(value) { - self.setDb(value); - }, - configurable: true, - enumerable:false - }); - -} - -LangUtils.inherits(DefaultDataContext, DataContext); - -/** - * Gets an instance of DataConfiguration class which is associated with this data context - * @returns {import('@themost/common').ConfigurationBase} - */ -DefaultDataContext.prototype.getConfiguration = function() { - return cfg.getCurrent(); -}; + } -/** - * Gets an instance of DataModel class based on the given name. - * @param {*} name - A variable that represents the model name. - * @returns {DataModel} - An instance of DataModel class associated with this data context. - */ -DefaultDataContext.prototype.model = function(name) { - var self = this; - if (name == null) { - return null; + /** + * Gets an instance of DataConfiguration class which is associated with this data context + * @returns {import('@themost/common').ConfigurationBase} + */ + getConfiguration() { + return cfg.getCurrent(); } - var modelName = name; - // if model is a function (is a constructor) - if (typeof name === 'function') { - // try to get EdmMapping.entityType() decorator - if (Object.prototype.hasOwnProperty.call(name, 'entityTypeDecorator')) { - // if entityTypeDecorator is string - if (typeof name.entityTypeDecorator === 'string') { - // get model name - modelName = name.entityTypeDecorator; + /** + * Gets an instance of DataModel class based on the given name. + * @param {*} name - A variable that represents the model name. + * @returns {DataModel} - An instance of DataModel class associated with this data context. + */ + model(name) { + const self = this; + if (name == null) { + return null; + } + let modelName = name; + // if model is a function (is a constructor) + if (typeof name === 'function') { + // try to get EdmMapping.entityType() decorator + if (Object.prototype.hasOwnProperty.call(name, 'entityTypeDecorator')) { + // if entityTypeDecorator is string + if (typeof name.entityTypeDecorator === 'string') { + // get model name + modelName = name.entityTypeDecorator; + } + } else { + // get function name as the requested model + modelName = name.name; } - } else { - // get function name as the requested model - modelName = name.name; } + const obj = this.getConfiguration().getStrategy(DataConfigurationStrategy).model(modelName); + if (obj == null) + return null; + const model = new DataModel(obj); + //set model context + model.context = self; + //return model + return model; } - var obj = self.getConfiguration().getStrategy(DataConfigurationStrategy).model(modelName); - if (_.isNil(obj)) - return null; - var model = new DataModel(obj); - //set model context - model.context = self; - //return model - return model; -}; -/** - * Finalizes the current data context - * @param {Function} cb - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. - */ -DefaultDataContext.prototype.finalize = function(cb) { - cb = cb || function () {}; - void this._finalize(function(err) { - if (err) { - TraceUtils.error('An error occurred while finalizing the underlying database context.'); - TraceUtils.error(err); - } - return cb(); - }); -}; - - -/** - * @classdesc Represents a data context based on a data adapter's name. - * The specified adapter name must be registered in application configuration. - * @class - * @constructor - * @augments DataContext - * @property {DataAdapter} db - Gets a data adapter based on the given adapter's name. - */ -function NamedDataContext(name) -{ - NamedDataContext.super_.bind(this)(); /** - * @type {DataAdapter} - * @private + * Finalizes the current data context + * @param {Function} cb - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. */ - var _db; - /** - * @private - */ - this._finalize = function(cb) { - if (_db) { - return _db.close(function(err) { - // destroy db context - _db = null; + finalize(cb) { + cb = cb || function () { }; + if (this._db) { + return this._db.close((err) => { + this._db = null; return cb(err); }); } return cb(); - }; - var self = this; - self[nameProperty] = name; + } +} + +class NamedDataContext extends DataContext { + /** + * @type {string} name + */ + constructor(name) { + + super(); + + Object.defineProperty(this, '_db', { + writable: true, + enumerable: false, + configurable: true, + value: null + }); + + Object.defineProperty(this, 'name', { + writable: false, + enumerable: false, + configurable: true, + value: name + }); + + this.user$ = new Observable((observer) => { + void this.model('User').where( + (x, name) => x.name === name, this.user && this.user.name + ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); + }).pipe(shareReplay(1)); + + this.interactiveUser$ = new Observable((observer) => { + void this.model('User').where( + (x, name) => x.name === name, this.interactiveUser && this.interactiveUser.name + ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); + }).pipe(shareReplay(1)); - self.getDb = function() { - if (_db) - return _db; - var strategy = self.getConfiguration().getStrategy(DataConfigurationStrategy); + } + + get db() { + if (this._db) + return this._db; + const strategy = this.getConfiguration().getStrategy(DataConfigurationStrategy); //otherwise load database options from configuration - var adapter = strategy.adapters.find(function(x) { - return x.name === self[nameProperty]; + const adapter = strategy.adapters.find((x) => { + return x.name === this.name; }); - var er; - if (typeof adapter ==='undefined' || adapter===null) { - er = new Error('The specified data adapter is missing.'); er.code = 'EADAPTER'; - throw er; + if (typeof adapter === 'undefined' || adapter === null) { + throw new DataError('ERR_ADAPTER', 'The specified data adapter is missing.'); } //get data adapter type - var adapterType = strategy.adapterTypes.get(adapter.invariantName); + const adapterType = strategy.adapterTypes.get(adapter.invariantName); // get data adapter instance - var createInstance = adapterType.createInstance; + const createInstance = adapterType.createInstance; // get adapter type constructor if any - var AdapterTypeCtor = adapterType.type; + const AdapterTypeCtor = adapterType.type; // create adapter instance - if (typeof AdapterTypeCtor === 'function') { - _db = new AdapterTypeCtor(adapter.options); + if (typeof AdapterTypeCtor === 'function') { + this._db = new AdapterTypeCtor(adapter.options); } else if (typeof createInstance === 'function') { - _db = createInstance(adapter.options); + this._db = createInstance(adapter.options); } else { // throw error - var err = new Error('The given adapter type is invalid. Adapter type constructor is undefined.'); - err.code = 'ERR_ADAPTER_TYPE'; - throw err; + throw new DataError('ERR_ADAPTER_TYPE', 'The given adapter type is invalid. Adapter type constructor is undefined.'); } - if (typeof _db.hasConfiguration === 'function') { - _db.hasConfiguration(function() { - return self.getConfiguration(); + if (typeof this._db.hasConfiguration === 'function') { + this._db.hasConfiguration(() => { + return this.getConfiguration(); }); } - return _db; - }; + return this._db; + } - /** - * @param {DataAdapter|*} value - */ - self.setDb = function(value) { - _db = value; - if (_db) { - if (typeof _db.hasConfiguration === 'function') { - _db.hasConfiguration(function() { - return self.getConfiguration(); + set db(value) { + this._db = value; + if (this._db) { + if (typeof this._db.hasConfiguration === 'function') { + this._db.hasConfiguration(() => { + return this.getConfiguration(); }); } } - - }; + } /** - * @name NamedDataContext#db - * @type {DataAdapter} + * Gets the current user + * @returns {import('rxjs').Observable<{id: string, name: string, groups: Array<{id: string, name: string}>}>} */ - - Object.defineProperty(self, 'db', { - get : function() { - return self.getDb(); - }, - set : function(value) { - self.setDb(value); - }, - configurable : true, - enumerable:false }); - + getUser() { + return this.user$; + } /** - * @name NamedDataContext#name - * @type {string} + * Gets the interactive user + * @returns {import('rxjs').Observable<{id: string, name: string, groups: Array<{id: string, name: string}>}>} */ - Object.defineProperty(self, 'name', { - get: function () { - return self[nameProperty]; - } - }); - -} -LangUtils.inherits(NamedDataContext, DataContext); - -/** - * Gets a string which represents the name of this context - * @returns {string} - */ -NamedDataContext.prototype.getName = function() { - return this[nameProperty]; -}; - -/** - * Gets an instance of DataConfiguration class which is associated with this data context - * @returns {DataConfiguration} - */ -NamedDataContext.prototype.getConfiguration = function() { - return cfg.getNamedConfiguration(this.name); -}; -/** - * Gets an instance of DataModel class based on the given name. - * @param name {string} - A string that represents the model name. - * @returns {DataModel} - An instance of DataModel class associated with this data context. - */ -NamedDataContext.prototype.model = function(name) { - var self = this; - if (name == null) { - return null; + getInteractiveUser() { + return this.interactiveUser$$; + } + /** + * Gets a string which represents the name of this context + * @returns {string} + */ + getName() { + return this.name; + } + /** + * Gets an instance of DataConfiguration class which is associated with this data context + * @returns {DataConfiguration} + */ + getConfiguration() { + return cfg.getNamedConfiguration(this.name); } - var modelName = name; - // if model is a function (is a constructor) - if (typeof name === 'function') { - // try to get EdmMapping.entityType() decorator - if (Object.prototype.hasOwnProperty.call(name, 'entityTypeDecorator')) { - // if entityTypeDecorator is string - if (typeof name.entityTypeDecorator === 'string') { - // get model name - modelName = name.entityTypeDecorator; + /** + * Gets an instance of DataModel class based on the given name. + * @param name {string} - A string that represents the model name. + * @returns {DataModel} - An instance of DataModel class associated with this data context. + */ + model(name) { + const self = this; + if (name == null) { + return null; + } + let modelName = name; + // if model is a function (is a constructor) + if (typeof name === 'function') { + // try to get EdmMapping.entityType() decorator + if (Object.prototype.hasOwnProperty.call(name, 'entityTypeDecorator')) { + // if entityTypeDecorator is string + if (typeof name.entityTypeDecorator === 'string') { + // get model name + modelName = name.entityTypeDecorator; + } + } else { + // get function name as the requested model + modelName = name.name; } - } else { - // get function name as the requested model - modelName = name.name; } + const obj = this.getConfiguration().getStrategy(DataConfigurationStrategy).model(modelName); + if (obj == null) + return null; + const model = new DataModel(obj); + //set model context + model.context = self; + //return model + return model; + } - var obj = self.getConfiguration().getStrategy(DataConfigurationStrategy).model(modelName); - if (_.isNil(obj)) - return null; - var model = new DataModel(obj); - //set model context - model.context = self; - //return model - return model; -}; -NamedDataContext.prototype.finalize = function(cb) { - cb = cb || function () {}; - this._finalize(function(err) { - if (err) { - TraceUtils.error('An error occurred while finalizing the underlying database context.'); - TraceUtils.error(err); + finalize(cb) { + cb = cb || function () { }; + if (this._db) { + return this._db.close((err) => { + this._db = null; + return cb(err); + }); } - cb(); - }); -}; + return cb(); + } +} module.exports = { DefaultDataContext, diff --git a/package-lock.json b/package-lock.json index b4e83af3..cd8c8dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "node-cache": "^1.1.0", "pluralize": "^7.0.0", "q": "^1.4.1", + "rxjs": "^7.8.2", "sprintf-js": "^1.1.2", "symbol": "^0.3.1", "uuid": "^10.0.0" @@ -8815,6 +8816,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -9420,6 +9430,12 @@ "typescript": ">=2.7" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 2a1bcca8..d4c187f5 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "node-cache": "^1.1.0", "pluralize": "^7.0.0", "q": "^1.4.1", + "rxjs": "^7.8.2", "sprintf-js": "^1.1.2", "symbol": "^0.3.1", "uuid": "^10.0.0" diff --git a/types.d.ts b/types.d.ts index 2f6129b2..3a666fbd 100644 --- a/types.d.ts +++ b/types.d.ts @@ -75,19 +75,19 @@ export declare interface ContextUser extends ContextUserBase { } -export declare class DataContext extends SequentialEventEmitter implements DataContextBase { +export declare abstract class DataContext extends SequentialEventEmitter implements DataContextBase { user?: ContextUser; interactiveUser?: ContextUser; - model(name:any): DataModel; - db: DataAdapterBase; - getConfiguration(): ConfigurationBase; + abstract model(name:any): DataModel; + + abstract getConfiguration(): ConfigurationBase; - finalize(callback?:(err?:Error) => void): void; + abstract finalize(callback?:(err?:Error) => void): void; finalizeAsync(): Promise; diff --git a/types.js b/types.js index 620ddc1c..e457ed51 100644 --- a/types.js +++ b/types.js @@ -295,18 +295,7 @@ function DataContext() { if (this.constructor === DataContext.prototype.constructor) { throw new AbstractClassError(); } - /** - * @property db - * @description Gets the current database adapter - * @type {DataAdapter} - * @memberOf DataContext# - */ - Object.defineProperty(this, 'db', { - get : function() { - return null; - }, - configurable : true, - enumerable:false }); + } // noinspection JSUnusedLocalSymbols /** From 8fdd08e0e48a38b592a302063a7ffbebc31b1864 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Wed, 26 Feb 2025 10:58:37 +0200 Subject: [PATCH 2/4] add getUser() and getInteractiveUser() --- data-context.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/data-context.js b/data-context.js index f05ada96..c8c0fd72 100644 --- a/data-context.js +++ b/data-context.js @@ -152,6 +152,21 @@ class DefaultDataContext extends DataContext { } return cb(); } + + /** + * Gets the current user + * @returns {import('rxjs').Observable} + */ + getUser() { + return this.user$; + } + /** + * Gets the interactive user + * @returns {import('rxjs').Observable} + */ + getInteractiveUser() { + return this.interactiveUser$; + } } class NamedDataContext extends DataContext { @@ -191,14 +206,15 @@ class NamedDataContext extends DataContext { } get db() { - if (this._db) + if (this._db) { return this._db; + } const strategy = this.getConfiguration().getStrategy(DataConfigurationStrategy); //otherwise load database options from configuration const adapter = strategy.adapters.find((x) => { return x.name === this.name; }); - if (typeof adapter === 'undefined' || adapter === null) { + if (adapter == null) { throw new DataError('ERR_ADAPTER', 'The specified data adapter is missing.'); } //get data adapter type @@ -247,7 +263,7 @@ class NamedDataContext extends DataContext { * @returns {import('rxjs').Observable<{id: string, name: string, groups: Array<{id: string, name: string}>}>} */ getInteractiveUser() { - return this.interactiveUser$$; + return this.interactiveUser$; } /** * Gets a string which represents the name of this context From 9217deb8b2b9f3c3e501012755494a3f2a106843 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Thu, 27 Feb 2025 10:46:35 +0200 Subject: [PATCH 3/4] use context user observables --- UnattendedMode.js | 4 +- data-context.d.ts | 6 +- data-context.js | 53 --------------- functions.js | 64 ++++-------------- spec/DataAttributeResolver.spec.ts | 33 +++------ spec/DataContext.spec.ts | 65 ++++++++++++++++++ spec/DataPrivileges.spec.ts | 15 ++--- spec/DataValidator.spec.ts | 8 +-- types.d.ts | 59 ++++++++++++++++ types.js | 105 ++++++++++++++++++++++++++++- 10 files changed, 264 insertions(+), 148 deletions(-) create mode 100644 spec/DataContext.spec.ts diff --git a/UnattendedMode.js b/UnattendedMode.js index 462bee28..4b233e69 100644 --- a/UnattendedMode.js +++ b/UnattendedMode.js @@ -45,7 +45,7 @@ function executeInUnattendedMode(context, callable, callback) { if (interactiveUser) { context.user = Object.assign({}, interactiveUser); } - delete context.interactiveUser; + context.interactiveUser = null; // exit unattended mode delete context[unattendedMode]; return callback(err); @@ -55,7 +55,7 @@ function executeInUnattendedMode(context, callable, callback) { if (interactiveUser) { context.user = Object.assign({}, interactiveUser); } - delete context.interactiveUser; + context.interactiveUser = null; // exit unattended mode delete context[unattendedMode]; return callback(err); diff --git a/data-context.d.ts b/data-context.d.ts index bed9f423..b2ee0f66 100644 --- a/data-context.d.ts +++ b/data-context.d.ts @@ -10,8 +10,6 @@ export declare class DefaultDataContext extends DataContext { finalize(callback?: (err?: Error) => void): void; constructor(); readonly name: string; - user$: Observable; - interactiveUser$: Observable; } export declare class NamedDataContext extends DataContext { @@ -20,7 +18,5 @@ export declare class NamedDataContext extends DataContext { finalize(callback?: (err?: Error) => void): void; constructor(name: string); readonly name: string; - getName(): string - user$: Observable; - interactiveUser$: Observable; + getName(): string; } diff --git a/data-context.js b/data-context.js index c8c0fd72..57acb918 100644 --- a/data-context.js +++ b/data-context.js @@ -5,7 +5,6 @@ const {DataContext} = require('./types'); const {DataConfigurationStrategy} = require('./data-configuration'); const cfg = require('./data-configuration'); const { DataModel } = require('./data-model'); -const { shareReplay, Observable } = require('rxjs'); class DefaultDataContext extends DataContext { constructor() { @@ -23,18 +22,6 @@ class DefaultDataContext extends DataContext { value: 'default' }); - this.user$ = new Observable((observer) => { - void this.model('User').where( - (x, name) => x.name === name, this.user && this.user.name - ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); - }).pipe(shareReplay()); - - this.interactiveUser$ = new Observable((observer) => { - void this.model('User').where( - (x, name) => x.name === name, this.interactiveUser && this.interactiveUser.name - ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); - }).pipe(shareReplay()); - } /** @@ -153,20 +140,6 @@ class DefaultDataContext extends DataContext { return cb(); } - /** - * Gets the current user - * @returns {import('rxjs').Observable} - */ - getUser() { - return this.user$; - } - /** - * Gets the interactive user - * @returns {import('rxjs').Observable} - */ - getInteractiveUser() { - return this.interactiveUser$; - } } class NamedDataContext extends DataContext { @@ -191,18 +164,6 @@ class NamedDataContext extends DataContext { value: name }); - this.user$ = new Observable((observer) => { - void this.model('User').where( - (x, name) => x.name === name, this.user && this.user.name - ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); - }).pipe(shareReplay(1)); - - this.interactiveUser$ = new Observable((observer) => { - void this.model('User').where( - (x, name) => x.name === name, this.interactiveUser && this.interactiveUser.name - ).expand((x) => x.groups).getItem().then((result) => observer.next(result)).catch((err) => observer.error(err)); - }).pipe(shareReplay(1)); - } get db() { @@ -251,20 +212,6 @@ class NamedDataContext extends DataContext { } } - /** - * Gets the current user - * @returns {import('rxjs').Observable<{id: string, name: string, groups: Array<{id: string, name: string}>}>} - */ - getUser() { - return this.user$; - } - /** - * Gets the interactive user - * @returns {import('rxjs').Observable<{id: string, name: string, groups: Array<{id: string, name: string}>}>} - */ - getInteractiveUser() { - return this.interactiveUser$; - } /** * Gets a string which represents the name of this context * @returns {string} diff --git a/functions.js b/functions.js index 220548cf..07c73a5e 100644 --- a/functions.js +++ b/functions.js @@ -1,12 +1,8 @@ -// MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved /*eslint no-var: "off"*/ -// noinspection ES6ConvertVarToLetConst - -var {TypeParser} = require('./types'); -var {TraceUtils} = require('@themost/common'); // eslint-disable-next-line no-unused-vars var moment = require('moment'); var _ = require('lodash'); +var { firstValueFrom } = require('rxjs'); /** * @class @@ -246,51 +242,19 @@ FunctionContext.prototype.password = function(length) { /** * @returns {Promise} */ -FunctionContext.prototype.user = function() { - var self = this; - // backward compatibility issue (get FunctionContext.model.context first) - var context = (self.model && self.model.context) || self.context; - var user = context.interactiveUser || context.user || { }; - return new Promise(function(resolve) { - if (typeof user.id !== 'undefined') { - return resolve(user.id); - } - var userModel = context.model('User'); - var parser; - var undefinedUser = null; - userModel.where('name').equal(user.name).silent().select('id').first(function(err, result) { - if (err) { - TraceUtils.log(err); - //try to get undefined user - parser = TypeParser.hasParser(userModel.field('id').type); - if (typeof parser === 'function') - undefinedUser = parser(null); - //set id for next calls - user.id = undefinedUser; - if (context.user == null) { - context.user = user; - } - return resolve(undefinedUser); - } - else if (result == null) { - //try to get undefined user - parser = TypeParser.hasParser(userModel.field('id').type); - if (typeof parser === 'function') - undefinedUser = parser(null); - //set id for next calls - user.id = undefinedUser; - if (context.user == null) { - context.user = user; - } - return resolve(undefinedUser); - } - else { - //set id for next calls - user.id = result.id; - return resolve(result.id); - } - }); - }); +FunctionContext.prototype.user = async function() { + /** + * @type {import('./types').DataContext} + */ + var context = (this.model && this.model.context) || this.context; + if (context.interactiveUser) { + const interactiveUser = await firstValueFrom(context.interactiveUser$); + return interactiveUser && interactiveUser.id ? interactiveUser.id : null; + } else if (context.user) { + const user = await firstValueFrom(context.user$); + return user && user.id ? user.id : null; + } + return null; }; /** * @returns {Promise|*} diff --git a/spec/DataAttributeResolver.spec.ts b/spec/DataAttributeResolver.spec.ts index 0538749e..91b51a08 100644 --- a/spec/DataAttributeResolver.spec.ts +++ b/spec/DataAttributeResolver.spec.ts @@ -3,6 +3,7 @@ import { DataContext } from '../index'; import { TestApplication, TestApplication2 } from './TestApplication'; import { TestUtils } from './adapter/TestUtils'; import { promisify } from 'util'; +import { firstValueFrom } from 'rxjs'; describe('DataAttributeResolver', () => { let app: TestApplication; @@ -16,10 +17,8 @@ describe('DataAttributeResolver', () => { await app.finalize(); }) it('should resolve child nested attributes', async () => { - Object.assign(context, { - user: { - name: 'anonymous' - } + context.switchUser({ + name: 'anonymous' }); let items = await context.model('Product').select( 'id', @@ -31,10 +30,8 @@ describe('DataAttributeResolver', () => { expect(item.orderID).toBe(null); expect(item.customer).toBe(null); } - Object.assign(context, { - user: { - name: 'luis.nash@example.com' - } + context.switchUser({ + name: 'luis.nash@example.com' }); const customer = await context.model('Person').where('user/name').equal('luis.nash@example.com').getItem(); expect(customer).toBeTruthy(); @@ -46,9 +43,7 @@ describe('DataAttributeResolver', () => { }); it('should resolve parent nested attributes', async () => { - Object.assign(context, { - user: null - }); + context.switchUser(); const items = await context.model('Order').select( 'id', 'orderedItem/name as productName', @@ -73,10 +68,8 @@ describe('DataAttributeResolver', () => { ] } ]); - Object.assign(context, { - user: { - name: 'michael.barret@example.com' - } + context.switchUser({ + name: 'michael.barret@example.com' }); let items = await context.model('Order').select('Delivered').getList(); expect(items).toBeTruthy(); @@ -93,10 +86,8 @@ describe('DataAttributeResolver', () => { ] }; await context.model('User').silent().save(newUser); - Object.assign(context, { - user: { - name: 'tom.hutchinson@example.com' - } + context.switchUser({ + name: 'tom.hutchinson@example.com' }); items = await context.model('Order').select('Delivered').getList(); expect(items.value.length).toBe(0); @@ -117,9 +108,7 @@ describe('DataAttributeResolver', () => { it('should get nested item', async () => { await TestUtils.executeInTransaction(context, async () => { - Object.assign(context, { - user: null - }); + context.switchUser(null); const product = await context.model('Product').asQueryable().silent().getItem(); product.productImage = { url: '/images/products/abc.png' diff --git a/spec/DataContext.spec.ts b/spec/DataContext.spec.ts new file mode 100644 index 00000000..c6ad49a7 --- /dev/null +++ b/spec/DataContext.spec.ts @@ -0,0 +1,65 @@ +import { TestApplication, TestApplication2 } from './TestApplication'; +import { DataContext, DataModel, DataObjectAssociationError, DataQueryable, DefaultDataContext, FunctionContext } from '../index'; +import { resolve } from 'path'; +import { TestUtils } from './adapter/TestUtils'; +import { firstValueFrom } from 'rxjs'; + +describe('DataContext', () => { + + let app: TestApplication; + let context: DefaultDataContext; + beforeAll((done) => { + app = new TestApplication2(); + context = app.createContext() as DefaultDataContext; + return done(); + }); + afterAll(async () => { + await context.finalizeAsync(); + await app.finalize(); + }); + + it('should create a new DataContext',async () => { + expect(context).toBeDefined(); + context.user = { + name: 'alexis.rees@example.com' + }; + const getItemSpy = jest.spyOn(DataQueryable.prototype, 'getItem'); + let user = await firstValueFrom(context.user$); + expect(getItemSpy).toHaveBeenCalled(); + expect(user).toBeDefined(); + expect(user.name).toEqual('alexis.rees@example.com'); + getItemSpy.mockClear(); + context.switchUser({ + name: 'anonymous' + }) + user = await firstValueFrom(context.user$); + expect(getItemSpy).toHaveBeenCalled(); + getItemSpy.mockClear(); + user = await firstValueFrom(context.user$); + expect(getItemSpy).not.toHaveBeenCalled(); + expect(user).toBeDefined(); + }); + + it('should use function context',async () => { + expect(context).toBeDefined(); + context.user = { + name: 'alexis.rees@example.com' + }; + const getItemSpy = jest.spyOn(DataQueryable.prototype, 'getItem'); + const functionContext = new FunctionContext(context); + let user = await functionContext.me(); + expect(getItemSpy).toHaveBeenCalled(); + expect(user).toBeDefined(); + getItemSpy.mockClear(); + context.switchUser({ + name: 'anonymous' + }) + user = await functionContext.me(); + expect(getItemSpy).toHaveBeenCalled(); + getItemSpy.mockClear(); + user = await functionContext.me(); + expect(getItemSpy).not.toHaveBeenCalled(); + expect(user).toBeDefined(); + }); + +}); \ No newline at end of file diff --git a/spec/DataPrivileges.spec.ts b/spec/DataPrivileges.spec.ts index 49e1c5dc..9972a7cf 100644 --- a/spec/DataPrivileges.spec.ts +++ b/spec/DataPrivileges.spec.ts @@ -35,10 +35,9 @@ describe('Permissions', () => { it('should validate create access', async () => { const Products = context.model('Product'); // set context user - Object.assign(context, { - user: { - name: 'christina.ali@example.com' - }}); + context.switchUser({ + name: 'christina.ali@example.com' + }); const orderedItem = await Products.where('name').equal( 'Apple MacBook Air (13.3-inch, 2013 Version)' ).getItem(); @@ -76,9 +75,7 @@ describe('Permissions', () => { expect(group).toBeTruthy(); const members = group.property('members').silent(); await members.insert(user); - Object.assign(context, { - user - }); + context.switchUser(user); const user1 = await context.model('User').find(user) .expand('groups').silent().getItem(); expect(user1).toBeTruthy(); @@ -113,9 +110,7 @@ describe('Permissions', () => { const user = { name: 'margaret.davis@example.com' }; - Object.assign(context, { - user - }); + context.switchUser(user); const group = await context.model('Group').where('name').equal('Contributors').getTypedItem(); expect(group).toBeTruthy(); const members = group.property('members').silent(); diff --git a/spec/DataValidator.spec.ts b/spec/DataValidator.spec.ts index cdc1e54d..6309e667 100644 --- a/spec/DataValidator.spec.ts +++ b/spec/DataValidator.spec.ts @@ -43,7 +43,7 @@ describe('DataValidator', () => { await expect(People.save(item)).resolves.toBeTruthy(); item.faxNumber = '301234567890'; await expect(People.save(item)).rejects.toThrowError('Fax number should with "+" e.g. +301234567890'); - delete context.user; + context.switchUser(); }); }); @@ -71,7 +71,7 @@ describe('DataValidator', () => { name: 'Lenovo Yoga 2 Pro' } })).rejects.toThrowError(field.validation.message); - delete context.user; + context.switchUser(); }); }); @@ -101,7 +101,7 @@ describe('DataValidator', () => { name: 'Lenovo Yoga 2 Pro' } })).rejects.toThrowError(field.validation.message); - delete context.user; + context.switchUser(); }); }); @@ -129,7 +129,7 @@ describe('DataValidator', () => { name: 'Lenovo Yoga 2 Pro' } })).rejects.toThrowError(field.validation.message); - delete context.user; + context.switchUser(); }); }); diff --git a/types.d.ts b/types.d.ts index 3a666fbd..791549b2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -2,6 +2,7 @@ import {DataModel} from "./data-model"; import {ConfigurationBase, SequentialEventEmitter, DataAdapterBase, DataAdapterMigration, DataContextBase, ContextUserBase} from "@themost/common"; import {DataAssociationMappingBase, DataFieldBase} from '@themost/common'; +import { BehaviorSubject, Observable } from "rxjs"; export declare function DataAdapterCallback(err?:Error, result?:any): void; @@ -77,21 +78,79 @@ export declare interface ContextUser extends ContextUserBase { export declare abstract class DataContext extends SequentialEventEmitter implements DataContextBase { + /** + * Optional property representing the use of the current context. + * + * @type {ContextUser} + */ user?: ContextUser; + /** + * Optional property representing the interactive user of the current context. + * The interactive user is the original user who initiated the current context. + */ interactiveUser?: ContextUser; + /** + * An observable stream that emits user-related data. + * + * @type {Observable} + */ + user$: Observable; + + /** + * An observable stream that emits interactive user-related data. + * + * @type {Observable} + */ + interactiveUser$: Observable; + + /** + * The database adapter instance used for interacting with the database. + * This property provides the necessary methods and properties to perform + * database operations such as querying, inserting, updating, and deleting records. + */ db: DataAdapterBase; + /** + * Returns an instance of the data model with the specified name. + * @param {*} name + */ abstract model(name:any): DataModel; + /** + * Returns the configuration service of the parent application. + * @returns {ConfigurationBase} + */ abstract getConfiguration(): ConfigurationBase; + /** + * Finalizes the current context and releases all resources. + * @param {(err?: Error) => void} callback + */ abstract finalize(callback?:(err?:Error) => void): void; + /** + * Finalizes the current context and releases all resources. + * @returns {Promise} + */ finalizeAsync(): Promise; + /** + * Executes the specified function within a transaction. + * A transaction is a set of operations that are executed as a single unit of work. + * @param func + */ executeInTransactionAsync(func: () => Promise): Promise; + + switchUser(user?: ContextUser): void; + + setUser(user?: ContextUser): void; + + switchInteractiveUser(user?: ContextUser): void; + + setInteractiveUser(user?: ContextUser): void; + } export declare class DataContextEmitter { diff --git a/types.js b/types.js index e457ed51..bd74d087 100644 --- a/types.js +++ b/types.js @@ -1,6 +1,7 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved var _ = require('lodash'); var {SequentialEventEmitter, LangUtils, AbstractClassError, AbstractMethodError} = require('@themost/common'); +var { shareReplay, Observable, BehaviorSubject, switchMap, of } = require('rxjs'); /** * @classdesc Represents an abstract data connector to a database @@ -286,7 +287,6 @@ function DataEventArgs() { * @classdesc Represents the main data context. * @class * @augments SequentialEventEmitter - * @constructor * @abstract */ function DataContext() { @@ -295,7 +295,55 @@ function DataContext() { if (this.constructor === DataContext.prototype.constructor) { throw new AbstractClassError(); } - + + this.refreshUser$ = new BehaviorSubject(void 0); + + this.user$ = new Observable(observer => observer.next()).pipe(switchMap(() => { + return this.getUser(); + }), shareReplay(1)); + + this.refreshInteractiveUser$ = new BehaviorSubject(void 0); + + this.interactiveUser$ = new Observable(observer => observer.next()).pipe(switchMap(() => { + return this.getInteractiveUser(); + }), shareReplay(1)); + + var _user = null; + Object.defineProperty(this, 'user', { + get: function() { + return _user; + }, + set: function(value) { + const previous = _user; + _user = value; + if ((previous && previous.name) !== (value && value.name)) { + this.user$ = new Observable(observer => observer.next()).pipe(switchMap(() => { + return this.getUser(); + }), shareReplay(1)); + } + }, + configurable: false, + enumerable: false + }); + + var _interactiveUser = null; + Object.defineProperty(this, 'interactiveUser', { + get: function() { + return _interactiveUser; + }, + set: function(value) { + const previous = _interactiveUser; + _interactiveUser = value; + if ((previous && previous.name) !== (value && value.name)) { + this.interactiveUser$ = new Observable(observer => observer.next()).pipe(switchMap(() => { + return this.getInteractiveUser(); + }), shareReplay(1)); + } + }, + configurable: false, + enumerable: false + }); + } // noinspection JSUnusedLocalSymbols /** @@ -308,6 +356,59 @@ function DataContext() { DataContext.prototype.model = function(name) { throw new AbstractMethodError(); }; +/** + * + * @returns {Observable} + */ +DataContext.prototype.getUser = function() { + return new Observable((observer) => { + if ((this.user && this.user.name) == null) { + return observer.next(null); + } + void this.model('User').where( + (x, name) => x.name === name, this.user && this.user.name + ).expand((x) => x.groups).silent().getItem().then((result) => { + return observer.next(result); + }).catch((err) => { + return observer.error(err); + }); + }); +}; + +DataContext.prototype.switchUser = function(user) { + this.user = user; +}; + +DataContext.prototype.setUser = function(user) { + this.user = user; +}; + +/** + * + * @returns {Observable} + */ +DataContext.prototype.getInteractiveUser = function() { + return new Observable((observer) => { + if ((this.interactiveUser && this.interactiveUser.name) == null) { + return observer.next(null); + } + void this.model('User').where( + (x, name) => x.name === name, this.interactiveUser && this.interactiveUser.name + ).expand((x) => x.groups).silent().getItem().then((result) => { + return observer.next(result) + }).catch((err) => { + return observer.error(err); + }); + }); +}; + +DataContext.prototype.switchInteractiveUser = function(user) { + this.interactiveUser = user; +}; + +DataContext.prototype.setInteractiveUser = function(user) { + this.interactiveUser = user; +}; /** * Gets an instance of DataConfiguration class which is associated with this data context From e76f3e923494217c18213db7ce99994b8b769233 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Thu, 27 Feb 2025 12:48:53 +0200 Subject: [PATCH 4/4] remove unused imports --- data-context.d.ts | 3 +-- types.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/data-context.d.ts b/data-context.d.ts index b2ee0f66..94f68bc7 100644 --- a/data-context.d.ts +++ b/data-context.d.ts @@ -1,8 +1,7 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved -import { Observable } from 'rxjs'; import {DataContext} from "./types"; import { ConfigurationBase, DataAdapterBase } from '@themost/common'; -import { DataModel } from 'data-model'; +import { DataModel } from './data-model'; export declare class DefaultDataContext extends DataContext { model(name: any): DataModel; diff --git a/types.js b/types.js index bd74d087..98d92804 100644 --- a/types.js +++ b/types.js @@ -1,7 +1,7 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved var _ = require('lodash'); var {SequentialEventEmitter, LangUtils, AbstractClassError, AbstractMethodError} = require('@themost/common'); -var { shareReplay, Observable, BehaviorSubject, switchMap, of } = require('rxjs'); +var { shareReplay, Observable, BehaviorSubject, switchMap } = require('rxjs'); /** * @classdesc Represents an abstract data connector to a database