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 executionendpointName + ':after'
event in case neither exception is raised nor App.preventDefault() is calledlaunchEndpoint:after
with parameters: (req, resp, endpointName, defaultPrevented)
To prevent endpoint handler execution App.preventDefault()
can be used inside :before
handler.
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)
# new ServerApp ()
Members
# blobStores static
BLOB stores methods. For usage examples see:
- App.blobStores.getContent - load content of BLOB into memory
- App.blobStores.getContentPath - get a path to the file based store BLOB content
- App.blobStores.putContent - put a BLOB content to the temporary storage
- App.blobStores.markRevisionAsPermanent - mark specified revision of a historical store as permanent
- App.blobStores.internalWriteDocumentToResp - mark specified revision of a historical store as permanent
- App.blobStores.shred - completely remove, including all historical revisions all BLOBs content for specified entity, attribute and ID(s)
- App.blobStores.shredAll - like shred, but for all entity BLOB attributes
# dbConnections : Object.<string, DBConnection> static
Databases connections pool
# defaultLang : string static
Application default language
# domainInfo : UBDomain static
Extended information about application domain (metadata)
# emitterEnabled : boolean deprecated static
Is event emitter enabled for App singleton. Default is false
Starting from 1.11 this property ignored (always TRUE)
# endpointContext : object static
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'
# externalURL : string static
URL that the User from the internet will use to access your server. To be used in case server is behind a reverse proxy
# fsObserve static
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.
# httpCallObserve static
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.
# localIPs static
List of a local server IP addresses CRLF (or CR for non-windows) separated
# package : object static
Application package.json
content (parsed)
# serverConfig : object static
Server configuration - result of argv.getServerConfiguration
Name | Type | Description |
---|---|---|
httpServer | object | HTTP server config |
application | object | |
application.name | string | |
application.defaultLang | string | |
application.domain | object | |
application.domain.models | Array | |
application.domain.supportedLanguages | Array.<string> | |
application.customSettings | object | |
uiSettings | object | Section |
security | object |
# serverPublicCert : string static
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
# serverURL : string static
Full URl HTTP server is listen on (if HTTP server enabled, else - empty string)
# staticPath : string static
Full path to application static folder if any, '' if static folder not set
Methods
# authFromRequest (noHTTPBodyInResp: boolean, doSetOutCookie: boolean, fallbackAuthTokenopt: string) → boolean static
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
Arguments:
noHTTPBodyInResp
: booleanIf true do not write a uData to the HTTP response
doSetOutCookie
: booleanIf true set an out authorization cookie on success response (Negotiate only)
fallbackAuthToken
: stringOptional fallback auth token. If passed - will be used by UB schema authorization if no other token (in headers\cookies\sessin_signature URI parameter) found
# confirmSession2faSecret (sessionID: string, secretopt: string) → boolean | string static
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
# dbCommit (connectionNameopt: string) → boolean static
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
Arguments:
connectionName
: string
# dbInTransaction (connectionName: string) → boolean static
Check database are used in current endpoint context and DB transaction is already active
Arguments:
connectionName
: string
# dbRollback (connectionNameopt: string) → boolean static
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
Arguments:
connectionName
: string
# dbStartTransaction (connectionName: string) → boolean static
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"
Arguments:
connectionName
: string
# deleteFromFTSIndex (entityName: string, instanceID: number) static
Delete row from FTS index for exemplar with instanceID
of entity entityName
(mixin fts
must be enabled for entity)
# els (entityCode: string, methodCode: string, rolesIDsopt: Array.<number>) → boolean static
Check Entity-Level-Security for specified entity/method
Arguments:
if App.els('uba_user', 'insert'){
// do something
}
# enterCriticalSection (csIndex: number) static
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.
Arguments:
csIndex
: numberA critical section index returned by
App.registerCriticalSection
# getUISettings () → string static deprecated
Use App.serverConfig.uiSettings: Object instead
Return stringify JSON specified in serverConfig.uiSettings
# getUserSessionsCount (userID: number) → number static
Return session count for specified user, including current session
Return:
session count for specified user, including current session
Arguments:
userID
: number
# globalCacheGet (key: string, optionsopt: object) → string static
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)
# globalCachePut (key: string, value: string | null, optionsopt: object) → boolean static
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)
Return:
return true if key is set, otherwise - false (for example when nx=true and key exists)
Arguments:
# grantEndpointToRole (endpointName: string, roleCode: string) → boolean static
Grant endpoint to role
Return:
true if endpoint exists and role not already granted, false otherwise
# isIPInBlackList (IP: string) → boolean | string static
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
Arguments:
IP
: string
# leaveCriticalSection (csIndex: number) static
Releases ownership of the specified critical section
Arguments:
csIndex
: number
# logEnter (methodName: string) static
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
Arguments:
methodName
: string
function wrapEnterLeave(enterText, originalMethod) {
return function(ctx) {
App.logEnter(enterText)
try {
originalMethod(ctx)
} finally {
App.logLeave()
}
}
}
# logLeave () static
Exit a log recursion call
# logout () → boolean static
Logout a current user (kill current session)
# memCacheGet (key: string, optionsopt: object) → string static
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)
# memCachePut (key: string, value: string | null, optionsopt: object) → boolean static
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)
Return:
return true if key is set, otherwise - false (for example when nx=true and key exists)
# preventDefault () static
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
# registerCriticalSection (csName: string) → number static
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.
Arguments:
csName
: stringCritical section name
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)
}
}
# registerEndpoint (endpointName: string, handler: function, authorizationRequiredopt: boolean, isDefaultopt: boolean, bypassHTTPLoggingopt: boolean) static
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 alwaysInternal server error
(for security reason)
Recommended way to throw a handled error inside endpoint handler is throw new UB.UBAbort('.....')
Arguments:
endpointName
: stringhandler
: functionauthorizationRequired
= true: booleanIf
true
UB will check for valid Authorization header before execute endpoint handlerisDefault
= false: booleanbypassHTTPLogging
= false: booleanDo not put HTTP body into log (for example if body contains sensitive information, like password)
// 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);
# reloadConfig () → boolean static
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)
Return:
true is config s valid and successfully reloaded
# removeUserSessions (userID: number, exceptCurrentopt: boolean) → boolean static
Remove all user sessions (logout user).
If exceptCurrent
is true
- do not remove current session (logout all other sessions except my).
Return:
true if user had had any session
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`)
}
}
# runInAutonomousTransaction (func: function) → * static
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.
Return:
a func
result
Arguments:
func
: function
# updateFTSIndex (entityName: string, instanceID: number) static
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.
# wrapEnterLeaveForUbMethod (enterText: string, originalMethod: function) → function static
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
Events
# 'commit:after' --> (connectionName: string)
Fires (by native code) for an UB.App just after RDBMS "commit" is successfully called
Arguments:
connectionName
: stringName of the committed connection
# 'commit:before' --> (connectionName: string)
Fires (by native code) for an UB.App just before RDBMS "commit" is called
App.on('commit:before', (connectionName) => {
console.debug('Event commit:before fired for connection', connectionName)
})
Arguments:
connectionName
: stringName of the connection being committed
# 'endpointName:after' --> (req: THTTPRequest, resp: THTTPResponse, endpointName: string)
Fires after endpoint execution. In example below handler is called before each getDocument
execution
Arguments:
req
: THTTPRequestresp
: THTTPResponseendpointName
: string
# 'endpointName:before' --> (req: THTTPRequest, resp: THTTPResponse, endpointName: string)
Fires before endpoint execution. In example below handler is called before each getDocument
execution
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)
Arguments:
req
: THTTPRequestresp
: THTTPResponseendpointName
: string
# 'launchEndpoint:after' --> (req: THTTPRequest, resp: THTTPResponse, endpointName: string)
Fires after any endpoint execution
Arguments:
req
: THTTPRequestresp
: THTTPResponseendpointName
: string
# 'launchEndpoint:before' --> (req: THTTPRequest, resp: THTTPResponse, endpointName: string)
Fires before any endpoint execution
Arguments:
req
: THTTPRequestresp
: THTTPResponseendpointName
: string
# 'rollback:after' --> (connectionName: string)
Fires (by native code) for an UB.App just after RDBMS "rollback" is called
Arguments:
connectionName
: stringName of the rollback connection
# 'rollback:before' --> (connectionName: string)
Fires (by native code) for an UB.App just before RDBMS "rollback" is called
App.on('rollback:before', (connectionName) => {
console.debug('Event rollback:before fired for connection', connectionName)
})
Arguments:
connectionName
: stringName of the connection being rollback
# applicationReady --> ()
Fires inside each working thread for an 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.
# domainIsLoaded --> ()
Fires inside each working thread for an 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.
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
}
}
})
# enterConnectionContext --> ()
Fires (by native code) for an 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
# getAppInfo --> (appInfo: object)
Models can modify getAppInto
response by mutate an appInto
parameter of getAppInfo
App event.
WARNING getAppInto
endpoint is called often, so event handler should be as fast as possible.
Since getAppInto
endpoint is called for non-authorised users, response MUST NOT contain a sensitive data
App.on('getAppInfo', function(appInfo) {
const serverConfig = App.serverConfig
const DSTU = serverConfig.security && serverConfig.security.dstu
appInfo.trafficEncryption = DSTU ? DSTU.trafficEncryption : false
})
Arguments:
appInfo
: object