/* eslint-disable prefer-promise-reject-errors */
/* global Promise, btoa */
/**
* Connection to UnityBase server for asynchronous clients (NodeJS, Browser)
*
* @module AsyncConnection
* @memberOf module:@unitybase/ub-pub
*/
const EventEmitter = require('./events')
const ubUtils = require('./utils')
const transport = require('./transport')
const csShared = require('@unitybase/cs-shared')
const LocalDataStore = csShared.LocalDataStore
const UBSession = csShared.UBSession
const UBDomain = csShared.UBDomain
const UBCache = require('./UBCache')
const SHA256 = require('@unitybase/cryptojs/sha256')
const HMAC_SHA256 = require('@unitybase/cryptojs/hmac-sha256')
const MD5 = require('@unitybase/cryptojs/md5')
const UBNotifierWSProtocol = require('./UBNotifierWSProtocol')
const ClientRepository = require('./ClientRepository')
// regular expression for URLs server not require authorization.
const NON_AUTH_URLS_RE = /(\/|^)(auth|getAppInfo|secondFactorConfirm)(\/|\?|$)/
// all request passed in this timeout to run will be sent into one runList server method execution
const BUFFERED_DELAY = 20
const AUTH_METHOD_URL = 'auth'
const ANONYMOUS_USER = 'anonymous'
const AUTH_SCHEMA_FOR_ANONYMOUS = 'None'
const LDS = typeof localStorage !== 'undefined' ? localStorage : false
/**
* Called by UBConnection on first authorized request.
*
* Must return promise what resolves to object with `authSchema` and other schema-depending authorization parameters,
* for example for UB schema: `{authSchema: 'UB', login: login, password: password }`
*
* For anonymous requests (when authorization is turned off for application) should return promise, resolved to `{authSchema: 'None'}`
*
* @callback authParamsCallback
* @param {UBConnection} conn
* @param {boolean} isRepeat true in case first auth request is invalid (wrong credentials returned in previous callback result)
*/
/**
* Called by UBConnection in case 2FA confirmation is required.
*
* In case second factor confirmation method is 'Code' callback should ask user for confirmation code and return code,
* for 'App' method callback should show a message like 'Please, confirm login on your device and press `Continue`' and
* resolves after user press Continue
*
* @callback request2faCallback
* @param {UBConnection} conn
* @param {UBSession} session
* @param {string} secondFactorConfirmationMethod 'Code' or 'App'
* @param {boolean} isRepeat
* @returns {Promise<string|void>}
*/
/**
* @classdesc
* Connection to the UnityBase server (for asynchronous client like Node.js or Browser)
*
* In case host set to value other than `location.host` server must be configured to accept
* <a href="https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS">CORS</a> requests.
* This is usually done by setting "HTTPAllowOrigin" server configuration option.
*
* > Recommended way to create a UBConnection is {@link module:@unitybase/ub-pub#connect UB.connect} method
*
* @example
// !! In most case UB.connect should be used to create a connection !!
// But can be created directly, for example in case of multiple connection from one page
const UB = require('@ubitybase/ub-pub')
const UBConnection = UB.UBConnection
// connect using UBIP schema
let conn = new UBConnection({
host: 'http://127.0.0.1:888',
requestAuthParams: function(conn, isRepeat){
if (isRepeat){
throw new UB.UBAbortError('invalid credential')
} else {
return Promise.resolve({authSchema: 'UBIP', login: 'admin'})
}
}
})
conn.query({entity: 'uba_user', method: 'select', fieldList: ['ID', 'name']}).then(UB.logDebug)
// Anonymous connect. Allow access to entity methods, granted by ELS rules to `Anonymous` role
// Request below will be success if we grant a `ubm_navshortcut.select` to `Anonymous` on the server side
let conn = new UBConnection({
host: 'http://127.0.0.1:8888'
})
conn.query({entity: 'ubm_navshortcut', method: 'select', fieldList: ['ID', 'name']}).then(UB.logDebug)
//subscribe to events
conn.on('authorizationFail', function(reason){
// indicate user credential is wrong
})
conn.on('authorized', function(ubConnection, session, authParams){console.debug(arguments)} )
* UBConnection mixes an EventEmitter, so you can subscribe for {@link event:authorized authorized}
* and {@link event:authorizationFail authorizationFail} events.
*
* @class UBConnection
* @mixes EventEmitter
* @param {object} connectionParams connection parameters
* @param {string} connectionParams.host UnityBase server host
* @param {string} [connectionParams.appName='/'] UnityBase application to connect to (obsolete)
* @param {authParamsCallback} [connectionParams.requestAuthParams] Auth parameters callback
* @param {request2faCallback} [connectionParams.request2fa] Second factor request callback
* @param {string} [connectionParams.protocol] either 'https' or 'http' (default)
* @param {boolean} [connectionParams.allowSessionPersistent=false] See {@link connect} for details
* @param {object} [connectionParams.defHeaders] XHR request headers, what will be added to each xhr request for this connection
* (after adding headers from UB.xhr.defaults). Object keys is header names. Example: `{"X-Tenant-ID": "12"}`
*/
function UBConnection (connectionParams) {
const host = connectionParams.host || 'http://localhost:8881'
let appName = connectionParams.appName || '/'
let requestAuthParams = connectionParams.requestAuthParams
let request2fa = connectionParams.request2fa
let baseURL
/*
* Current session (Promise). Result of {@link UBConnection#auth auth} method
* {@link UBConnection#xhr} use this promise as a first `then` in call chain. In case of 401 response
* authPromise recreated.
*/
let currentSession
EventEmitter.call(this)
Object.assign(this, EventEmitter.prototype)
/**
* Fired for {@link UBConnection} instance in case authentication type CERT and simpleCertAuth is true
* just after private key is loaded and certificate is parsed but before auth handshake starts.
*
* Here you can extract username from certificate. By default, it is EDPOU or DRFO or email.
*
* @event defineLoginName
* @memberOf module:@unitybase/ub-pub.module:AsyncConnection~UBConnection
* @param {UBConnection} conn
* @param {object} urlParams
* @param {object} certInfo
*/
/**
* WebSocket `ubNotifier` protocol instance
*
* @type {UBNotifierWSProtocol}
*/
this.ubNotifier = null
/**
* Application settings transferred form a server
*
* @type {{}}
*/
this.appConfig = {}
/**
* The preferred (used in previous user session if any or a default for application) locale
*
* @type {string}
*/
this.preferredLocale = 'en'
/**
* Domain information. Initialized after promise, returned by function {@link UBConnection#getDomainInfo getDomainInfo} is resolved
*
* @type {UBDomain}
*/
this.domain = null
/**
* Allow to override a connection requestAuthParams function passed as config to UBConnection instance
*
* @param {authParamsCallback} authParamsFunc Function with the same signature as requestAuthParams parameter in UBConnection constructor
*/
this.setRequestAuthParamsFunction = function (authParamsFunc) {
requestAuthParams = authParamsFunc
}
/**
* Allow to override a connection onRequest2fa function passed as config to UBConnection instance
*
* @param {request2faCallback} onRequest2fa Function with the same signature as requestAuthParams parameter in UBConnection constructor
*/
this.setRequest2faFunction = function (onRequest2fa) {
request2fa = onRequest2fa
}
if (appName.charAt(0) !== '/') {
appName = '/' + appName
}
/**
* For a browser env. check silence kerberos login {@see UB.connect UB.connect} for details
*
* @param {UBConnection} conn
* @param {boolean} isRepeat
* @returns {*}
* @private
*/
function doOnCredentialsRequired (conn, isRepeat) {
// only anonymous authentication or requestAuthParams not passed in config
if (!conn.authMethods.length || !requestAuthParams) {
if (isRepeat) {
throw new ubUtils.UBError('Access deny')
} else {
return Promise.resolve({ authSchema: AUTH_SCHEMA_FOR_ANONYMOUS, login: ANONYMOUS_USER })
}
}
return requestAuthParams(conn, isRepeat)
}
const serverURL = host + appName
/**
* UB Server URL with protocol and host
*
* @type {string}
* @readonly
*/
this.serverUrl = serverURL
// for react native window exists but window.location - not
baseURL = ((typeof window !== 'undefined') && window.location && (window.location.origin === host)) ? appName : serverURL
if (baseURL.charAt(baseURL.length - 1) !== '/') baseURL = baseURL + '/'
/**
* The base of all urls of your requests. Will be prepended to all urls while call UB.xhr
*
* @type {string}
* @readonly
*/
this.baseURL = baseURL
/**
* UB application name
*
* @type {string}
* @readonly
*/
this.appName = appName
this.allowSessionPersistent = connectionParams.allowSessionPersistent && (LDS !== false)
if (this.allowSessionPersistent) this.__sessionPersistKey = this.serverUrl + ':storedSession'
this.cache = null
/**
* Store a cached entities request.
* - keys is calculated from attributes using this.cacheKeyCalculate
* - values is a xhr promise
* UBConnection use this map internally to prevent duplicate requests to server for cached entities data,
* for example in case we put several combobox on form with the same cached entity
*
* @private
* @type {{key: string, Promise}}
*/
this._pendingCachedEntityRequests = {}
/**
* Last successful login name. Filled AFTER first 401 response
*
* @type {string}
*/
this.lastLoginName = ''
this._bufferedRequests = []
this._bufferTimeoutID = 0
this.uiTag = ''
/**
* Additional headers what will be added to each XHR request
*
* @type {object|{}}
*/
this.defHeaders = connectionParams.defHeaders || {}
/**
* Is user currently logged in. There is no guaranty what session actually exist in server
*
* @returns {boolean}
*/
this.isAuthorized = function () {
return (currentSession !== undefined)
}
/**
* Return current user logon name or 'anonymous' in case not logged in
*
* @returns {string}
*/
this.userLogin = function () {
return this.userData('login')
}
/**
* Return current user language or 'en' in case not logged in
*
* @returns {string}
*/
this.userLang = function () {
return this.userData('lang')
}
/**
* Return custom data for logged-in user, or {lang: 'en', login: 'anonymous'} in case not logged in
*
* If key is provided - return only key part of user data. For a list of possible keys see
* <a href="../server-v5/namespace-Session.html#uData">Session.uData</a> in server side documentation
*
* @example
$App.connection.userData('lang');
// or the same but dedicated alias
$App.connection.userLang()
* @param {string} [key] Optional key
* @returns {*}
*/
this.userData = function (key) {
const uData = this.isAuthorized()
? currentSession.userData
: { lang: this.appConfig.defaultLang || 'en', login: ANONYMOUS_USER }
return key ? uData[key] : uData
}
function udot (conn, lg) {
if ((typeof document === 'undefined') || (typeof window === 'undefined') || typeof btoa !== 'function') return
if (!document.body || !window.location || !window.encodeURIComponent) return
const h = window.location.hostname
const appV = conn.appConfig.appVersion
if (/(localhost|0.0.1)/.test(h)) return
if (/-dev$/.test(window.location.pathname)) return
const aui = conn.appConfig.uiSettings.adminUI
let apn = aui && aui.applicationName
if (apn && typeof apn === 'object') {
const k = Object.keys(apn)[0]
apn = apn[k]
} else if (typeof apn !== 'string') {
apn = '-'
}
apn = apn.replace(/<\/?[^>]+(>|$)/g, '').slice(0, 50).replace(/[:", ]/g, '.')
const ut = btoa(window.encodeURIComponent(`${conn.serverVersion}:${MD5(lg)}:${apn}:${h}:${appV}`))
const t = document.createElement('img')
t.style.position = 'absolute'
t.style.display = 'none'
t.style.width = t.style.height = '0px'
t.style.display = 'none'
t.src = `https://unitybase.info/udot.gif?ut=${ut}&rn=${Math.trunc(1e6 * Math.random())}`
document.body.appendChild(t)
}
/**
* @private
* @param data
* @param secretWord
* @param authSchema
* @param {boolean} [restored=false] true in case session is restored from persistent storage
* @returns {UBSession}
*/
function doCreateNewSession (data, secretWord, authSchema, restored = false) {
const ubSession = new UBSession(data, secretWord, authSchema)
if (data.secondFactor) {
ubSession._secondFactor = data.secondFactor
}
// noinspection JSAccessibilityCheck
const userData = ubSession.userData
// noinspection JSPotentiallyInvalidUsageOfThis
if (!userData.lang || this.appConfig.supportedLanguages.indexOf(userData.lang) === -1) {
// noinspection JSPotentiallyInvalidUsageOfThis
userData.lang = this.appConfig.defaultLang
}
csShared.formatByPattern.setDefaultLang(userData.lang)
if (!restored) udot(this, userData.login)
return ubSession
}
/**
* The starter method for all authorized requests to UB server. Return authorization promise resolved to {@link UBSession}.
* In case unauthorized:
*
* - call requestAuthParams method passed to UBConnection constructor to retrieve user credentials
* - call {@link UBConnection#doAuth} method
*
* Used inside {@link UBConnection#xhr}, therefore developer rarely call it directly
*
* @function
* @param {boolean} [isRepeat] in case user provide wrong credential - we must show logon window
* @fires authorized
* @fires authorizationFail
* @returns {Promise<UBSession>} Resolved to {UBSession} if auth success or rejected to `{errMsg: string, errCode: number, errDetails: string}` if fail
*/
this.authorize = async function (isRepeat) {
const me = this
if (currentSession) return currentSession
if (this.allowSessionPersistent && !isRepeat) {
const storedSession = LDS.getItem(this.__sessionPersistKey)
if (storedSession) {
try {
const parsed = JSON.parse(storedSession)
currentSession = doCreateNewSession.call(this, parsed.data, parsed.secretWord, parsed.authSchema, true)
me.emit('authorized', me, currentSession)
// setup key ejection watchdog for CERT2 auth
const authSchema = LDS.getItem(ubUtils.LDS_KEYS.LAST_AUTH_SCHEMA)
if ((authSchema === 'CERT2') &&
(this.appConfig.uiSettings.adminUI?.authenticationCert?.preventLogoutOnKeyEjected !== true)
) {
const ti = LDS.getItem('iitSignWebKeyMediaType')
const di = LDS.getItem('iitSignWebKeyDevice')
if (di !== 'null') { // do not set watchdog up for file based key (device index === null)
// delay this.pki call since Vue is not loaded yet, but required by pki implementation
setTimeout(() => {
me.pki().then(pkiIntf => {
if (pkiIntf.direct.setupCert2KeyWatchdog) {
pkiIntf.direct.setupCert2KeyWatchdog(ti, di)
}
})
}, 10000)
}
}
return Promise.resolve(currentSession)
} catch (e) {
LDS.removeItem(this.__sessionPersistKey) // wrong session persistent data
}
}
}
// this.exchangeKeysPromise = null
const authParams = await doOnCredentialsRequired(this, isRepeat)
const lastAuthType = authParams.authSchema
try {
const session = await me.doAuth(authParams)
if (session._secondFactor) {
// remove previously stored session and restore it on 2FA success
const storedSession = LDS.getItem(this.__sessionPersistKey)
if (storedSession) LDS.removeItem(this.__sessionPersistKey)
// 3 attempts for 2FA
for (let attempt = 0; attempt < 3; attempt++) {
const is2fsRepeat = attempt > 0
const secret = await request2fa(me, session, session._secondFactor, is2fsRepeat)
// for `App` 2FA kind secret can be empty
const confirmResp = await me.post('/secondFactorConfirm', { sessionID: session.sessionID, secret })
if (confirmResp.data.success) {
delete session._secondFactor
if (storedSession) LDS.setItem(this.__sessionPersistKey, storedSession) // restore possible stored session
break
} else if (confirmResp.data.needRepeatAuth || (attempt >= 2)) { // session no longer valid or 3 attempt - throw to repeat whole auth
throw new ubUtils.UBError('Auth2faAttemptsExceeded')
}
// else - ask for code again
}
}
currentSession = session
if (LDS) {
LDS.setItem(ubUtils.LDS_KEYS.LAST_LOGIN, session.logonname)
LDS.setItem(ubUtils.LDS_KEYS.LAST_AUTH_SCHEMA, lastAuthType) // session.authSchema
}
/**
* Fired for {@link UBConnection} instance after success authorization
*
* @event authorized
* @memberOf module:@unitybase/ub-pub.module:AsyncConnection~UBConnection
* @param {UBConnection} conn
* @param {UBSession} session
* @param {object} [authParams]
*/
me.emit('authorized', me, session, authParams)
return session
} catch (reason) {
if (LDS) {
LDS.removeItem(ubUtils.LDS_KEYS.SILENCE_KERBEROS_LOGIN)
if (this.allowSessionPersistent) LDS.removeItem(this.__sessionPersistKey)
}
const parsedErr = ubUtils.parseUBAuthError(reason, authParams)
if (!parsedErr || !(parsedErr instanceof ubUtils.UBAbortError)) {
/**
* Fired for {@link UBConnection} instance in case of bad authorization
*
* @event authorizationFail
* @memberOf module:@unitybase/ub-pub.module:AsyncConnection~UBConnection
* @param {*} reason
* @param {UBConnection} conn
*/
me.emit('authorizationFail', parsedErr, me)
}
}
// auth fail - repeat
return me.authorize(true)
}
/**
* Clear current user authorization promise. Next request repeat authorization
*
* @private
*/
this.authorizationClear = function () {
this.lastLoginName = this.userLogin()
currentSession = undefined
}
/**
* Switch current session. Use only on server side
*
* @param {UBSession} session
*/
this.switchCurrentSession = function (session) {
currentSession = session
}
/**
* UBIP Auth schema implementation
*
* @param {object} authParams
* @returns {Promise}
* @private
*/
this.authHandshakeUBIP = function (authParams) {
if (!authParams.login) {
return Promise.reject({ errMsg: 'invalid user name' })
}
return this.post(AUTH_METHOD_URL, '', { headers: { Authorization: authParams.authSchema + ' ' + authParams.login } })
}
/**
* openID Connect auth schema.
* This function act as a proxy but change authSchema back to 'UB' for authorization token generation
*
* @param {object} authParams
* @returns {*}
* @private
*/
this.authHandshakeOpenIDConnect = function (authParams) {
return Promise.resolve(authParams).then(function (authParams) {
authParams.authSchema = 'UB'
return authParams
})
}
/**
* UB Auth schema implementation
*
* @param {object} authParams
* @returns {Promise<XHRResponse>}
* @private
*/
this.authHandshakeUB = async function (authParams) {
const me = this
let secretWord
if (!authParams.login || !authParams.password) {
return Promise.reject({ errMsg: 'invalid user name or password' })
}
const resp = await this.post(AUTH_METHOD_URL, '', {
params: {
AUTHTYPE: authParams.authSchema,
userName: authParams.login
}
})
let serverNonce, pwdHash, pwdForAuth
const request = {
params: {
AUTHTYPE: authParams.authSchema,
userName: authParams.login,
stage: 2,
prefUData: authParams.prefUData
}
}
const reqBody = {
password: '',
clientNonce: ''
}
const clientNonce = me.authMock
? SHA256('1234567890abcdef').toString()
: SHA256(new Date().toISOString().slice(0, 16)).toString()
reqBody.clientNonce = clientNonce
if (resp.data.connectionID) {
request.params.connectionID = resp.data.connectionID
}
// LDAP AUTH?
const realm = resp.data.realm
if (realm) {
serverNonce = resp.data.nonce
if (!serverNonce) {
throw new Error('invalid LDAP auth response')
}
// window.btoa(authParams.password) fails on non Latin1 chars
pwdForAuth = await ubUtils.base64FromAny(authParams.password)
secretWord = pwdForAuth // unsecured - to be used only with HTTPS!!
} else {
serverNonce = resp.data.result
if (!serverNonce) {
throw new Error('invalid auth response')
}
pwdHash = SHA256('salt' + authParams.password).toString()
const appForAuth = appName === '/' ? '/' : appName.replace(/\//g, '')
if (request.params.userName) request.params.userName = request.params.userName.toLowerCase() // uba_user stores name in lower case
pwdForAuth = SHA256(appForAuth.toLowerCase() + serverNonce + clientNonce + request.params.userName + pwdHash).toString()
secretWord = pwdHash
}
reqBody.password = pwdForAuth
return me.post(AUTH_METHOD_URL, reqBody, request).then(function (response) {
response.secretWord = secretWord
return response
})
}
/**
* Do authentication in UnityBase server. Usually called from UBConnection #authorize method in case authorization expire or user not authorized.
* Resolve to {@link UBSession} session object.
*
* @private
* @param {object} authParams
* @param {string} [authParams.authSchema] Either 'UB' (default) or 'CERT'. On case of CERT UBDesktop service NPI extension must be installed in browser
* @param {string} [authParams.login] Optional login
* @param {string} [authParams.password] Optional password
* @returns {Promise<UBSession>} Authentication promise. Resolved to {@link UBSession} is auth success or rejected to {errMsg: string, errCode: number, errDetails: string} if fail
*/
this.doAuth = function (authParams) {
authParams.authSchema = authParams.authSchema || 'UB'
if (this.isAuthorized()) {
return Promise.reject({ errMsg: 'invalid auth call', errDetails: 'contact developers' })
}
let promise
authParams.prefUData = this.getPreferredUData(authParams.login)
switch (authParams.authSchema) {
case AUTH_SCHEMA_FOR_ANONYMOUS:
promise = Promise.resolve({ data: { result: '0+0', uData: JSON.stringify({ login: ANONYMOUS_USER }) }, secretWord: '' })
break
case 'UB':
promise = this.authHandshakeUB(authParams)
break
case 'CERT2':
promise = this.pki().then(pkiInterface => pkiInterface.authHandshakeCERT2(authParams))
break
case 'UBIP':
promise = this.authHandshakeUBIP(authParams)
break
case 'OpenIDConnect':
promise = this.authHandshakeOpenIDConnect(authParams)
break
case 'Negotiate':
promise = this.post(AUTH_METHOD_URL, '', {
params: {
USERNAME: '',
AUTHTYPE: authParams.authSchema,
prefUData: authParams.prefUData
}
}).then(function (resp) {
resp.secretWord = resp.headers('X-UB-Nonce')
if (!resp.secretWord) throw new Error('X-UB-Nonce header is required to complete Negotiate authentication. Please, upgrade UB to >= 5.17.9')
return resp
})
break
default:
promise = Promise.reject({ errMsg: 'invalid authentication schema ' + authParams.authSchema })
break
}
promise = promise.then(
(authResponse) => {
const ubSession = doCreateNewSession.call(this, authResponse.data, authResponse.secretWord, authParams.authSchema)
if (this.allowSessionPersistent) {
LDS.setItem(
this.__sessionPersistKey,
JSON.stringify({ data: authResponse.data, secretWord: authResponse.secretWord, authSchema: authResponse.authSchema })
)
}
return ubSession
}
)
return promise
}
this.recordedXHRs = []
/**
* Set it to `true` for memorize all requests to recordedXHRs array (for debug only!)
*
* @type {boolean}
*/
this.recorderEnabled = false
}
/**
* Initialize client cache. Called from application after obtain userDbVersion
*
* - recreate Indexed Db database if version changed
* - create instance of UBCache (accessible via {@link UBConnection#cache UBConnection.cache} property) and clear UBCache.SESSION store.
*
* @param {number} userDbVersion Indexed DB database version required for current application
* @returns {Promise}
* @private
*/
UBConnection.prototype.initCache = function (userDbVersion) {
const dbName = this.baseURL === '/' ? 'UB' : this.baseURL
/**
* @property {UBCache} cache
* @readonly
* @type {UBCache}
*/
this.cache = new UBCache(dbName, userDbVersion)
/**
* List of keys, requested in the current user session.
* Cleared each time login done
*
* @property {object} cachedSessionEntityRequested
*/
this.cachedSessionEntityRequested = {}
// clear use session store
return this.cache.createStorage().then(cache => cache.clear(UBCache.SESSION))
}
/**
* Calculate cache key for request. This key is used to store data inside UBCache
*
* @param {string} root This is usually entity name
* @param {Array<string>} [attributes] if present - add attributes hash. This is usually array of entity attributes we want to put inside cache
* @returns {string}
*/
UBConnection.prototype.cacheKeyCalculate = function (root, attributes) {
const keyPart = [this.userLogin().toLowerCase(), this.userLang(), root]
if (Array.isArray(attributes)) {
keyPart.push(MD5(JSON.stringify(attributes)).toString())
}
return keyPart.join('#').replace(/[\\:.]/g, '#') // replace all :, \ -> #;
}
/**
* Refresh all cache occurrence for root depending on cacheType:
*
* - if `Session` - clear indexedDB for this root.
* - if `SessionEntity` - remove entry in {@link UBConnection#cachedSessionEntityRequested}
* - else - do nothing
*
* @param {string} root Root part of cache key. The same as in {@link UBConnection#cacheKeyCalculate}
* @param {UBCache.cacheTypes} cacheType
* @returns {Promise}
*/
UBConnection.prototype.cacheOccurrenceRefresh = function (root, cacheType) {
const me = this
let promise = Promise.resolve(true)
if (cacheType === UBCache.cacheTypes.Session || cacheType === UBCache.cacheTypes.SessionEntity) {
const entity = this.domain.get(root)
if (entity && entity.hasMixin('unity')) {
const unityMixin = entity.mixin('unity')
const unityEntity = this.domain.get(unityMixin.entity)
if (unityEntity && (unityMixin.entity !== root) && (unityEntity.cacheType !== UBCache.cacheTypes.None)) {
promise = promise.then(
() => me.cacheOccurrenceRefresh(unityMixin.entity, unityEntity.cacheType)
)
}
}
const cacheKey = me.cacheKeyCalculate(root)
const machRe = new RegExp('^' + cacheKey)
const machKeys = Object.keys(me.cachedSessionEntityRequested).filter(function (item) {
return machRe.test(item)
})
machKeys.forEach(function (key) {
delete me.cachedSessionEntityRequested[key]
})
if (cacheType === UBCache.cacheTypes.Session) {
promise = promise.then(function () {
return me.cache.removeIfMach(machRe, UBCache.SESSION)
})
}
}
return promise
}
/**
* Remove all cache occurrence for root depending on cacheType:
*
* - clear indexedDB for this root.
* - remove entry in {@link UBConnection#cachedSessionEntityRequested}
*
* @param {string} root Root part of cache key. The same as in {@link UBConnection#cacheKeyCalculate}
* @param {string} cacheType One of {@link UBCache#cacheTypes}
* @returns {Promise}
*/
UBConnection.prototype.cacheOccurrenceRemove = function (root, cacheType) {
const me = this
const cacheKey = me.cacheKeyCalculate(root)
const machRe = new RegExp('^' + cacheKey)
const machKeys = Object.keys(me.cachedSessionEntityRequested).filter(function (item) {
return machRe.test(item)
})
machKeys.forEach(function (key) {
delete me.cachedSessionEntityRequested[key]
})
const cacheStore = (cacheType === UBCache.cacheTypes.Session) ? UBCache.SESSION : UBCache.PERMANENT
return me.cache.removeIfMach(machRe, cacheStore)
}
/**
* Clear all local cache (indexedDB session & permanent and UBConnection.cachedSessionEntityRequested)
*
* @returns {Promise}
*/
UBConnection.prototype.cacheClearAll = function () {
const me = this
Object.keys(me.cachedSessionEntityRequested).forEach(function (item) {
delete me.cachedSessionEntityRequested[item]
})
return Promise.all([me.cache.clear(UBCache.SESSION), me.cache.clear(UBCache.PERMANENT)])
}
/**
* The same as {@link module:@unitybase/ub-pub#get UB.get} but with authorization
*
* @example
// call entity method using rest syntax
const certResp = await UB.connection.get('/rest/uba_usercertificate/getCertificate?ID=334607980199937')
const certBin = certResp.data
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {object} [config] optional configuration object - see {@link module:@unitybase/ub-pub#xhr UB.xhr}
* @returns {Promise<XHRResponse>} Future object
*/
UBConnection.prototype.get = function (url, config) {
return this.xhr(Object.assign({}, config, {
method: 'GET',
url
}))
}
/**
* The same as {@link module:@unitybase/ub-pub#post UB.post} but with authorization
*
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {*} data Request content
* @param {object} [config] optional configuration object - see {@link module:@unitybase/ub-pub#xhr UB.xhr}
* @returns {Promise<XHRResponse>} Future object
*/
UBConnection.prototype.post = function (url, data, config) {
return this.xhr(Object.assign({}, config, {
method: 'POST',
url,
data
}))
}
// noinspection JSUnusedLocalSymbols
/**
* @param {UBSession} session
* @param {object} cfg
* @returns {boolean}
*/
UBConnection.prototype.checkChannelEncryption = function (session, cfg) {
return true
}
/**
* Shortcut method to perform authorized/encrypted request to application we connected.
* Will:
*
* - add Authorization header for non-anonymous sessions
* - add {@link UBConnection#baseURL} to config.url
* - call {@link module:@unitybase/ub-pub#xhr UB.xhr}
* - in case server return 401 clear current authorization, call {@link UBConnection#authorize} and repeat the request
*
* By default, `xhr` retrieve data in JSON format, but for `ubql` endpoint can accept `Content-Type` header and
* serialize `DataStore` in one of:
* - `text/xml; charset=UTF-8`
* - `application/vnd.oasis.opendocument.spreadsheet`
* - `text/csv; charset=UTF-8`
* - `text/html; charset=UTF-8`
*
* @example
// get response as CSV and save it to file
const { data } = await UB.connection.xhr({
method: 'POST',
url: 'ubql',
data: [UB.Repository.attrs('*').ubql()],
responseType: 'blob',
headers: { 'Content-Type': 'text/csv; charset=UTF-8' }
})
window.saveAs(data, `${fileName}.csv`)
* @param {object} config Request configuration as described in {@link module:@unitybase/ub-pub#xhr UB.xhr}
* @fires passwordExpired
* @returns {Promise<XHRResponse>}
*/
UBConnection.prototype.xhr = function (config) {
const me = this
const cfg = Object.assign({ headers: Object.assign({}, this.defHeaders) }, config)
const url = cfg.url
let promise
if (cfg.data &&
((cfg.data instanceof ArrayBuffer) || (cfg.data.toString() === '[object File]') || ArrayBuffer.isView(cfg.data)) && // (cfg.data instanceof File) not work in Node.js
!(cfg.headers['Content-Type'] || cfg.headers['content-type'])
) {
cfg.headers['Content-Type'] = 'application/octet-stream' // force binary content-type header
}
if (me.recorderEnabled) {
me.recordedXHRs.push(config)
}
// prepend baseURl only if not already prepended
if (url.length < me.baseURL.length || url.substring(0, me.baseURL.length) !== me.baseURL) {
cfg.url = me.baseURL + cfg.url
}
if (NON_AUTH_URLS_RE.test(url)) { // request not require authentication - pass is as is
promise = transport.xhr(cfg)
} else {
promise = me.authorize().then((session) => me.checkChannelEncryption(session, cfg))
promise = promise.then(function () {
// we must repeat authorize to obtain new session key ( because key exchange may happen before)
return me.authorize().then(/** @param {UBSession} session */ function (session) {
const head = session.authHeader(me.authMock)
if (head) cfg.headers.Authorization = head // do not add header for anonymous session
return transport.xhr(cfg)
})
}).catch(function (reason) { // in case of 401 - do auth and repeat request
let errMsg = ''
if (reason.status === 0) {
throw new ubUtils.UBError('serverIsBusy', 'network error')
}
if (reason.status === 401) {
if (me.allowSessionPersistent) LDS.removeItem(me.__sessionPersistKey) // addled session persisted data
ubUtils.logDebug('unauth: %j', reason)
if (me.isAuthorized()) {
me.authorizationClear()
}
// reason.config.url: "/bla-bla/logout"
if (reason.config.url && /\/logout/.test(reason.config.url)) {
me.lastLoginName = ''
} else {
transport.xhr.allowRequestReiteration() // prevent a monkeyRequestsDetected error during relogon [UB-1699]
return me.xhr(config)
}
}
if (reason.status === 413) { // Request Entity Too Large
throw new ubUtils.UBError('Request Entity Too Large')
} else if (reason.status === 502) { // Bad Gateway
throw new ubUtils.UBError('ERR_WAITING_SERVER_TO_START')
}
// eslint-disable-next-line no-prototype-builtins
if (reason.data && reason.data.hasOwnProperty('errCode')) { // this is server side error response
const errCode = reason.data.errCode
const errDetails = errMsg = reason.data.errMsg
errMsg = ubUtils.parseAndTranslateUBErrorMessage(errMsg)
/**
* Fired for {@link UBConnection} instance in case user password is expired.
* The only valid endpoint after this is `changePassword`
*
* Accept 1 arg `(connection: UBConnection)
*
* @event passwordExpired
* @memberOf module:@unitybase/ub-pub.module:AsyncConnection~UBConnection
*/
if ((errCode === 72) && me.emit('passwordExpired', me)) {
throw new ubUtils.UBAbortError()
}
throw new ubUtils.UBError(errMsg, errDetails, errCode)
} else if (reason.status === 403) {
throw new ubUtils.UBError('Access deny')
} else {
throw reason //! Important - rethrow the reason is important. Do not create a new Error here
}
})
}
return promise
}
/**
* Base64 encoded server certificate
*
* @property {string} serverCertificate
* @readonly
*/
/**
* Lifetime (in second) of session encryption
*
* @property {number} encryptionKeyLifetime
* @readonly
*/
/**
* Possible server authentication method
*
* @property {Array.<string>} authMethods
* @readonly
*/
/**
* Retrieve application information. Usually this is first method developer must call after create connection
*
* @function
* @returns {Promise} Promise resolved to result of getAppInfo method
*/
UBConnection.prototype.getAppInfo = function () {
const me = this
return me.get('getAppInfo') // non-auth request
.then(function (resp) {
const appInfo = resp.data
if (typeof appInfo !== 'object') {
// UB server or any proxy behind it might return an HTML page, when server is not available
throw new Error(`/getAppInfo response expected to to be an object, but got ${typeof appInfo}. Service may be unavailable or in maintenance mode`)
}
/**
* Is server require content encryption
*
* @property {boolean} trafficEncryption
* The base of all urls of your requests. Will be prepended to all urls.
* @readonly
*/
Object.defineProperty(me, 'trafficEncryption', { enumerable: true, writable: false, value: appInfo.trafficEncryption || false })
/**
* The server certificate for cryptographic operations (base46 encoded)
*
* @property {boolean} serverCertificate
* @readonly
*/
Object.defineProperty(me, 'serverCertificate', { enumerable: true, writable: false, value: appInfo.serverCertificate || '' })
Object.defineProperty(me, 'encryptionKeyLifetime', { enumerable: true, writable: false, value: appInfo.encryptionKeyLifetime || 0 })
Object.defineProperty(me, 'authMethods', { enumerable: true, writable: false, value: appInfo.authMethods })
Object.defineProperty(me, 'simpleCertAuth', { enumerable: true, writable: false, value: appInfo.simpleCertAuth || false })
/**
* An array of WebSocket protocol names supported by server
*
* @property {Array<string>} supportedWSProtocols
*/
Object.defineProperty(me, 'supportedWSProtocols', { enumerable: true, writable: false, value: appInfo.supportedWSProtocols || [] })
/**
* UnityBase server version
*
* @property {string} serverVersion
* @readonly
*/
Object.defineProperty(me, 'serverVersion', { enumerable: true, writable: false, value: appInfo.serverVersion || '' })
ubUtils.apply(me.appConfig, appInfo.uiSettings.adminUI)
const v = appInfo.serverVersion.split('.')
const isUBQLv2 = ((v[0] >= 'v5') && (v[1] >= 10))
/**
* UBQL v2 (value instead of values)
*
* @property {boolean} UBQLv2
* @readonly
*/
Object.defineProperty(me, 'UBQLv2', { enumerable: true, writable: false, value: isUBQLv2 })
ClientRepository.prototype.UBQLv2 = isUBQLv2
Object.defineProperty(me, 'authMock', { enumerable: false, writable: false, value: appInfo.authMock || false })
if (appInfo.supportedIrregularLang) {
csShared.formatByPattern.addIrregularLangToLocales(appInfo.supportedIrregularLang)
}
return appInfo
})
}
/**
* Retrieve domain information from server. Promise resolve instance of UBDomain
*
* @returns {Promise}
*/
UBConnection.prototype.getDomainInfo = function () {
const me = this
return me.get('getDomainInfo', {
params: { v: 4, userName: this.userLogin() }
}).then(function (response) {
const result = response.data
const domain = new UBDomain(result)
me.domain = domain
return domain
})
}
/**
* Process buffered requests from this._bufferedRequests
*
* @private
*/
UBConnection.prototype.processBuffer = function processBuffer () {
const bufferCopy = this._bufferedRequests
// get ready to new buffer queue
this._bufferTimeoutID = 0
this._bufferedRequests = []
const reqData = bufferCopy.map(r => r.request)
const rq = buildUriQueryPath(reqData)
const uri = `ubql?rq=${rq}&uitag=${this.uiTag}`
this.post(uri, reqData).then(
(responses) => {
// we expect responses in order we send requests to server
bufferCopy.forEach(function (bufferedRequest, num) {
bufferedRequest.deferred.resolve(responses.data[num])
})
},
(failReason) => {
bufferCopy.forEach(function (bufferedRequest) {
bufferedRequest.deferred.reject(failReason)
})
}
)
}
/**
* Promise of running UBQL command(s) (asynchronously).
* The difference from {@link UBConnection.post} is:
*
* - ability to buffer request: can merge several `query` in the 20ms period into one ubql call
*
* For well known UnityBase methods use aliases (addNew, select, insert, update, doDelete)
*
* @param {object} ubq Request to execute
* @param {string} ubq.entity Entity to execute the method
* @param {string} ubq.method Method of entity to executed
* @param {Array.<string>} [ubq.fieldList]
* @param {object} [ubq.whereList]
* @param {object} [ubq.execParams]
* @param {number} [ubq.ID]
* @param {object} [ubq.options]
* @param {string} [ubq.lockType]
* @param {boolean} [ubq.__skipOptimisticLock] In case this parameter true and in the buffered
* @param {boolean} [ubq.__nativeDatasetFormat]
* @param {boolean} [allowBuffer] Allow buffer this request to single runList. False by default
* @function
* @returns {Promise}
*
* @example
//this two execution is passed to single ubql server execution
$App.connection.query({entity: 'uba_user', method: 'select', fieldList: ['*']}, true).then(UB.logDebug);
$App.connection.query({entity: 'ubm_navshortcut', method: 'select', fieldList: ['*']}, true).then(UB.logDebug);
//but this request is passed in separate ubql (because allowBuffer false in first request
$App.connection.query({entity: 'uba_user', method: 'select', fieldList: ['*']}).then(UB.logDebug);
$App.connection.query({entity: 'ubm_desktop', method: 'select', fieldList: ['*']}, true).then(UB.logDebug);
*/
UBConnection.prototype.query = function query (ubq, allowBuffer) {
const me = this
if (!allowBuffer || !BUFFERED_DELAY) {
const uri = `ubql?rq=${ubq.entity}.${ubq.method}&uitag=${this.uiTag}`
return me.post(uri, [ubq]).then(function (response) {
return response.data[0]
})
} else {
if (!this._bufferTimeoutID) {
this._bufferTimeoutID = setTimeout(me.processBuffer.bind(me), BUFFERED_DELAY)
}
return new Promise(function (resolve, reject) {
me._bufferedRequests.push({ request: ubq, deferred: { resolve, reject } })
})
}
}
/**
* @deprecated Since UB 1.11 use `query` method
* @private
*/
UBConnection.prototype.run = UBConnection.prototype.query
/**
* Promise of running UBQL command(s) (asynchronously).
*
* Result is array of objects or null.
*
* The difference from {@link UBConnection.post} is:
*
* - ability to buffer request: can merge several `query` in the 20ms period into one ubql call
*
* For well known UnityBase methods use aliases (addNew, select, insert, update, doDelete)
*
* @param {object} ubq Request to execute
* @param {string} ubq.entity Entity to execute the method
* @param {string} ubq.method Method of entity to executed
* @param {Array.<string>} [ubq.fieldList]
* @param {object} [ubq.whereList]
* @param {object} [ubq.execParams]
* @param {number} [ubq.ID]
* @param {object} [ubq.options]
* @param {string} [ubq.lockType]
* @param {boolean} [ubq.__skipOptimisticLock] In case this parameter true and in the buffered
* @param {boolean} [ubq.__nativeDatasetFormat]
* @param {Object<string, string>} [fieldAliases] Optional object to change attribute names during transform array to object. Keys are original names, values - new names
* @param {boolean} [allowBuffer] Allow buffer this request to single runList. False by default
* @function
* @returns {Promise<Array|null>}
*
* @example
//this two execution is passed to single ubql server execution
$App.connection.queryAsObject({entity: 'uba_user', method: 'select', fieldList: ['*']}, true).then(UB.logDebug);
$App.connection.queryAsObject({entity: 'ubm_navshortcut', method: 'select', fieldList: ['*']}, true).then(UB.logDebug);
//but this request is passed in separate ubql (because allowBuffer false in first request
$App.connection.queryAsObject({entity: 'uba_user', method: 'select', fieldList: ['*']}).then(UB.logDebug);
$App.connection.queryAsObject({entity: 'ubm_desktop', method: 'select', fieldList: ['*']}, true).then(UB.logDebug);
*/
UBConnection.prototype.queryAsObject = function queryAsObject (ubq, fieldAliases, allowBuffer) {
if (ubq.execParams && (ubq.method === 'insert' || ubq.method === 'update')) {
const newEp = stringifyExecParamsValues(ubq.execParams)
if (newEp) ubq.execParams = newEp
}
return this.query(ubq, allowBuffer).then(function (res) {
return (res.resultData && res.resultData.data && res.resultData.data.length)
? LocalDataStore.selectResultToArrayOfObjects(res, fieldAliases)
: null
})
}
/**
* Convert raw server response data to javaScript data according to attribute types.
* Called by {@link UBConnection#select}
* Currently only Data/DateTime & boolean conversion done
* If resultLock present - resultLock.lockTime also converted
*
* @example
// convert all string representation of date/dateTime to Date object, integer representation of bool to Boolean
return me.query({entity: 'my_entity', method: 'select'}, true)
.then(me.convertResponseDataToJsTypes.bind(me))
* @function
* @param serverResponse
* @returns {*}
*/
UBConnection.prototype.convertResponseDataToJsTypes = function (serverResponse) {
return LocalDataStore.convertResponseDataToJsTypes(this.domain, serverResponse)
}
/**
* Call a {@link LocalDataStore#doFilterAndSort} - see a parameters there
*
* @protected
* @param {TubCachedData} cachedData
* @param {UBQL} ubql
* @returns {object}
*/
UBConnection.prototype.doFilterAndSort = function (cachedData, ubql) {
return LocalDataStore.doFilterAndSort(cachedData, ubql)
}
/**
* Promise of running UBQL command with `addNew` method (asynchronously).
*
* Response "data" is an array of default values for row.
*
* Two difference from {@link class:UBConnection.query UBConnection.query}:
*
* - ubRequest.method set to 'addnew'
* - requests is always buffered in the 20ms period into one ubql call
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
*
* @example
$App.connection.addNew({entity: 'uba_user', fieldList: ['*']}).then(UB.logDebug)
// [{"entity":"uba_user","fieldList":["ID","isPending"],"method":"addnew",
// "resultData":{"fields":["ID","isPending"],"rowCount": 1, "data":[[332462711046145,0]]}
// }]
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {Array.<string>} serverRequest.fieldList
* @param {object} [serverRequest.execParams]
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @returns {Promise<object>}
*/
UBConnection.prototype.addNew = function (serverRequest) {
const me = this
serverRequest.method = 'addnew'
return me.query(serverRequest, true)
.then(me.convertResponseDataToJsTypes.bind(me))
}
/**
* Promise of running UBQL command with `addNew` method (asynchronously).
*
* Result is Object with default values for row.
*
* @example
$App.connection.addNewAsObject({"entity":"uba_user"}).then(UB.logDebug)
// result is {ID: 332462709833729, isPending: false}
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {Array.<string>} serverRequest.fieldList
* @param {object} [serverRequest.execParams]
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {Object<string, string>} [fieldAliases] Optional object to change attribute names during transform array to object. Keys are original names, values - new names
* @return {Promise<object>}
*/
UBConnection.prototype.addNewAsObject = function (serverRequest, fieldAliases) {
return this.addNew(serverRequest).then(function (res) {
return LocalDataStore.selectResultToArrayOfObjects(res, fieldAliases)[0]
})
}
/**
* Called in update/insert/delete methods and if request entity is cached then clear cache
*
* @private
* @param serverResponse
* @returns {Promise} Promise resolved to serverResponse
*/
UBConnection.prototype.invalidateCache = function (serverResponse) {
const me = this
const cacheType = me.domain.get(serverResponse.entity).cacheType
if (cacheType === UBCache.cacheTypes.none) {
return Promise.resolve(serverResponse)
}
return me.cacheOccurrenceRefresh(serverResponse.entity, cacheType).then(function () {
return serverResponse
})
}
/**
* Check execParams contains values of type Object and if Yes - return new execParams with stringified objects values
* else return false
*
* Since ub-pub@5.23.19 keys what contains . are not stringified (most likely this is JSON attribute property)
*
* @private
* @param {object} execParams
* @returns {object|false}
*/
function stringifyExecParamsValues (execParams) {
const keys = Object.keys(execParams)
const L = keys.length
let needTransform = false
for (let i = 0; i < L; i++) {
const v = execParams[keys[i]]
if (v && (typeof v === 'object') && !(v instanceof Date) && (keys[i].indexOf('.') === -1)) {
needTransform = true
break
}
}
if (!needTransform) return false
const newParams = {}
for (let i = 0; i < L; i++) {
const v = execParams[keys[i]]
newParams[keys[i]] = (v && (typeof v === 'object') && !(v instanceof Date) && (keys[i].indexOf('.') === -1))
? JSON.stringify(v)
: v
}
return newParams
}
/**
* Promise of running UBQL command with `update` method (asynchronously).
* Difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'update'
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
* - if necessary it will clear cache
*
* In case `fieldList` is passed - result will contains updated values for attributes specified in `fieldList`
* in Array representation
*
* @example
$App.connection.update({
entity: 'uba_user',
fieldList: ['ID','name', 'mi_modifyDate'],
execParams: {ID: 332462122205200, name:'test', mi_modifyDate:"2019-04-23T13:00:00Z"}
}).then(UB.logDebug);
// [{"entity":"uba_user","fieldList":["ID","name","mi_modifyDate"],
// "execParams":{"ID":332462122205200,"name":"test","mi_modifyDate":"2019-04-23T13:03:51Z","mi_modifyUser":10},
// "method":"update",
// "resultData":{"fields":["ID","name","mi_modifyDate"],"rowCount": 1,
// "data":[[332462122205200,"test","2019-04-23T13:03:51Z"]]}
// }]
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {string} [serverRequest.method='update'] Method of entity to executed
* @param {Array.<string>} [serverRequest.fieldList]
* @param {object} serverRequest.execParams Values to update. ID should be present
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {boolean} [allowBuffer=false] Allow several "in the same time" request to be buffered to one transaction.
* @returns {Promise<object>}
*/
UBConnection.prototype.update = function (serverRequest, allowBuffer) {
const me = this
serverRequest.method = serverRequest.method || 'update'
if (serverRequest.execParams) {
const newEp = stringifyExecParamsValues(serverRequest.execParams)
if (newEp) serverRequest.execParams = newEp
}
return me.query(serverRequest, allowBuffer)
.then(me.convertResponseDataToJsTypes.bind(me))
.then(me.invalidateCache.bind(me))
}
/**
* Promise of running UBQL command with `update` method (asynchronously).
*
* In case `fieldList` is passed - result will contain updated values for attributes specified in `fieldList` as Object;
* >If `fieldList` is not passed or empty - return `null`
*
* @example
$App.connection.updateAsObject({
entity: 'uba_user',
fieldList: ['ID','name','mi_modifyDate', 'isPending'],
execParams: {ID: 33246, name:'newName', mi_modifyDate:"2019-04-23T13:00:00Z"}
}).then(UB.logDebug);
// {"ID": 332462122205200, "name": newName", "mi_modifyDate": new Date("2019-04-23T13:03:51Z"), isPending: false}
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {string} [serverRequest.method='update'] Method of entity to executed
* @param {Array.<string>} [serverRequest.fieldList]
* @param {object} serverRequest.execParams Values to update. ID should be present
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {Object<string, string>} [fieldAliases] Optional object to change attribute names during transform array to object. Keys are original names, values - new names
* @param {boolean} [allowBuffer=false] Allow several "in the same time" request to be buffered to one transaction.
* @returns {Promise<object>}
*/
UBConnection.prototype.updateAsObject = function (serverRequest, fieldAliases, allowBuffer) {
return this.update(serverRequest, allowBuffer).then(function (res) {
return (res.resultData && res.resultData.data && res.resultData.data.length)
? LocalDataStore.selectResultToArrayOfObjects(res, fieldAliases)[0]
: null
})
}
/**
* Promise of running UnityBase UBQL command with `insert` method (asynchronously).
* Difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'insert'
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
* - if necessary it will clear cache
*
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {string} [serverRequest.method='insert'] Method of entity to executed
* @param {Array.<string>} serverRequest.fieldList
* @param {object} [serverRequest.execParams]
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {boolean} [allowBuffer=false] Allow several "in the same time" request to be buffered to one transaction.
* @function
* @returns {Promise}
* @example
$App.connection.insert({
entity: 'uba_user', fieldList: ['ID','name'], execParams: {ID: 1, name:'newName'}
}).then(UB.logDebug);
*/
UBConnection.prototype.insert = function (serverRequest, allowBuffer) {
const me = this
serverRequest.method = serverRequest.method || 'insert'
if (serverRequest.execParams) {
const newEp = stringifyExecParamsValues(serverRequest.execParams)
if (newEp) serverRequest.execParams = newEp
}
return me.query(serverRequest, allowBuffer)
.then(me.convertResponseDataToJsTypes.bind(me))
.then(me.invalidateCache.bind(me))
}
/**
* Promise of running UnityBase UBQL command with `insert` method (asynchronously).
*
* In case `fieldList` is passed - result will contains new values for attributes specified in `fieldList` as Object, otherwise - null
*
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {string} [serverRequest.method='insert'] Method of entity to executed
* @param {Array.<string>} [serverRequest.fieldList] Attributes to be returned in result
* @param {object} serverRequest.execParams Attributes values to be inserted. If `ID` is omitted it will be autogenerated
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {Object<string, string>} [fieldAliases] Optional object to change attribute names during transform array to object. Keys are original names, values - new names
* @param {boolean} [allowBuffer=false] Allow several "in the same time" request to be buffered to one transaction.
* @function
* @returns {Promise<object>}
* @example
$App.connection.insertAsObject({
entity:"uba_user",
fieldList:['ID', 'name', 'mi_modifyDate'],
execParams: {name: 'insertedName'}
}).then(UB.logDebug)
// {ID: 332462911062017, mi_modifyDate: Tue Apr 23 2019 17:04:30 GMT+0300 (Eastern European Summer Time), name: "insertedname"}
*/
UBConnection.prototype.insertAsObject = function (serverRequest, fieldAliases, allowBuffer) {
return this.insert(serverRequest, allowBuffer).then(function (res) {
return (res.resultData && res.resultData.data && res.resultData.data.length)
? LocalDataStore.selectResultToArrayOfObjects(res, fieldAliases)[0]
: null
})
}
/**
* Promise of running UBQL command with delete method (asynchronously).
* Difference from {@link UBConnection.query}:
*
* - ubRequest.method set to 'delete' by default
* - if necessary it will clear cache
*
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {string} [serverRequest.method] Method of entity to executed. Default to 'delete'
* @param {object} [serverRequest.execParams]
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {boolean} [allowBuffer] Default - false. Allow several "in the same time" request to be buffered to one transaction.
* @function
* @returns {Promise}
* @example
$App.connection.doDelete({
entity: 'uba_user', fieldList: ['ID','name'], execParams: {ID: 1, name:'newName'}
}).then(UB.logDebug)
*/
UBConnection.prototype.doDelete = function (serverRequest, allowBuffer) {
const me = this
serverRequest.method = serverRequest.method || 'delete'
return me.query(serverRequest, allowBuffer)
.then(me.invalidateCache.bind(me))
}
/**
* Promise of running UBQL (asynchronously).
* Two difference from {@link class:UBConnection.query UBConnection.query}:
*
* - ubRequest.method by default set to 'select'
* - requests is always buffered in the 20ms period into one ubql call
* - `Date` & 'DateTime' entity attributes are converted from ISO8601 text representation to javaScript Date object
* - if request entity is cached - cache used
*
* @param {object} serverRequest Request to execute
* @param {string} serverRequest.entity Entity to execute the method
* @param {string} [serverRequest.method] Method of entity to executed. Default to 'select'
* @param {number} [serverRequest.ID] if passed - request bypass cache, where & order list is ignored. Can be used to load single record from server
* @param {Array.<string>} serverRequest.fieldList
* @param {object} [serverRequest.whereList]
* @param {object} [serverRequest.execParams]
* @param {object} [serverRequest.options]
* @param {string} [serverRequest.lockType]
* @param {boolean} [serverRequest.alsNeed]
* @param {boolean} [serverRequest.__skipOptimisticLock] In case this parameter true and in the buffered
* @param {boolean} [bypassCache=false] Do not use cache while request even if entity cached.
* If `__mip_disablecache: true` is passed in serverRequest cache is also disabled.
* @function
* @returns {Promise}
* @example
//retrieve users
$App.connection.select({entity: 'uba_user', fieldList: ['*']}).then(UB.logDebug);
//retrieve users and desktops and then both done - do something
Promise.all($App.connection.select({entity: 'uba_user', fieldList: ['ID', 'name']})
$App.connection.select({entity: 'ubm_desktop', fieldList: ['ID', 'code']})
).then(UB.logDebug);
*/
UBConnection.prototype.select = function (serverRequest, bypassCache) {
const me = this
let dataPromise
bypassCache = bypassCache || (serverRequest.__mip_disablecache === true) || (serverRequest.__mip_recordhistory_all === true)
const cacheType = bypassCache || serverRequest.ID || serverRequest.bypassCache
? UBCache.cacheTypes.None
: me.domain.get(serverRequest.entity).cacheType
if (!serverRequest.method) {
serverRequest.method = 'select'
}
// if exist expression where ID = ... bypass cache
// if (idInWhere(serverRequest.whereList)){
// cacheType = cacheTypes.None;
// }
if (cacheType === UBCache.cacheTypes.None) { // where & order is done by server side
dataPromise = this.query(serverRequest, true)
.then(this.convertResponseDataToJsTypes.bind(this))
.then(response => {
const responseWithTotal = {}
ubUtils.apply(responseWithTotal, response)
if (response.__totalRecCount) {
responseWithTotal.total = response.__totalRecCount
} else if (response.resultData && response.resultData.data) {
const resRowCount = response.resultData.data.length
if (!serverRequest.options) {
responseWithTotal.total = resRowCount
} else {
const opt = serverRequest.options || {}
const start = opt.start ? opt.start : 0
const limit = opt.limit || 0
// in case we fetch less data then requested - this is last page and we know total
responseWithTotal.total = (resRowCount < limit) ? start + resRowCount : -1
}
}
return responseWithTotal
})
} else { // where & order is done by client side
return this._doSelectForCacheableEntity(serverRequest, cacheType)
}
return dataPromise
}
/**
* @private
* @param {object} serverRequest Request to execute
* @param {UBCache.cacheTypes} cacheType
*/
UBConnection.prototype._doSelectForCacheableEntity = function (serverRequest, cacheType) {
// TODO check all filtered attribute is present in whereList - rewrite me.checkFieldList(operation);
const cKey = this.cacheKeyCalculate(serverRequest.entity, serverRequest.fieldList)
const cacheStoreName = (cacheType === UBCache.cacheTypes.Session) ? UBCache.SESSION : UBCache.PERMANENT
// retrieve data either from cache or from server
return this.cache.get(cKey + ':v', cacheStoreName).then((version) => {
let cachedPromise
if (!version || // no data in cache or invalid version
// or must re-validate version
(version && cacheType === UBCache.cacheTypes.Entity) ||
// or SessionEntity cached not for current cache version
(version && cacheType === UBCache.cacheTypes.SessionEntity && this.cachedSessionEntityRequested[cKey] !== version)
) {
// remove where order logicalPredicates & limits
const serverRequestWOLimits = {}
Object.keys(serverRequest).forEach(function (key) {
if (['whereList', 'orderList', 'options', 'logicalPredicates'].indexOf(key) === -1) {
serverRequestWOLimits[key] = serverRequest[key]
}
})
serverRequestWOLimits.version = version || '-1'
const pendingCachedEntityRequest = this._pendingCachedEntityRequests[cKey]
? this._pendingCachedEntityRequests[cKey]
: this._pendingCachedEntityRequests[cKey] = this.query(serverRequestWOLimits, true)
cachedPromise = pendingCachedEntityRequest
.then( // delete pending request in any case
(data) => {
delete this._pendingCachedEntityRequests[cKey]
return data
},
(reason) => {
delete this._pendingCachedEntityRequests[cKey]
throw reason
}
)
.then(this.convertResponseDataToJsTypes.bind(this))
.then(response => this._cacheVersionedResponse(response, cacheStoreName, cKey))
} else { // retrieve data from cache
cachedPromise = this.cache.get(cKey, cacheStoreName)
}
return cachedPromise
}).then(cacheResponse => {
return this.doFilterAndSort(cacheResponse, serverRequest)
})
}
/**
* Put response to cache
*
* @private
* @param {object} serverResponse
* @param {object} serverResponse.resultData
* @param {boolean} [serverResponse.resultData.notModified]
* @param {string} storeName
* @param {string} cKey Cache key
* @returns {*}
*/
UBConnection.prototype._cacheVersionedResponse = function (serverResponse, storeName, cKey) {
if (serverResponse.resultData.notModified) {
// in case we refresh cachedSessionEntityRequested or just after login - put version to cachedSessionEntityRequested
this.cachedSessionEntityRequested[cKey] = serverResponse.version
return this.cache.get(cKey, storeName)
} else {
return this.cache.put([
{ key: cKey + ':v', value: serverResponse.version },
{ key: cKey, value: serverResponse.resultData }
], storeName).then(() => {
this.cachedSessionEntityRequested[cKey] = serverResponse.version
return serverResponse.resultData
})
}
}
/**
* Alias to {@link LocalDataStore#selectResultToArrayOfObjects LocalDataStore.selectResultToArrayOfObjects}
*
* @param {{resultData: {data: Array.<Array>, fields: Array.<string>}}} selectResult
* @returns {Array.<*>}
* @private
* @deprecated Use LocalDataStore.selectResultToArrayOfObjects
*/
UBConnection.selectResultToArrayOfObjects = LocalDataStore.selectResultToArrayOfObjects
/**
* Group several ubRequest into one server request (executed in singe transaction on server side)
*
* $App.connection.runTrans([
* { entity: 'my_entity', method: 'update', ID: 1, execParams: {code: 'newCode'} },
* { entity: 'my_entity', method: 'update', ID: 2, execParams: {code: 'newCodeforElementWithID=2'} },
* ]).then(UB.logDebug);
*
* @function
* @param {Array.<ubRequest>} ubRequestArray
* @returns {Promise} Resolved to response.data
*/
UBConnection.prototype.runTrans = function (ubRequestArray) {
for (const serverRequest of ubRequestArray) {
if (serverRequest.execParams && ((serverRequest.method === 'insert') || ((serverRequest.method === 'update')))) {
const newEp = stringifyExecParamsValues(serverRequest.execParams)
if (newEp) serverRequest.execParams = newEp
}
}
const rq = buildUriQueryPath(ubRequestArray)
const uri = `ubql?rq=${rq}&uitag=${this.uiTag}`
return this.post(uri, ubRequestArray).then((response) => response.data)
}
/**
* Group several ubRequest into one server request (executed in singe transaction on server side)
*
* Each response will be returned in the same array position as corresponding request.
*
* In case response contains `resultData` property of type {data: fields: } it will be converted to array-of-object dataStore format
*
* In case method is insert or update array is replaced by first element. Example below use one entity,
* but real app can use any combination of entities and methods
*
* @example
$App.connection.runTransAsObject([
{entity: "tst_aclrls", method: 'insert', fieldList: ['ID', 'caption'], execParams: {caption: 'inserted1'}},
{entity: "tst_aclrls", method: 'insert', opaqueParam: 'insertWoFieldList', execParams: {caption: 'inserted2'}},
{entity: "tst_aclrls", method: 'update', fieldList: ['ID', 'caption'], execParams: {ID: 332463213805569, caption: 'updated1'}},
{entity: "tst_aclrls", method: 'delete', execParams: {ID: 332463213805572}}]
).then(UB.logDebug)
// result is:
[{
"entity": "tst_aclrls","method": "insert","fieldList": ["ID","caption"],"execParams": {"caption": "inserted1","ID": 332463256010753},
"resultData": {"ID": 332463256010753,"caption": "inserted1"}
}, {
"entity": "tst_aclrls","method": "insert","opaqueParam": "insertWoFieldList","execParams": {"caption": "inserted2","ID": 332463256010756},"fieldList": []
}, {
"entity": "tst_aclrls","method": "update","fieldList": ["ID","caption"],"execParams": {"ID": 332463213805569,"caption": "updated1"},
"resultData": {"ID": 332463213805569,"caption": "updated1"}
}, {
"entity": "tst_aclrls","method": "delete","execParams": {"ID": 332463213805572},
"ID": 332463213805572
}]
* @function
* @param {Array.<ubRequest>} ubRequestArray
* @param {Array.<Object<string, string>>} [fieldAliasesArray] Optional array of object to change attribute names during transform.
* Keys are original names, values - new names
* @returns {Promise<Array<object>>}
*/
UBConnection.prototype.runTransAsObject = function (ubRequestArray, fieldAliasesArray = []) {
for (const serverRequest of ubRequestArray) {
if (serverRequest.execParams && ((serverRequest.method === 'insert') || ((serverRequest.method === 'update')))) {
const newEp = stringifyExecParamsValues(serverRequest.execParams)
if (newEp) serverRequest.execParams = newEp
}
}
const me = this
return this.post('ubql', ubRequestArray).then((response) => {
const mutatedEntitiesNames = []
const respArr = response.data
respArr.forEach((resp, idx) => {
const isInsUpd = ((resp.method === 'insert') || (resp.method === 'update'))
if (resp.entity && (isInsUpd || (resp.method === 'delete'))) {
if (!mutatedEntitiesNames.includes(resp.entity)) {
mutatedEntitiesNames.push(resp.entity)
}
}
if (resp.resultData && resp.resultData.data && resp.resultData.data && resp.resultData.fields) {
me.convertResponseDataToJsTypes(resp) // mutate resp
const asObjectArr = LocalDataStore.selectResultToArrayOfObjects(resp, fieldAliasesArray[idx])
resp.resultData = isInsUpd ? asObjectArr[0] : asObjectArr
}
})
if (!mutatedEntitiesNames.length) {
// cache invalidation is not required
return respArr
}
const invalidations = mutatedEntitiesNames.map(eName => {
return me.invalidateCache({ entity: eName })
})
// await for cache invalidation
return Promise.all(invalidations).then(() => {
return respArr
})
})
}
const ALLOWED_GET_DOCUMENT_PARAMS = ['entity', 'attribute', 'ID', 'id', 'isDirty', 'forceMime', 'fileName', 'store', 'revision']
/**
* Get a http link to the "Document" attribute content which is valid for the duration of the user session.
*
* This link can be used, for example, in <img src=...> HTML tag and so on.
*
* Used in `$App.downloadDocument` method to download a BLOB content
* and in `FileRenderer` Vue component to display a BLOB content in browser.
*
* @example
//Retrieve content of document as string using GET
const docURL = await UB.connection.getDocumentURL({
entity:'ubm_form',
attribute: 'formDef',
ID: 100000232003,
revision: 22,
})
// result is alike "/getDocument?entity=ubm_form&attribute=formDef&ID=100000232003&revision=22&session_signature=cbe83ece60126ee4a20d40c2"
* @function
* @param {object} params
* @param {string} params.entity Code of entity to retrieve from
* @param {string} params.attribute `document` type attribute code
* @param {number} params.ID Instance ID
* @param {number} [params.revision] Revision of the document. We strongly recommend to pass this argument for correct HTTP cache work
* @param {boolean} [params.isDirty] Set it to `true` to retrieve a document in **dirty** state
* @param {string} [params.fileName] For dirty document should be passed - getDocument endpoint uses this file
* extension to create a correct Content-Type header.
* If not passed - dirty document returned with Content-Type: application/octet-stream.
*
* @returns {Promise<string>} Document URL (valid for the duration of the user session)
*/
UBConnection.prototype.getDocumentURL = async function (params) {
const urlParams = []
for (const p in params) {
if ((ALLOWED_GET_DOCUMENT_PARAMS.indexOf(p) !== -1) && (typeof params[p] !== 'undefined')) {
urlParams.push(encodeURIComponent(p) + '=' + encodeURIComponent(params[p]))
}
}
const session = await this.authorize()
urlParams.push('session_signature=' + session.signature())
return '/getDocument?' + urlParams.join('&')
}
/**
* Retrieve content of `document` type attribute field from server. Usage samples:
*
* @example
//Retrieve content of document as string using GET
$App.connection.getDocument({
entity:'ubm_form',
attribute: 'formDef',
ID: 100000232003
}).then(function(result){console.log(typeof result)}); // string
//The same, but using POST for bypass cache
$App.connection.getDocument({
entity:'ubm_form',
attribute: 'formDef',
ID: 100000232003
}, {
bypassCache: true
}).then(function(result){console.log(typeof result)}); // string
//Retrieve content of document as ArrayBuffer and bypass cache
$App.connection.getDocument({
entity:'ubm_form',
attribute: 'formDef',
ID: 100000232003
}, {
bypassCache: true, resultIsBinary: true
}).then(function(result){
console.log('Result is', typeof result, 'of length' , result.byteLength, 'bytes'); //output: Result is ArrayBuffer of length 2741 bytes
let uiArr = new Uint8Array(result) // view into ArrayButter as on the array of byte
console.log('First byte of result is ', uiArr[0])
})
* @function
* @param {object} params
* @param {string} params.entity Code of entity to retrieve from
* @param {string} params.attribute `document` type attribute code
* @param {number} params.id Instance ID
* @param {string} [params.forceMime] If passed and server support transformation from source MIME type to `forceMime`
* server perform transformation and return document representation in the passed MIME
* @param {number} [params.revision] Optional revision of the document (if supported by server-side store configuration).
* Default is current revision.
* @param {string} [params.fileName] For dirty document should be passed - getDocument endpoint uses this file
* extension to create a correct Content-Type header.
*
* If not passed - dirty document returned with Content-Type: application/octet-stream.
* For non-dirty documents Content-Type retrieved from JSON in DB.
* @param {boolean} [params.isDirty=false] Optional ability to retrieve document in **dirty** state
* @param {string} [params.store] ????
* @param {object} [options] Additional request options
* @param {boolean} [options.resultIsBinary=false] if true - return document content as ArrayBuffer
* @param {boolean} [options.bypassCache] HTTP POST verb will be used instead of GET for bypass browser cache
* @returns {Promise} Resolved to document content (either ArrayBuffer in case options.resultIsBinary===true or text/json)
*/
UBConnection.prototype.getDocument = function (params, options) {
const opt = Object.assign({}, options)
const reqParams = {
url: 'getDocument',
method: opt.bypassCache ? 'POST' : 'GET'
}
if (options && options.resultIsBinary) {
reqParams.responseType = 'arraybuffer'
}
if (opt.bypassCache) {
reqParams.data = Object.assign({}, params)
Object.keys(reqParams.data).forEach(function (key) {
if (ALLOWED_GET_DOCUMENT_PARAMS.indexOf(key) === -1) {
delete reqParams.data[key]
ubUtils.logDebug('invalid parameter "' + key + '" passed to getDocument request')
}
})
} else {
reqParams.params = params
}
return this.xhr(reqParams).then((response) => response.data)
}
/**
* Saves a file content to the TEMP store of the specified entity attribute of Document type.
*
* Should be called before insert of update. Result of this function is what shall be assigned to the
* attribute value during insert/update operation.
*
* @function
* @param {*} content BLOB attribute content
* @param {object} params Additional parameters
* @param {string} params.entity Entity name
* @param {string} params.attribute Entity attribute name
* @param {number} params.id ID of the record
* @param {string} params.origName
* @param {number} [params.chunkSizeMb] Chunks size in Mb. Can be set for each request individually.
* - If not defined - uiSettings.adminUI.uploadChunkSizeMb is used
* - if === 0 - chunked upload will be disabled
* @param {string} [params.fileName] If not specified, `params.origName` will be used
* @param {string} [params.encoding] Encoding of `data`. Either omit for binary data
* or set to `base64` for base64 encoded data
* @param {Function} [onProgress] Optional onProgress callback
* @returns {Promise<object>} Promise resolved blob store metadata
*/
UBConnection.prototype.setDocument = async function (content, params, onProgress) {
let chunkSize = params.chunkSizeMb === undefined
? this.appConfig.uiSettings?.adminUI?.uploadChunkSizeMb || 0
: params.chunkSizeMb
chunkSize = chunkSize * (1024 * 1024) // Mb -> bytes
const xhrParams = {
url: 'setDocument',
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream'
}
}
// Use chunked upload only if it's enabled and file size > chunk size
if (chunkSize && (content.size > chunkSize)) {
const fileChunk = []
let fileStreamPos = 0
let endPos = chunkSize
while (fileStreamPos < content.size) {
fileChunk.push(content.slice(fileStreamPos, endPos))
fileStreamPos = endPos
endPos = fileStreamPos + chunkSize
}
xhrParams.params = {
...params,
chunkSize,
chunksTotal: fileChunk.length
}
for (let i = 0; i < fileChunk.length; i++) {
xhrParams.data = new File([fileChunk[i]], String(i))
xhrParams.params.chunkNum = i
if (onProgress) xhrParams.onProgress = onProgress
const res = await this.xhr(xhrParams)
if (res.data.success && res.data.result) return res.data.result
}
throw new ubUtils.UBError('File upload error')
} else {
xhrParams.params = params
if (onProgress) xhrParams.onProgress = onProgress
xhrParams.data = content
return this.xhr(xhrParams).then(serverResponse => serverResponse.data.result)
}
}
/**
* Alias to {@link UBSession.hexa8 UBSession.hexa8}
*
* @private
* @deprecated since 1.8 use UBSession.prototype.hexa8 instead
*/
UBConnection.prototype.hexa8 = UBSession.prototype.hexa8
/**
* Alias to {@link UBSession.hexa8 UBSession.crc32}
*
* @private
* @deprecated since 1.8 use UBSession.prototype.crc32 instead
*/
UBConnection.prototype.crc32 = UBSession.prototype.crc32
/**
* Log out user from server
*
* @param {object} [reasonParams={}]
*/
UBConnection.prototype.logout = function (reasonParams = {}) {
if (this.allowSessionPersistent) LDS.removeItem(this.__sessionPersistKey)
if (!this.isAuthorized()) return Promise.resolve(true)
let logoutPromise = this.post('logout', {}, { params: reasonParams })
if (this._pki) { // unload encryption private key
const me = this
logoutPromise = logoutPromise.then(
() => new Promise((resolve) => { setTimeout(() => { me._pki.closePrivateKey(); resolve(true) }, 20) })
)
}
return logoutPromise
}
/**
* @class SignatureValidationResultAction
* @property {string} icon Icon css class name
* @property {string} tooltip Tooltip caption
* @property {Function} callback Function for handle click
*/
/**
* PKI interface
*
* @interface
*/
function UbPkiInterface () {}
/**
* Name of used library
*
* @type {string}
*/
UbPkiInterface.prototype.libName = ''
/**
* Direct library interface
*
* @type {{}}
*/
UbPkiInterface.prototype.direct = {}
/**
* Returns true in case loaded private key certificate marked as 'Digital seal'
* @returns {Promise<boolean>}
*/
UbPkiInterface.prototype.isLoadedKeyDigitalStamp = async function () {}
/**
* Set requirement for next loaded private key to be a 'Digital seal' (true) or not (false).
* `null` will disable verification at all, si either signature or seal can be loaded
*
* @param {boolean|null} aDigitalStamp
* @returns {Promise<boolean|null>} promise what resolves to aDigitalStamp value
*/
UbPkiInterface.prototype.setRequireDigitalStamp = async function (aDigitalStamp) {}
/**
* Read private key
*/
UbPkiInterface.prototype.readPrivateKey = function () {}
/**
* Close private key. If clearCache is `true` and `setAllowPrivateKeyCache(true)` is called before - clear key cache to
* force user to enter key credentials on next load
*
* @param {boolean} [clearCache]
**/
UbPkiInterface.prototype.closePrivateKey = function (clearCache) {}
/**
* Return parsed certificate for loaded private key
*/
UbPkiInterface.prototype.getPrivateKeyOwnerInfo = function () {}
/**
* Sing one or several documents.
*
* Can accept {BlobStoreRequest} as item - in this case signature hash is calculated on server side
* for document stored in BLOB store (@ub-d/crypto-api model must be added into domain)
*
* @param {Uint8Array|ArrayBuffer|string|BlobStoreRequest|Array<Uint8Array|ArrayBuffer|string|BlobStoreRequest>} data
* @param {boolean} [resultIsBinary=false]
* @param {Function} [ownerKeyValidationFunction] optional function what called with one parameter - certInfo: CertificateJson before signing.
* Should validate is owner of passed certificate allowed to perform signing,
* for example by check equality of certInfo.serial with conn.userData('userCertificateSerial');
* In case function returns rejected promise or throw then private key will be unloaded from memory
* to allow user to select another key
* @returns {Promise<ArrayBuffer|string|Array<ArrayBuffer|string>>} signature or array of signatures if data is array.
* If resultIsBinary===true each signature is returned as ArrayBuffer, otherwise as base64 encoded string
*/
UbPkiInterface.prototype.sign = function (data, resultIsBinary, ownerKeyValidationFunction) {}
/**
* Verify signature(s) for data. If signature is string function await
* this is a base64 encoded binary signature
*
* Data can be BlobStoreRequest - in this case verification is done on the server (@ub-d/crypto-api model must be added to domain)
*
* @param {File|ArrayBuffer|Blob|Array|String|BlobStoreRequest|Array<File|ArrayBuffer|Blob|Array|String|BlobStoreRequest>} signatures
* @param {Uint8Array|String|BlobStoreRequest} data
* @param {boolean} [verifyTimestamp=true]
* @returns {Promise<SignatureValidationResult|Array<SignatureValidationResult>>}
*/
UbPkiInterface.prototype.verify = function (signatures, data, verifyTimestamp) {}
/**
* CERT2 auth implementation
*
* @param {object} authParams
*/
UbPkiInterface.prototype.authHandshakeCERT2 = function (authParams) {}
/**
* Show UI for library settings
*
* @return {Promise<boolean>}
*/
UbPkiInterface.prototype.settingsUI = function () {}
/**
* Show UI for signature verification result
*
* @param {Array<SignatureValidationResult>} validationResults Array of UbPkiInterface.verify() results
* @param {Array<string>} [sigCaptions] Array of type and name for each signature from validationResults
* @param {Array<SignatureValidationResultAction>} [actions] Array of action button (icon and callback)
* @return {Promise<boolean>}
*/
UbPkiInterface.prototype.verificationUI = function (validationResults, sigCaptions, actions) {}
/**
* Enable\disable in-memory private keys cache. If enabled, will cache parameters for Signature and Stamp,
* so switching between Signature-Stamp using `setRequireDigitalStamp(true|false)` will ask for password
* during first key loading only
*
* @param {boolean} isAllow
*/
UbPkiInterface.prototype.setAllowPrivateKeyCache = function (isAllow) {}
/**
* Inject encryption implementation and return a promise to object what implements a UbPkiInterface
*
* @return {Promise<UbPkiInterface>}
*/
UBConnection.prototype.pki = async function () {
if (this._pki) return this._pki
if (!this.appConfig.uiSettings) throw new Error('connection.pki() can be called either after connect() or inside connection.onGotApplicationConfig')
const availableEncryptions = this.appConfig.availableEncryptions
let pkiImplModule
if (availableEncryptions) {
if (availableEncryptions.length === 1) { // single encryption implementation - select it
pkiImplModule = availableEncryptions[0].moduleURI
} else {
if (window && (typeof window.capiSelectionDialog === 'function')) {
pkiImplModule = await window.capiSelectionDialog(this)
} else { // no encryption selection function defined in $App - choose first encryption
pkiImplModule = availableEncryptions[0].moduleURI
}
}
}
if (!pkiImplModule) {
throw new Error('"encryptionImplementation" not defined in "appConfig.uiSettings.adminUI" or "@ub-d/crypto-api" model is not added into domain')
}
// use global UB to prevent circular dependency
// eslint-disable-next-line no-undef
await UB.inject(pkiImplModule)
// UA_CRYPT is injected on demand
// eslint-disable-next-line no-undef
this._pki = await UA_CRYPT.getPkiInterface(this)
return this._pki
}
/**
* Known server-side error codes
*
* @enum
* @private
*/
UBConnection.prototype.serverErrorCodes = ubUtils.SERVER_ERROR_CODES
/**
* Return server-side error message by error number
* @param {number} errorNum
* @return {string}
*/
UBConnection.prototype.serverErrorByCode = function (errorNum) {
return this.serverErrorCodes[errorNum]
}
/**
* Create a new instance of repository
*
* @param {String|Object} entityCodeOrUBQL The name of the Entity for which the Repository is being created or UBQL
* @returns {ClientRepository}
*/
UBConnection.prototype.Repository = function (entityCodeOrUBQL) {
if (typeof entityCodeOrUBQL === 'string') {
return new ClientRepository(this, entityCodeOrUBQL)
} else {
return new ClientRepository(this, '').fromUbql(entityCodeOrUBQL)
}
}
/**
* Calc SHA256 from string
*
* var shaAsSting = UB.connection.SHA256('something').toString()
*/
UBConnection.prototype.SHA256 = SHA256
/**
* Calc HMAC_SHA256 from key and string
*
* var shaAsSting = UB.connection.HMAC_SHA256('secretKey', 'something').toString()
*/
UBConnection.prototype.HMAC_SHA256 = HMAC_SHA256
/**
* Sets UI tag for connection.
*
* This tag will be added to a ubql HTTP request as `uitag=${uiTag}` and can be used to track from which part of UI request is generated
*
* Recommended naming convention for tags are:
* - nsc-${shortcutCode} for something executed from navshortcut
* - frm-${formCode} for forms
* - afm-${entity} for auto-forms
* - rpt-${reportCode} for reports
*
* @param {string} uiTag
*/
UBConnection.prototype.setUiTag = function (uiTag) {
this.uiTag = encodeURIComponent(uiTag || '')
}
/**
* Fires after successful response for update/insert/delete for entity received
* @example
const entityChangedHandler = function ({entity, method, resultData}) {
console.log(`Someone call ${method} for ${entity} with ID ${resultData.ID}`)
}
// inside initialization of some form = subscribe to changes
UB.connection.on('uba_user:changed', entityChangedHandler)
// *IMPORTANT* inside form destroy - unsubscribe
UB.connection.removeListener('uba_user:changed', entityChangedHandler)
* @event entity_name:changed
* @memberOf module:@unitybase/ub-pub.module:AsyncConnection~UBConnection
* @param {object} ubqlResponse
*/
/**
* Emit `${entityCode}:changed` event. In case entity has a unity mixin - emit also for unityEntity
*
* @param {string} entityCode
* @param {object} payload An object with at last {entity: 'entityCode', method: 'entityMethod', resultData: {} } attributes
*/
UBConnection.prototype.emitEntityChanged = function (entityCode, payload) {
const e = this.domain.get(entityCode, false)
this.emit(`${entityCode}:changed`, payload)
if (e && e.hasMixin('unity') && e.mixins.unity.entity) {
const U = e.mixins.unity
// transform response to match a unity
const unityPayload = {
entity: U.entity,
method: payload.method,
resultData: U.defaults || {}
}
const uAttrsSet = new Set(U.attributeList)
const uMapping = U.mapping || {}
const RD = payload.resultData || {}
for (const attr in RD) {
if (attr === 'ID') {
unityPayload.resultData.ID = RD.ID
} else if (uAttrsSet.has(attr)) {
unityPayload.resultData[attr] = RD[attr]
} else if (uMapping[attr]) {
unityPayload.resultData[uMapping[attr]] = RD[attr]
}
}
this.emit(`${e.mixins.unity.entity}:changed`, unityPayload)
}
}
/**
* Is auth schema for logged-in user allows password changing (currently - only UB and CERT* with requireUserName)
*
* @returns {boolean}
*/
UBConnection.prototype.userCanChangePassword = function () {
if (!LDS) return false
const lastAuthType = LDS.getItem(ubUtils.LDS_KEYS.LAST_AUTH_SCHEMA) || '' // session.authSchema
const auis = (this.appConfig.uiSettings && this.appConfig.uiSettings.adminUI) || {}
return (lastAuthType === 'UB' || lastAuthType === 'Basic') ||
(lastAuthType.startsWith('CERT') && auis.authenticationCert && auis.authenticationCert.requireUserName)
}
/**
* Return preferred uData (stored in localStorage for specified user).
* Preferred uData passed as `prefUData` URL param during `/auth` handshake and can be used in server-side Session.on('login') handler
*
* @param {string} [userName] if not passed - localStorage.LAST_LOGIN is used (if sets)
* @returns {object|undefined}
*/
UBConnection.prototype.getPreferredUData = function (userName) {
if (!LDS) return
if (!userName) userName = LDS.getItem(ubUtils.LDS_KEYS.LAST_LOGIN)
if (!userName) return
return LDS.getItem(ubUtils.LDS_KEYS.PREFFERED_UDATA_PREFIX + userName)
}
/**
* Sets currently logged-in user preferred uData (stored in localStorage for specified user).
* Preferred uData passed as `prefUData` URL param during `/auth` handshake and can be used in server-side Session.on('login') handler
*
* @param {object|null} preferredUData If `null` - preferred user data will be deleted
*/
UBConnection.prototype.setPreferredUData = function (preferredUData) {
if (!this.isAuthorized || !LDS) return
const key = ubUtils.LDS_KEYS.PREFFERED_UDATA_PREFIX + this.userLogin()
if (!preferredUData) {
LDS.removeItem(key)
} else {
LDS.setItem(key, JSON.stringify(preferredUData))
}
}
/**
* see docs in ub-pub main module
*
* @private
* @param {object} cfg
* @param {string} cfg.host Server host
* @param {string} [cfg.path='/'] API path - the same as in Server config `httpServer.path`
* @param {authParamsCallback} cfg.onCredentialRequired Callback for requesting a user credentials.
* See {@link authParamsCallback} description for details
* @param {request2faCallback} [cfg.onRequest2fa] Callback for requesting second factor (if needed).
* See {@link request2faCallback} description for details
* @param {boolean} [cfg.allowSessionPersistent=false] For a non-SPA browser client allow to persist a Session in the local storage between reloading of pages.
* In case user is logged out by server this persistent don't work and UBConnection will call onCredentialRequired handler,
* so user will be prompted for credentials
* @param {Function} [cfg.onAuthorizationFail] Callback for authorization failure. See {@link event:authorizationFail} event.
* @param {Function} [cfg.onAuthorized] Callback for authorization success. See {@link event:authorized} event.
* @param {Function} [cfg.onNeedChangePassword] Callback for a password expiration. See {@link event:passwordExpired} event
* @param {Function} [cfg.onGotApplicationConfig] Called just after application configuration retrieved from server.
* Accept one parameter - `connection: UBConnection`
* Usually on this stage application inject some scripts required for authentication (locales, cryptography etc).
* Should return a promise then done
* @param {Function} [cfg.onGotApplicationDomain]
* @param {object} [cfg.defHeaders] XHR request headers, what will be added to each xhr request for this connection
(after adding headers from UB.xhr.defaults). Object keys is header names. Example: `{"X-Tenant-ID": "12"}`
* @param {object} [ubGlobal=null]
* @returns {Promise<UBConnection>}
*/
function connect (cfg, ubGlobal = null) {
const config = this.config = Object.assign({}, cfg)
const connection = new UBConnection({
host: config.host,
appName: config.path || '/',
requestAuthParams: config.onCredentialRequired,
request2fa: config.onRequest2fa,
allowSessionPersistent: cfg.allowSessionPersistent,
defHeaders: config.defHeaders
})
// inject connection instance to global UB just after connection creation
if (ubGlobal) ubGlobal.connection = connection
if (config.onAuthorizationFail) {
connection.on('authorizationFail', config.onAuthorizationFail)
}
if (config.onNeedChangePassword) {
connection.on('passwordExpired', config.onNeedChangePassword)
}
if (config.onAuthorized) {
connection.on('authorized', config.onAuthorized)
}
return connection.getAppInfo().then(function (appInfo) {
// apply a default app settings to the gerAppInfo result
connection.appConfig = Object.assign({
applicationName: 'UnityBase',
applicationTitle: 'UnityBase',
loginWindowTopLogoURL: '',
loginWindowBottomLogoURL: '',
themeName: 'UBGrayTheme',
userDbVersion: null,
defaultLang: 'en',
supportedLanguages: ['en'],
uiSettings: {}
}, appInfo)
// create ubNotifier after retrieve appInfo (we need to know supported WS protocols)
connection.ubNotifier = new UBNotifierWSProtocol(connection)
// try to determinate default user language
let preferredLocale = null
if (LDS) {
preferredLocale = LDS.getItem(ubUtils.LDS_KEYS.PREFERRED_LOCALE)
}
if (!preferredLocale) {
preferredLocale = connection.appConfig.defaultLang
}
// is language supported by application?
if (connection.appConfig.supportedLanguages.indexOf(preferredLocale) === -1) {
preferredLocale = connection.appConfig.defaultLang
}
connection.preferredLocale = preferredLocale
// localize application mae
const adminUICfg = connection.appConfig.uiSettings.adminUI
if (adminUICfg.applicationName) {
const appName = (typeof adminUICfg.applicationName === 'string')
? adminUICfg.applicationName
: adminUICfg.applicationName[connection.preferredLocale]
if (appName) connection.appConfig.applicationName = appName
}
return config.onGotApplicationConfig ? config.onGotApplicationConfig(connection) : true
}).then(function () {
return connection.initCache(connection.appConfig.userDbVersion)
}).then(function () {
return connection.authorize()
}).then(function () {
// here we authorized and know a user-related data
const myLocale = connection.userData('lang')
LDS && LDS.setItem(ubUtils.LDS_KEYS.PREFERRED_LOCALE, myLocale)
connection.preferredLocale = myLocale
let domainPromise = connection.getDomainInfo()
if (config.onGotApplicationDomain) {
domainPromise = domainPromise.then((domain) => {
config.onGotApplicationDomain(domain)
return domain
})
}
return domainPromise
}).then(function (domain) {
connection.domain = domain
return connection
})
}
/**
* Helper function for building `rq` parameter value for ubql endpoint.
* Takes into account the same method calls sequences. Limit URI length
*
* @private
* @param {Array<ubRequest>} reqData
*/
function buildUriQueryPath (reqData) {
const L = reqData.length
if (!L) return ''
if (L === 1) return `${reqData[0].entity}.${reqData[0].method}`
if (L) {
const methodsArr = []
methodsArr.push(`${reqData[0].entity}.${reqData[0].method}`)
let i = 1
while (i < L) {
// repeatable methods
if ((reqData[i].entity === reqData[i - 1].entity) &&
(reqData[i].method === reqData[i - 1].method)) {
let repeats = 1
do {
repeats++
i++
} while ((i < L) && (reqData[i].entity === reqData[i - 1].entity) && (reqData[i].method === reqData[i - 1].method))
methodsArr.push(repeats)
}
if (i < L) {
methodsArr.push(`${reqData[i].entity}.${reqData[i].method}`)
if (methodsArr.length > 20) { // in any case limit a URI length
methodsArr.push('**')
break
}
i++
}
}
return methodsArr.join('*')
}
}
module.exports.UBConnection = UBConnection
module.exports.connect = connect