/* global _App */
if (typeof _App === 'undefined') {
throw new Error('(@unitybase/ub).App accessible only inside server thread')
}
const argv = require('@unitybase/base').argv
const path = require('path')
const UBDomain = require('@unitybase/cs-shared').UBDomain
const EventEmitter = require('events').EventEmitter
const THTTPResponse = require('./HTTPResponse')
const THTTPRequest = require('./HTTPRequest')
const createDBConnectionPool = require('@unitybase/base').createDBConnectionPool
const blobStores = require('@unitybase/blob-stores')
const base = require('@unitybase/base')
/**
* @classdesc
* Singleton instance of UnityBase application. Allow direct access to the database connections, blob stores,
* HTTP endpoints (full control on HTTP request & response) registration, read domain and server config.
*
* Mixes EventEmitter, and emit:
*
* - `launchEndpoint:before` with parameters: (req, resp, endpointName)
* - `endpointName + ':before'` event before endpoint handler execution
* - `endpointName + ':after'` event in case neither exception is raised nor App.preventDefault() is called
* - `launchEndpoint:after` with parameters: (req, resp, endpointName, defaultPrevented)
*
* To prevent endpoint handler execution `App.preventDefault()` can be used inside `:before` handler.
* @example
const App = require('@unitybase/ub').App
// Register public (accessible without authentication) endpoint
App.registerEndpoint('echoToFile', echoToFile, false)
// write custom request body to file FIXTURES/req and echo file back to client
// @ param {THTTPRequest} req
// @ param {THTTPResponse} resp
function echoToFile (req, resp) {
var fs = require('fs')
fs.writeFileSync(path.join(FIXTURES, 'req'), req.read('bin'))
resp.statusCode = 200
resp.writeEnd(fs.readFileSync(path.join(FIXTURES, 'req'), {encoding: 'bin'}))
}
//Before getDocument requests
//@ param {THTTPRequest} req
//@ param {THTTPResponse} resp
function doSomethingBeforeGetDocumentCall(req, resp){
console.log('User with ID', Session.userID, 'try to get document')
}
// Adds hook called before each call to getDocument endpoint
App.on('getDocument:before', doSomethingBeforeGetDocumentCall)
//
//After getDocument requests
//@ param {THTTPRequest} req
//@ param {THTTPResponse} resp
function doSomethingAfterGetDocumentCall(req, resp){
params = req.parsedParameters
console.log('User with ID', Session.userID, 'obtain document using params', params)
}
App.on('getDocument:after', doSomethingAfterGetDocumentCall)
* @class ServerApp
* @mixes EventEmitter
*/
const ServerApp = {}
/**
* Fires inside each working thread for an {@link module:@unitybase/ub#App UB.App} just after all domain entities (all *.meta) are loaded into server memory
* and all server-side js are evaluated (for each working thread) but before BLOB stores initialization and endpoints registration.
*
* On this stage you can subscribe on a cross-model handles.
*
* @example
const UB = require('@unitybase/ub')
const App = UB.App
App.once('domainIsLoaded', function () {
const entities = App.domainInfo.entities
for (const eName in entities) {
// if entity have attribute mi_fedUnit
if (entities[eName].attributes.mi_fedUnit) {
let entityObj = global[eName]
entityObj.on('insert:before', fedBeforeInsert) // add before insert handler
}
}
})
* @event domainIsLoaded
* @memberOf ServerApp
*/
/**
* Fires inside each working thread for an {@link module:@unitybase/ub#App UB.App} after all initialization step is done (domain, endpoints and BLOB stores are initialized).
*
* On this stage application can use both domain and blobStores.
*
* @event applicationReady
* @memberOf ServerApp
*/
/**
* Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just after HTTP request context tries to get
* a DB connection for the first time
*
* On this stage DB session properties, specific for a current Session can be sets.
* For example multi-tenancy mixin subscribes for this event and sets a `ub_tenantID` DB session variable value to `Session.tenantID`
*
* @event enterConnectionContext
* @memberOf ServerApp
*/
/**
* Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just before RDBMS "commit" is called
*
* @example
App.on('commit:before', (connectionName) => {
console.debug('Event commit:before fired for connection', connectionName)
})
* @event 'commit:before'
* @param {string} connectionName Name of the connection being committed
* @memberOf ServerApp
*/
/**
* Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just after RDBMS "commit" is successfully called
*
* @event 'commit:after'
* @param {string} connectionName Name of the committed connection
* @memberOf ServerApp
*/
/**
* Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just before RDBMS "rollback" is called
*
* @example
App.on('rollback:before', (connectionName) => {
console.debug('Event rollback:before fired for connection', connectionName)
})
* @event 'rollback:before'
* @param {string} connectionName Name of the connection being rollback
* @memberOf ServerApp
*/
/**
* Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just after RDBMS "rollback" is called
*
* @event 'rollback:after'
* @param {string} connectionName Name of the rollback connection
* @memberOf ServerApp
*/
// add eventEmitter to application object
EventEmitter.call(ServerApp, 'App')
Object.assign(ServerApp, EventEmitter.prototype)
let __preventDefault = false
// TODO - remove `ServerApp.emitWithPrevent` when all App level method will be implemented in JS
/**
* Called by native
*
* @param {string} eventName
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
* @private
* @returns {boolean}
*/
ServerApp.emitWithPrevent = function (eventName, req, resp) {
__preventDefault = false
this.emit(eventName, req, resp)
return __preventDefault
}
/**
* Accessible inside app-level `:before` event handler. Call to prevent default method handler.
* In this case developer are responsible to fill response object, otherwise HTTP 400 is returned
*
* @memberOf ServerApp
*/
ServerApp.preventDefault = function () {
__preventDefault = true
}
/**
* Called by native HTTP server worker.
* Before UB@5.22.23 `App.endpointContext` variable is cleared inside JS launchEndpoint and therefore
* context is not available in 'commit|rollback:before|after' events
*
* In UB@5.22.23 context is cleared by native code after connections committed/rollback
*
* @param {string} endpointName
* @fires 'launchEndpoint:before'
* @fires 'launchEndpoint:after'
* @private
*/
ServerApp.launchEndpoint = launchEndpointWOClearContext
/**
* @param {string} endpointName
*/
function launchEndpointWOClearContext (endpointName) {
__preventDefault = false
const req = new THTTPRequest()
const resp = new THTTPResponse()
/**
* Fires before any endpoint execution
*
* @event 'launchEndpoint:before'
* @memberOf ServerApp
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
* @param {string} endpointName
*/
this.emit('launchEndpoint:before', req, resp, endpointName)
/**
* Fires before endpoint execution. In example below handler is called before each `getDocument` execution
*
* @example
const UB = require('@unitybase/ub')
const App = UB.App
function doSomethingBeforeGetDocumentCall(req, resp){
console.log('User with ID', Session.userID, 'try to get document')
}
// Adds hook called before each call to getDocument endpoint
App.on('getDocument:before', doSomethingBeforeGetDocumentCall)
* @event 'endpointName:before'
* @memberOf ServerApp
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
* @param {string} endpointName
*/
this.emit(endpointName + ':before', req, resp)
if (!__preventDefault) {
const handler = appBinding.endpoints[endpointName]
if (handler) { // JS endpoint
handler(req, resp)
} else { // native endpoint
appBinding.launchNativeEndpoint()
}
/**
* Fires after endpoint execution. In example below handler is called before each `getDocument` execution
*
* @event 'endpointName:after'
* @memberOf ServerApp
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
* @param {string} endpointName
*/
this.emit(endpointName + ':after', req, resp)
}
/**
* Fires after any endpoint execution
*
* @event 'launchEndpoint:after'
* @memberOf ServerApp
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
* @param {string} endpointName
*/
this.emit('launchEndpoint:after', req, resp, endpointName, __preventDefault)
// ServerApp.endpointContext = {} is called by native in UB@5.22.23
}
const ERR_VALUE_UNIQ = '<<<VALUE_MUST_BE_UNIQUE>>>'
const dbErrorHandlers = [
// Reference Errors ==================================================================================================
/**
* Handle Postgres DB reference error.
*
* Examples of "errMsg" to handle (added line breaks - the strings are long):
*
* 1) when trying to delete a record being referenced by a foreign key constraint.
* Rarely, this is an update of the PK value referenced by another table:
*
* TSQLDBPostgresLib PGERRCODE: 23503, ERROR:
* update or delete on table "uba_role" violates foreign key constraint "fk_els_rulerole_ref_role" on table "uba_els"
* DETAIL: Key (id)=(1000000006207) is still referenced from table "uba_els".
*
* 2) when trying to insert or update a records using values that points to non-existing records in other tables,
* rarely, it is an update of PK value referenced by other table (almost never happens in UB):
*
* TSQLDBPostgresLib PGERRCODE: 23503, ERROR:
* insert or update on table "uba_userrole" violates foreign key constraint "fk_usrole_roleid_ref_role"
* DETAIL: Key (roleid)=(23434) is not present in table "uba_role".
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function postgresReferenceError (errMsg) {
if (!errMsg.includes('PGERRCODE: 23503')) {
// In either case, the error code is 23503
return
}
if (errMsg.includes('update or delete')) {
return '<<<ERR_REFERENCE_ERROR>>>'
}
if (errMsg.includes('insert or update')) {
return '<<<ERR_SET_REFERENCE_ERROR>>>'
}
console.warn('Unhandled Postgres reference error:', errMsg)
},
/**
* Handle SQL Server DB reference error.
*
* Examples of "errMsg" to handle (added line breaks - the strings are long):
*
* 1) when trying to delete a record being referenced by a foreign key constraint.
*
* TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The DELETE statement conflicted with the REFERENCE constraint "FK_ELS_RULEROLE_REF_ROLE".
* The conflict occurred in database "scriptum", table "dbo.uba_els", column 'ruleRole'. (547)
* [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The statement has been terminated. (3621)
*
* 2) when trying to insert a records using values that points to non-existing records in other tables,
* TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The INSERT statement conflicted with the FOREIGN KEY constraint "FK_USROLE_ROLEID_REF_ROLE".
* The conflict occurred in database "scriptum", table "dbo.uba_role", column 'ID'. (547)
* [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The statement has been terminated. (3621)
*
* 3) when trying to update a records using values that points to non-existing records in other tables,
*
* TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_USROLE_ROLEID_REF_ROLE".
* The conflict occurred in database "scriptum", table "dbo.uba_role", column 'ID'. (547)
* [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The statement has been terminated. (3621)
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function sqlServerReferenceError (errMsg) {
if (errMsg.includes('The DELETE statement conflicted with the REFERENCE constraint')) {
return '<<<ERR_REFERENCE_ERROR>>>'
}
if (errMsg.includes('The INSERT statement conflicted with the FOREIGN KEY constraint') ||
errMsg.includes('The UPDATE statement conflicted with the FOREIGN KEY constraint')) {
return '<<<ERR_SET_REFERENCE_ERROR>>>'
}
},
/**
* Handle Oracle DB reference error.
*
* Examples of "errMsg" to handle (added line breaks - the strings are long):
*
* 1) when trying to delete a record being referenced by a foreign key constraint.
*
* TSQLDBOracleStatement error: ORA-02292: integrity constraint (UB.FK_ELS_RULEROLE_REF_ROLE) violated - child record found
*
* 2) when trying to insert or update a records using values that points to non-existing records in other tables:
*
* ORA-02291: integrity constraint (UB.FK_USROLE_ROLEID_REF_ROLE) violated - parent key not found
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function oracleReferenceError (errMsg) {
if (errMsg.includes(' ORA-02292')) {
return '<<<ERR_REFERENCE_ERROR>>>'
}
if (errMsg.includes(' ORA-02291')) {
return '<<<ERR_SET_REFERENCE_ERROR>>>'
}
},
function sqliteReferenceError (errMsg) {
if (errMsg.includes('FOREIGN KEY')) {
return '<<<ERR_REFERENCE_ERROR>>>'
}
},
// Unique Errors =====================================================================================================
/**
* Handle Postgres unique constraint error.
*
* Example of "errMsg" to handle:
* TSQLDBPostgresLib PGERRCODE: 23505, ERROR: duplicate key value violates unique constraint "uidx_role_name"
* DETAIL: Key (name)=(SysOps) already exists.
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function postgresUniqueError (errMsg) {
if (errMsg.includes('unique constraint')) {
const r = /\)=(\(.*\))/.exec(errMsg)
if (!r || !r[1]) {
return JSON.stringify(`${ERR_VALUE_UNIQ}|[""]`)
}
// Remove `, 9999-12-31 00:00:00` - the mi_deleteDate value
const tuple = r[1].replace(/, 9999-12-31 00:00:00/g, '')
return JSON.stringify(`${ERR_VALUE_UNIQ}|["${tuple}"]`)
}
},
/**
* Handle SQL Server unique constraint error.
*
* Example of "errMsg" to handle:
* TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* Cannot insert duplicate key row in object 'dbo.uba_role' with unique index 'UIDX_ROLE_NAME'.
* The duplicate key value is (SysOps). (2601)
* [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
* The statement has been terminated. (3621)
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function sqlServerUniqueError (errMsg) {
if (errMsg.includes(' duplicate key row')) {
const r = /duplicate key value is (\(.*\))\./.exec(errMsg)
if (!r || !r[1]) {
return JSON.stringify(`${ERR_VALUE_UNIQ}|[""]`)
}
// Remove `, Dec 31 9999 12:00AM` - the mi_deleteDate value
const tuple = r[1].replace(/, Dec 31 9999 12:00AM/g, '')
return JSON.stringify(`${ERR_VALUE_UNIQ}|["${tuple}"]`)
}
},
/**
* Handle Oracle unique constraint error.
*
* Example of "errMsg" to handle:
* ORA-00001: unique constraint (UB.UIDX_ROLE_NAME) violated
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function oracleUniqueError (errMsg) {
if (errMsg.includes(' ORA-00001')) {
return ERR_VALUE_UNIQ
}
},
function sqliteUniqueError (errMsg) {
// UNIQUE constraint failed: org_employee.code, org_employee.mi_dateTo, org_employee.mi_deleteDate,
// extended_errcode=2067
if (errMsg.includes('UNIQUE constraint')) {
return ERR_VALUE_UNIQ
}
},
// Resource Limit Errors =============================================================================================
/**
* Handle Postgres resource limit error.
*
* Example of "errMsg" to handle:
* TSQLDBPostgresLib PGERRCODE: 57014, ERROR: canceling statement due to statement timeout
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function postgresResourceLimitError (errMsg) {
if (errMsg.includes('statement timeout')) {
return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
}
},
/**
* Handle SQL Server resource limit error.
*
* Example of "errMsg" to handle:
* Query timeout expired
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function sqlServerResourceLimitError (errMsg) {
if (errMsg.includes('Query timeout expired')) {
return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
}
},
/**
* Handle Oracle resource limit error.
*
* Example of "errMsg" to handle:
* ORA-00040: active time limit exceeded - call aborted
* ORA-56735: elapsed time limit exceeded - call aborted
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function oracleResourceLimitError (errMsg) {
if (
errMsg.includes('ORA-00040') ||
errMsg.includes('ORA-56735')
) {
return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
}
},
/**
* Handle UB error, when statement memory limit exceeded
*
* Example of "errMsg" to handle:
* `FetchAllTo[Binary|JSON|CSVValues]: overflow` max statement memory
*
* @param {string} errMsg
* @returns {string|undefined}
*/
function maxStatementMemoryError (errMsg) {
if (errMsg.includes('FetchAllTo') && errMsg.includes(': overflow')) {
return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
}
}
]
/**
* Called by native HTTP server worker in case of unhandled error in `launchEndpoint` (after transaction is rolled back).
* Should not use a DB, because on this stage DB connection may be in inconsistent stage.
*
* @param {string} errMsg
* @returns {string|null} Either null for default behavior, or <<< >>> wrapped error text what returned to caller (client)
* @private
*/
ServerApp.transformUnhandledError = function (errMsg) {
for (const handler of dbErrorHandlers) {
const result = handler(errMsg)
if (result) {
return result
}
}
return null
}
/**
* Called by native RLS mixin. Task of method is to either run a `rls.func` or eval a `rls.expression` for ctx.dataStore.Entity
*
* @param {ubMethodParams} ctx
* @private
*/
ServerApp.launchRLS = function (ctx) {
const rlsMixin = ctx.dataStore.entity.mixins.rls
if (rlsMixin.func) { // functional RLS
if (!rlsMixin.__funcVar) { // parse func namespace 'uba_user.somefunc' (string) -> uba_user.somefunc (function)
const fPath = rlsMixin.func.split('.')
let f = global[fPath[0]]
for (let i = 1, L = fPath.length; i < L; i++) {
f = f[fPath[i]]
}
if (typeof f !== 'function') throw new Error(`${ctx.dataStore.entity.name} rls func "${rlsMixin.func}" is not a function`)
rlsMixin.__funcVar = f
}
console.debug('Call func', rlsMixin.func)
rlsMixin.__funcVar.call(global[ctx.dataStore.entity.name], ctx) // call RLS function using entity namespace as this
} else { // expression
const mParams = ctx.mParams
// eslint-disable-next-line no-eval
const expr = eval(rlsMixin.expression)
console.debug('Eval rls expression to', expr)
if (!mParams.whereList) {
mParams.whereList = {}
}
mParams.whereList[`rls${Date.now()}`] = {
expression: expr,
condition: 'custom'
}
}
}
// eslint-disable-next-line n/no-deprecated-api
const appBinding = process.binding('ub_app')
/**
* Register a server endpoint.
* One of the endpoints can be default endpoint - it will be used as a fallback
* in case URL do not start with any of known endpoints name.
*
* Exceptions inside endpoint handler are intercepted by UB server. In case exception is occurred
* server will roll back any active DB transactions and serialize an exception message
* to response depending on server execution mode:
* - for `dev` mode - original exception text will be serialized (for debugging purpose)
* - for production mode - in case exception message is wrapped into `<<<..>>>` then this message will be serialized,
* if not - text will be always `Internal server error` (for security reason)
*
* Recommended way to throw a handled error inside endpoint handler is `throw new UB.UBAbort('.....')`
*
* @example
// Write a custom request body to file FIXTURES/req and echo file back to client
// @param {THTTPRequest} req
// @param {THTTPResponse} resp
//
function echoToFile(req, resp) {
var fs = require('fs');
fs.writeFileSync(FIXTURES + 'req', req.read('bin'));
resp.statusCode = 200;
resp.writeEnd(fs.readFileSync(FIXTURES + 'req', {encoding: 'bin'}));
}
App.registerEndpoint('echoToFile', echoToFile);
* @param {string} endpointName
* @param {Function} handler
* @param {boolean} [authorizationRequired=true] If `true` UB will check for valid Authorization header before
* execute endpoint handler
* @param {boolean} [isDefault=false]
* @param {boolean} [bypassHTTPLogging=false] Do not put HTTP body into log (for example if body contains sensitive information, like password)
* @memberOf ServerApp
*/
ServerApp.registerEndpoint = function (endpointName, handler, authorizationRequired, isDefault, bypassHTTPLogging) {
if (!appBinding.endpoints[endpointName]) {
appBinding.endpoints[endpointName] = handler
return appBinding.registerEndpoint(
endpointName,
authorizationRequired === undefined ? true : authorizationRequired,
isDefault === true,
bypassHTTPLogging === true
)
}
}
/**
* Grant endpoint to role
*
* @param {string} endpointName
* @param {string} roleCode
* @returns {boolean} true if endpoint exists and role not already granted, false otherwise
* @memberOf ServerApp
*/
ServerApp.grantEndpointToRole = function (endpointName, roleCode) {
return appBinding.grantEndpointToRole(endpointName, roleCode)
}
/**
* Server configuration - result of {@link module:argv~getServerConfiguration argv.getServerConfiguration}
*
* @readonly
* @type {object}
* @property {object} httpServer HTTP server config
* @property {object} application
* @property {string} application.name
* @property {string} application.defaultLang
* @property {object} application.domain
* @property {Array} application.domain.models
* @property {Array<string>} application.domain.supportedLanguages
* @property {object} application.customSettings
* @property {object} uiSettings Section `uiSettings` of ubConfig
* @property {object} security
*/
ServerApp.serverConfig = undefined
const SERVER_CONFIG_CS = appBinding.registerCriticalSection('SERVER_CONFIG_CS')
appBinding.enterCriticalSection(SERVER_CONFIG_CS)
try {
try {
ServerApp.serverConfig = argv.getServerConfiguration()
} catch (e) {
console.error(e.message, e)
}
} finally {
appBinding.leaveCriticalSection(SERVER_CONFIG_CS)
}
/**
* Application `package.json` content (parsed)
*
* @type {object}
*/
ServerApp.package = require(path.join(process.configPath, 'package.json'))
/**
* Full path to application static folder if any, '' if static folder not set
*
* @type {string}
* @readonly
*/
ServerApp.staticPath = ''
if (ServerApp.serverConfig.httpServer && ServerApp.serverConfig.httpServer.inetPub &&
ServerApp.serverConfig.httpServer.inetPub.trim()) {
const sp = ServerApp.serverConfig.httpServer.inetPub
ServerApp.staticPath = path.isAbsolute(sp)
? sp
: path.join(process.configPath, sp)
}
/**
* Application default language
*
* @type {string}
* @readonly
*/
ServerApp.defaultLang = ServerApp.serverConfig.application.defaultLang
/**
* Custom settings for application from ubConfig.app.customSettings
*
* @deprecated Use App.serverConfig.application.customSettings: Object instead
* @type {string}
*/
Object.defineProperty(ServerApp, 'customSettings', {
enumerable: true,
get: function () {
console.warn('App.customSettings is deprecated. Use App.serverConfig.application.customSettings instead')
return JSON.stringify(ServerApp.serverConfig.application.customSettings)
}
})
/**
* Return stringify JSON specified in serverConfig.uiSettings
*
* @deprecated Use App.serverConfig.uiSettings: Object instead
* @returns {string}
*/
ServerApp.getUISettings = function () {
console.warn('App.getUISettings is deprecated. Use App.serverConfig.uiSettings: Object instead')
return JSON.stringify(ServerApp.serverConfig.uiSettings)
}
/**
* Full URl HTTP server is listen on (if HTTP server enabled, else - empty string)
*
* @type {string}
* @readonly
*/
ServerApp.serverURL = argv.serverURLFromConfig(ServerApp.serverConfig)
/**
* URL that the User from the internet will use to access your server. To be used in case server is behind a reverse proxy
*
* @type {string}
* @readonly
*/
ServerApp.externalURL = ServerApp.serverConfig.httpServer.externalURL || ServerApp.serverURL
/**
* List of a local server IP addresses CRLF (or CR for non-windows) separated
*/
ServerApp.localIPs = _App.localIPs
/**
* Current application Domain
*
* @deprecated UB >=4 use App.domainInfo - a pure JS domain representation
* @readonly
*/
Object.defineProperty(ServerApp, 'domain', {
enumerable: true,
get: function () {
throw new Error('App.domain is obsolete. Use App.domainInfo')
}
})
/**
* For UB EE return true in case product license is exceed. For UB Se always `false`
*
* @type {string}
*/
Object.defineProperty(ServerApp, 'isLicenseExceed', {
enumerable: true,
get: function () {
return typeof appBinding.isLicenseExceed === 'function'
? appBinding.isLicenseExceed()
: false
}
})
const getDomainInfo = appBinding.getDomainInfo
let _domainCache
/**
* Extended information about application domain (metadata)
*
* @memberOf ServerApp
* @member {UBDomain} domainInfo
*/
Object.defineProperty(ServerApp, 'domainInfo', {
enumerable: true,
get: function () {
if (!_domainCache) {
_domainCache = (new UBDomain(getDomainInfo(true))) // get extended domain information
}
return _domainCache
}
})
/**
* Called by @unitybase/ub initialization process to reset domain after all models evaluated, because entity methods
* are added by models using entity.addMethod
*
* @private
*/
ServerApp._resetDomainCache = function () {
_domainCache = undefined
}
/**
* Get value from global cache. Global cache shared between all threads and,
* if redis.useForGlobalCache=true in config - between all servers in server group
*
* Return '' (empty string) in case key not present in cache.
*
* For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
*
* @param {string} key Key to retrieve
* @param {object} [options]
* @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
* @returns {string}
*/
ServerApp.globalCacheGet = function (key, options) {
const opt = Object.assign({ ignoreTenants: false }, options)
return _App.globalCacheGet(key, opt.ignoreTenants)
}
/**
* Put value to global cache. Global cache shared between all threads and,
* if redis.useForGlobalCache=true in config - between all servers in server group
*
* For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
*
* @param {string} key key to hold the string value
* @param {string|null} value Value to put into this key. If === null then key will be remover from cache
* @param {object} [options]
* @param {boolean} [options.nx=false] only set the key if it does not already exist
* @param {number} [options.px=0] *REDIS* backend only. Set the specified expire time, in milliseconds (a positive integer); 0 - not expired
* @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
* @returns {boolean} return true if key is set, otherwise - false (for example when nx=true and key exists)
*/
ServerApp.globalCachePut = function (key, value, options) {
const opt = Object.assign({ ignoreTenants: false, nx: false, px: 0 }, options)
return _App.globalCachePut(key, value, opt.ignoreTenants, opt.nx, opt.px)
}
/**
* Get value from memory cache. Memory cache shared between all threads of current instance.
* If you are *completely sure* that the value should be used by _only_ one instance, use this function, otherwise use `globalCacheGet`.
*
* Return '' (empty string) in case key not present in cache.
*
* For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
*
* @param {string} key Key to retrieve
* @param {object} [options]
* @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
* @returns {string}
*/
ServerApp.memCacheGet = function (key, options) {
const opt = Object.assign({ ignoreTenants: false }, options)
return _App.memCacheGet(key, opt.ignoreTenants)
}
/**
* Put value to memory cache. Memory cache shared between all threads of current instance.
* If you are *completely sure* that the value should be used by _only_ one instance, use this function, otherwise use `globalCachePut`.
*
* For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
*
* @param {string} key Key to put into
* @param {string|null} value Value to put into this key. If === null then key will be remover from cache
* @param {object} [options]
* @param {boolean} [options.nx=false] only set the key if it does not already exist
* @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
* @returns {boolean} return true if key is set, otherwise - false (for example when nx=true and key exists)
*/
ServerApp.memCachePut = function (key, value, options) {
const opt = Object.assign({ ignoreTenants: false, nx: false, px: 0 }, options)
return _App.memCachePut(key, value, opt.ignoreTenants, opt.nx, opt.px)
}
/**
* Delete row from FTS index for exemplar with `instanceID` of entity `entityName` (mixin `fts` must be enabled for entity)
*
* @param {string} entityName
* @param {number} instanceID
*/
ServerApp.deleteFromFTSIndex = function (entityName, instanceID) {
_App.deleteFromFTSIndex(entityName, instanceID)
}
/**
* Update FTS index for exemplar with `instanceID` of entity `entityName` (mixin `fts` must be enabled for entity).
* In case row does not exist in FTS perform insert action automatically.
*
* @param {string} entityName
* @param {number} instanceID
*/
ServerApp.updateFTSIndex = function (entityName, instanceID) {
_App.updateFTSIndex(entityName, instanceID)
}
/**
* Databases connections pool
*
* @type {Object<string, DBConnection>}
*/
ServerApp.dbConnections = createDBConnectionPool(ServerApp.domainInfo.connections)
/**
* Check database are used in current endpoint context and DB transaction is already active
*
* @param {string} connectionName
* @returns {boolean}
*/
ServerApp.dbInTransaction = function (connectionName) {
return _App.dbInTransaction(connectionName)
}
/**
* Commit active database transaction if any.
* In case `connectionName` is not passed will commit all active transactions for all connections.
* Return `true` if transaction is committed, or `false` if database not in use or no active transaction
*
* @param {string} [connectionName]
* @returns {boolean}
*/
ServerApp.dbCommit = function (connectionName) {
return connectionName ? _App.dbCommit(connectionName) : _App.dbCommit()
}
/**
* Rollback active database transaction if any.
* In case `connectionName` is not passed will rollback all active transactions for all connections.
* Return `true` if transaction is rollback'ed, or `false` if database not in use or no active transaction
*
* @param {string} [connectionName]
* @returns {boolean}
*/
ServerApp.dbRollback = function (connectionName) {
return connectionName ? _App.dbRollback(connectionName) : _App.dbRollback()
}
/**
* Start a transaction for a specified database. If database is not used in this context will
* create a connection to the database and start transaction.
*
* For Oracle with DBLink first statement to DBLink'ed table must be
* either update/insert/delete or you MUST manually start transaction
* to prevent "ORA-01453: SET TRANSACTION be first statement"
*
* @param {string} connectionName
* @returns {boolean}
*/
ServerApp.dbStartTransaction = function (connectionName) {
return _App.dbStartTransaction(connectionName)
}
/**
* Run `func` in autonomous transaction
*
* All database statements, or direct SQL execution from inside `func` will be executed in a separate connection
* intended for executing autonomous transactions and automatically committed (or rolled back in case of an exception).
*
* Can't be nested.
*
* @param {function} func
* @returns {*} a `func` result
*/
ServerApp.runInAutonomousTransaction = function (func) {
let res
appBinding.logEnter('Autonomous transaction')
try {
_App.enterAutonomousTransaction()
try {
res = func()
_App.commitAutonomousTransaction()
} catch (e) {
_App.rollbackAutonomousTransaction()
throw e
}
} finally {
appBinding.logLeave()
}
return res
}
/**
* Try retrieve or create new session from request headers.
* Return `true` if success, `false` if more auth handshakes is required.
* In case of invalid credential throw security exception
*
* @param {boolean} noHTTPBodyInResp If true do not write a uData to the HTTP response
* @param {boolean} doSetOutCookie If true set an out authorization cookie on success response (Negotiate only)
* @param {string} [fallbackAuthToken] Optional fallback auth token. If passed - will be used by UB schema authorization
* if no other token (in headers\cookies\sessin_signature URI parameter) found
* @returns {boolean}
*/
ServerApp.authFromRequest = function (noHTTPBodyInResp = false, doSetOutCookie = false, fallbackAuthToken) {
return _App.authFromRequest(noHTTPBodyInResp, doSetOutCookie, fallbackAuthToken)
}
/**
* Logout a current user (kill current session)
* @returns {boolean}
*/
ServerApp.logout = function () {
return _App.logout()
}
/**
* Check Entity-Level-Security for specified entity/method
*
* @example
if App.els('uba_user', 'insert'){
// do something
}
* @param {string} entityCode
* @param {string} methodCode
* @param {Array<number>} [rolesIDs] If not passed - current user session is used for roles (faster)
* @returns {boolean}
*/
ServerApp.els = function (entityCode, methodCode, rolesIDs) {
return rolesIDs ? _App.els(entityCode, methodCode, rolesIDs) : _App.els(entityCode, methodCode)
}
/**
* Check IP match any of line from file specified in `ubConfig.security.blackListFileName`.
* Returns false if not match or string with reason (comment from file line) if so
*
* @param {string} IP
* @returns {boolean|string}
*/
ServerApp.isIPInBlackList = function (IP) {
return _App.isIPInBlackList(IP)
}
/**
* Register a named critical section. Can be done only in initialization mode.
* In case section with the same name already registered in another thread - returns existed CS index
*
* All threads MUST register section in the same way, do not put call into condition what may evaluate
* to the different values in the different threads.
*
* @example
const App = require('@unitybase/ub').App
// critical section must be registered once, at the moment modules are evaluated without any conditions
const MY_CS = App.registerCriticalSection('SHARED_FILE_ACCESS')
function concurrentFileAccess() {
// prevents mutual access to the same file from the different threads
App.enterCriticalSection(FSSTORAGE_CS)
try {
const data = fs.readfileSync('/tmp/concurrent.txt', 'utf8')
// do some operation what modify data
fs.writefileSync('/tmp/concurrent.txt', data)
} finally {
// important to leave critical section in finally block to prevent forever lock
App.leaveCriticalSection(FSSTORAGE_CS)
}
}
* @function
* @param {string} csName Critical section name
* @returns {number}
*/
ServerApp.registerCriticalSection = appBinding.registerCriticalSection
/**
* Waits for ownership of the specified critical section object. The function returns when the calling thread is granted ownership.
*
* ** IMPORTANT** A thread must call `App.leaveCriticalSection` once for each time that it entered the critical section.
*
* @function
* @param {number} csIndex A critical section index returned by `App.registerCriticalSection`
*/
ServerApp.enterCriticalSection = appBinding.enterCriticalSection
/**
* Releases ownership of the specified critical section
*
* @function
* @param {number} csIndex
*/
ServerApp.leaveCriticalSection = appBinding.leaveCriticalSection
/**
* Enter a log recursion call.
* ** IMPORTANT** A thread must call `App.logLeave` once for each time that it entered the log recursion.
* For method with `ctx: ubMethodParam` parameter `App.wrapEnterLeaveForUbMethod` can be used
* to create an enter/leave log wrapper
*
* @example
function wrapEnterLeave(enterText, originalMethod) {
return function(ctx) {
App.logEnter(enterText)
try {
originalMethod(ctx)
} finally {
App.logLeave()
}
}
}
* @function
* @param {string} methodName
*/
ServerApp.logEnter = appBinding.logEnter
/**
* Exit a log recursion call
*
* @function
*/
ServerApp.logLeave = appBinding.logLeave
/**
* Enter a log recursion call with `enterText`, call `methodImpl` and exit from log recursion call.
*
* In case `enterText` is `method(myMixin) my_entity.select`, logging will be:
*
* 20210314 09224807 " + method(myMixin) my_entity.select
* 20210314 09224807 " debug some debug (shifted by recursion level automatically)
* 20210314 09224807 " - 00.005.124
*
* @function
* @param {string} enterText Text what will be logged in the beginning of function call
* @param {Function} originalMethod function what accept one parameter - ctx: ubMethodParam
* @returns {Function}
*/
ServerApp.wrapEnterLeaveForUbMethod = function wrapEnterLeaveForUbMethod (enterText, originalMethod) {
const f = function (ctx) {
appBinding.logEnter(enterText)
try {
originalMethod(ctx)
} finally {
appBinding.logLeave()
}
}
f._skipEmitterTrace = true
return f
}
/**
* Partially reload server config - the same as -HUP signal for process
* + set JS variable `App.serverConfig` to new config
* + reset generated index page (uiSettings block of config)
*
* @function
* @returns {boolean} true is config s valid and successfully reloaded
*/
ServerApp.reloadConfig = function () {
appBinding.reloadConfig()
const arrData = JSON.parse(_App.globalCacheList(base.GC_KEYS.COMPILED_INDEX_)) // [['ID', 'key', 'keyValue'],...]
// for UB < 5.23.7 _App.globalCacheList returns all keys
const compiledIndexes = arrData.filter(r => r[1].startsWith(base.GC_KEYS.COMPILED_INDEX_)).map(r => r[1])
compiledIndexes.forEach(cIdxKey => {
this.globalCachePut(cIdxKey, null) // reset compiled index page
})
console.debug('Compiled index page resets for', compiledIndexes)
let result = true
try {
const newServerCfg = argv.getServerConfiguration()
ServerApp.serverConfig = newServerCfg
console.debug('App.serverConfig reloaded')
} catch (e) {
result = false
console.error(e.message, e)
}
return result
}
/**
* Observe a file system operation time (exposed as prometheus `unitybase_fs_operation_duration_seconds` histogram).
*
* **WARNING** do not use a full file path - use a folder name or better a mnemonic name (BLOB store name for example).
* Amount of metric labels SHOULD be as small as possible. The same is true for operation`s names.
*
* See fileSystemBlobStore for real life usage example.
*
* @param {number} durationSec fs operation duration in **seconds**
* @param {string} path
* @param {string} operation
*/
ServerApp.fsObserve = appBinding.fsObserve
/**
* Observe an HTTP client operation time (exposed as prometheus `unitybase_httpext_duration_seconds` histogram).
*
* `http` module automatically observe each request, passing `host` as `uri` parameter.
* Method can be called manually in case some part of path should be observed also.
*
* **WARNING** do not use a full path - use a part what identify an endpoint without parameters.
* Amount of metric labels SHOULD be as small as possible. The same is true for operation`s names.
*
* @param {number} durationSec request duration in **seconds**
* @param {string} uri request URI
* @param {number} respStatus HTTP response status code
*/
ServerApp.httpCallObserve = appBinding.httpCallObserve
/**
* Remove all user sessions (logout user).
*
* If `exceptCurrent` is `true` - do not remove current session (logout all other sessions except my).
*
* @example
const UB = require('@unitybase/ub')
const Session = UB.Session
const App = UB.App
Session.on('login', logoutAllMyOldSessions)
// One user - one session mode
function logoutAllMyOldSessions (req) {
if (App.removeUserSessions(Session.userID, true)) {
console.log(`All other sessions for user ${Session.userID} are removed`)
}
}
* @function
* @param {number} userID
* @param {boolean} [exceptCurrent=false] If `true` - do not remove current session
* @returns {boolean} true if user had had any session
*/
ServerApp.removeUserSessions = function (userID, exceptCurrent = false) {
return appBinding.removeUserSessions(userID, exceptCurrent)
}
/**
* Return session count for specified user, including current session
*
* @function
* @param {number} userID
* @returns {number} session count for specified user, including current session
* @since UB@5.22
*/
ServerApp.getUserSessionsCount = function (userID) {
return appBinding.getUserSessionsCount(userID)
}
/**
* Confirm 2FA secret previously sets by `Session.setExpected2faSecret`.
* If secrets match - reset session expected secret and returns `true`. After this endpoints execution became allowed;
* If secret does not match - return string with one of the reason (do not expose this reason to caller!):
* - 'sessionNotFound': provided sessionID not exists
* - 'sessionIsSystem': provided sessionID is for system session, such sessions can't be a subject of 2FA
* - 'wrongSecret': provided secret do not match session 2FA secret
* - 'wrongSecretUserLocked': provided secret do not match session 2FA secret many times, so session is closed and user is locked
* If secret is empty - return confirmation status `true` if 2fa is confirmed, `false` if not, string with reason in case of error
*
* @function
* @param {string} sessionID
* @param {string} [secret=''] if secret is empty - status request
* @returns {boolean|string}
* @since UB@5.22.18
*/
ServerApp.confirmSession2faSecret = function (sessionID, secret = '') {
return appBinding.confirmSession2faSecret(sessionID, secret)
}
/**
* Is event emitter enabled for App singleton. Default is `false`
*
* @deprecated Starting from 1.11 this property ignored (always TRUE)
* @type {boolean}
*/
ServerApp.emitterEnabled = true
/**
* Defense edition only,
* Base64 encoded public server certificate
*
* Contains non-empty value in case `security.dstu.trafficEncryption` === `true` and
* key name defined in `security.dstu.novaLib.keyName`
*
* @type {string}
*/
ServerApp.serverPublicCert = _App.serverPublicCert
/**
* BLOB stores methods. For usage examples see:
* - {@link module:@unitybase/blob-stores~getContent App.blobStores.getContent} - load content of BLOB into memory
* - {@link module:@unitybase/blob-stores~getContentPath App.blobStores.getContentPath} - get a path to the file based store BLOB content
* - {@link module:@unitybase/blob-stores~putContent App.blobStores.putContent} - put a BLOB content to the temporary storage
* - {@link module:@unitybase/blob-stores~markRevisionAsPermanent App.blobStores.markRevisionAsPermanent} - mark specified revision of a historical store as permanent
* - {@link module:@unitybase/blob-stores~internalWriteDocumentToResp App.blobStores.internalWriteDocumentToResp} - mark specified revision of a historical store as permanent
* - {@link module:@unitybase/blob-stores~shred App.blobStores.shred} - *completely remove*, including all historical revisions all BLOBs content for
* specified entity, attribute and ID(s)
* - {@link module:@unitybase/blob-stores~shred App.blobStores.shredAll} - like shred, but for all entity BLOB attributes
*/
ServerApp.blobStores = {
getContent: blobStores.getContent,
getContentPath: blobStores.getContentPath,
getContentPathAndBlobInfo: blobStores.getContentPathAndBlobInfo,
putContent: blobStores.putContent,
markRevisionAsPermanent: blobStores.markRevisionAsPermanent,
internalWriteDocumentToResp: blobStores.internalWriteDocumentToResp,
shred: blobStores.shred,
shredAll: blobStores.shredAll
}
/**
* Endpoint context. Application logic can store here some data what required during single HTTP method call;
* Starting from UB@5.17.9 server reset `App.endpointContext` to {} after endpoint implementation execution,
* so in the beginning of execution it's always empty
*
* App.endpointContext.MYMODEL_mykey = 'some value we need to share between different methods during a single user request handling'
*
* @type {object}
*/
ServerApp.endpointContext = {}
module.exports = ServerApp