const EventEmitter = require('events').EventEmitter
const { UBDomain, formatByPattern } = require('@unitybase/cs-shared')
const ServerRepository = require('@unitybase/base').ServerRepository
const repositoryFabric = ServerRepository.fabric // for backward compatibility with UB 1.7
const App = require('./modules/App')
const Session = require('./modules/Session')
const format = require('@unitybase/base').format
const blobStores = require('@unitybase/blob-stores')
const TubDataStore = require('./TubDataStore')
const Errors = require('./modules/ubErrors')
const ws = require('./modules/web-sockets')
const mI18n = require('./modules/i18n')
const modelLoader = require('./modules/modelLoader')
const mixinsFactory = require('./modules/mixinsFactory')
const _ = require('lodash')
const LANGS_SET = new Set(App.serverConfig.application.domain.supportedLanguages)
const FORMAT_RE = /{([0-9a-zA-Z_]+)}/g
// if (typeof global['UB'] !== 'undefined') throw new Error('@unitybase/ub already required')
/**
* @module @unitybase/ub
*/
const UB = module.exports = {
/**
* If we are in UnityBase server scripting (both -f or server thread) this property is true, if in browser - undefined or false.
* Use it for check execution context in scripts, shared between client & server.
* To check we are in server thread use process.isServer
*
* @readonly
*/
isServer: true,
/**
* Server-side Abort exception. To be used in server-side logic in case of HANDLED
* exception. These errors logged using "Error" log level to prevent unnecessary
* EXC log entries
*
* @example
// UB client will show message inside <<<>>> to user (and translate it using UB.i18n)
const UB = require('@unitybase/ub')
throw new UB.UBAbort('<<<textToDisplayForClient>>>')
// In case, client-side message shall be formatted:
throw new UB.UBAbort('<<<file_not_found>>>', 'bad_file.name')
// The "file_not_found" i18n string on client should be like `'File "{0}" is not found or not accessible'
// Format args can be translated by assing a :i18n modifier to template string: `'File "{0:i18n}" is not found or not accessible'
// In case message should not be shown to the end used by ub-pub globalExceptionHandler `<<<>>>` can be omitted
throw new UB.UBAbort('wrongParameters')
* @function UBAbort
* @param {string} [message] Error message
* @param {...any} args
* @augments {Error}
*/
UBAbort: Errors.UBAbort,
/**
* Server-side Security exception. Throwing of such exception will trigger
* `Session.securityViolation` event
*
* @property {ESecurityException} ESecurityException
*/
ESecurityException: Errors.ESecurityException,
/**
* Creates namespaces to be used for scoping variables and classes so that they are not global.
*
* @example
UB.ns('DOC.Report');
DOC.Report.myReport = function() { ... };
* @deprecated Try to avoid namespaces - instead create a module and use require()
* @param {string} namespacePath
* @returns {object} The namespace object.
*/
ns,
format,
/**
* Create new instance of {@link ServerRepository}
*
* @param {string} entityName
* @param {object} [cfg]
* @param {SyncConnection} [connection] Pass in case of remote UnityBase server connection.
* @returns {ServerRepository}
*/
Repository: function (entityName, cfg, connection) {
connection = connection || global.conn
return repositoryFabric(entityName, connection)
},
getWSNotifier: ws.getWSNotifier,
/**
* Information about the logged-in user
*
* @property {Session} Session
*/
Session,
/**
* Construct new data store
*
* @param {string} entityCode
* @returns {TubDataStore}
*/
DataStore: function (entityCode) {
return new TubDataStore(entityCode)
},
/**
* Return locale-specific resource from it identifier.
* Resources are defined by {@link module:@unitybase/ub#i18nExtend UB.i18nExtend}
*
* In case firs element of args is a string with locale code supported by application then translate to specified locale,
* in other case - to locale of the current user (user who done the request to the server)
*
* Localized string can be optionally formatted by position args
*
* @example
UB.i18nExtend({
en: { greeting: 'Hello {0}, welcome to {1}' },
uk: { greeting: 'Привіт {0}, ласкаво просимо до {1}' }
})
UB.i18n('greeting', 'Mark', 'Kiev') // in case current user language is en -> "Hello Mark, welcome to Kiev"
UB.i18n('greeting', 'uk', 'Mark', 'Kiev') // in case ru lang is supported -> "Привіт Mark, ласкаво просимо до Kiev"
* @param {string} msg Message to translate
* @param {...*} args Format args
* @returns {*}
*/
i18n: function i18n (msg, ...args) {
let lang = args[0]
if (lang && LANGS_SET.has(lang)) {
args.shift() // first element is language
} else {
lang = Session.userLang || App.defaultLang
}
const res = mI18n.lookup(lang, msg)
if (args && args.length && (typeof res === 'string')) {
// key-value object
if ((args.length === 1) && (typeof args[0] === 'object')) {
const first = args[0]
return res.replace(FORMAT_RE, function (m, k) {
return _.get(first, k)
})
} else { // array of values
return res.replace(FORMAT_RE, function (m, i) {
return args[i]
})
}
} else {
return res
}
},
/**
* Merge localizationObject to UB.i18n
*
* @example
const UB = require('@unitybase/ub')
UB.i18nExtend({
"en": {yourMessage: "yourTranslationToEng", ...},
"uk": {yourMessage: "yourTranslationToUk", ...},
....
})
// if logged-in user language is `en` will output "yourTranslationToEng"
console.log(UB.i18n(yourMessage))
// will output "yourTranslationToUk"
console.log(UB.i18n(yourMessage, 'uk'))
* @param {object} localizationObject
*/
i18nExtend: mI18n.extend,
/**
* For UB < 4.2 compatibility - require all non - entity js
* in specified folder add its sub-folder (one level depth),
* exclude `modules` and `node_modules` sub-folder's.
* From 'public' sub-folder only cs*.js are required.
* To be called in model index.js as such:
*
* const UB = require('@unitybase/ub')
* UB.loadLegacyModules(__dirname)
*
* For new models we recommend to require non-entity modules manually
*
* @param {string} folderPath
* @param {boolean} isFromPublicFolder
* @param {number} depth Current recursion depth
*/
loadLegacyModules: modelLoader.loadLegacyModules,
/**
* Application instance
*
* @property {ServerApp} App
*/
App,
start,
/**
* A way to add additional mixins into domain
*
* @param {string} mixinName A name used as "mixins" section key inside entity *.meta file
* @param {MixinModule} mixinModule A module what implements a MixinModule interface
*/
registerMixinModule: mixinsFactory.registerMixinModule
}
/**
* Initialize UnityBase application:
* - create namespaces (global objects) for all `*.meta` files from domain
* - require all packages specified in config `application.domain.models`
* - apply a server-side i18n JSONs from models serverLocale folder
* - emit {@link event:domainIsLoaded App.domainIsLoaded} event
* - initialize BLOB stores
* - emit {@link event:applicationReady App.applicationReady} event
* - register build-in UnityBase {@link module:@unitybase/ub.module:endpoints endpoints}
*/
function start () {
normalizeEnums()
initializeDomain()
/**
* @deprecated Use `const UB = require('@unitybase/ub')`
*/
global.UB = UB
/**
* @deprecated Use `const App = require('@unitybase/ub').App`
*/
global.App = App
// for each model:
// - load all entities modules
// - require a model itself
const orderedModels = App.domainInfo.orderedModels
orderedModels.forEach((model) => {
if (model.realPath && (model.name !== 'UB')) { // UB already loaded by UB.js
modelLoader.loadEntitiesModules(model.realPath)
modelLoader.loadServerLocale(model.realPath)
require(model.realPath)
}
})
mixinsFactory.initializeMixins()
App.emit('domainIsLoaded')
blobStores.initBLOBStores(App, Session)
// ENDPOINTS
const { clientRequireEp, modelsEp, getAppInfoEp, getDomainInfoEp, staticEp, runSQLEp, restEp, allLocalesEp } = require('./modules/endpoints')
App.registerEndpoint('getAppInfo', getAppInfoEp, false)
App.registerEndpoint('models', modelsEp, false)
App.registerEndpoint('clientRequire', clientRequireEp, false)
App.registerEndpoint('getDomainInfo', getDomainInfoEp, true)
App.registerEndpoint('getDocument', blobStores.getDocumentEndpoint, true)
App.registerEndpoint('setDocument', blobStores.setDocumentEndpoint, true)
App.registerEndpoint('checkDocument', blobStores.checkDocumentEndpoint, true)
App.registerEndpoint('runSQL', runSQLEp, true)
App.registerEndpoint('rest', restEp, true)
App.registerEndpoint('allLocales', allLocalesEp, false)
App.registerEndpoint('statics', staticEp, false, true)
// some methods can be added in model initialization - reset domain cache here
App._resetDomainCache()
App.emit('applicationReady')
}
// normalize ENUMS TubCacheType = {Entity: 1, SessionEntity: 2} => {Entity: 'Entity', SessionEntity: 'SessionEntity'}
/**
*
*/
function normalizeEnums () {
const enums = ['TubftsScope', 'TubCacheType', 'TubEntityDataSourceType',
'TubAttrDataType', 'TubSQLExpressionType', 'TubSQLDialect', 'TubSQLDriver']
enums.forEach(eN => {
const e = global[eN]
if (!e) return
const vals = Object.keys(e)
vals.forEach(k => { e[k] = k })
})
}
// domain initialization
/**
*
*/
function initializeDomain () {
// eslint-disable-next-line n/no-deprecated-api
const { addEntityMethod, getDomainInfo } = process.binding('ub_app')
// create scope for all domain objects
const tempDomain = new UBDomain(getDomainInfo(true))
tempDomain.eachEntity(entity => {
const e = global[entity.code] = { }
Object.defineProperty(e, 'entity', {
writable: false,
enumerable: true,
value: entity
})
EventEmitter.call(e, entity.code)
Object.assign(e, EventEmitter.prototype)
e.entity.addMethod = (function (entityCode) {
return function (methodCode) {
addEntityMethod(entityCode, methodCode)
}
})(entity.code)
})
// TODO - validate domain
// 1) if (FAttr.dataType = adtDocument) and (FAttr.storeName <> '') and
// ubLog.Add.Log(sllError, 'DomainLoad: Error loading entity "%". A storeName="%" specified in attribute "%" not in defined in application.blobStore config)',
// 2) fLog.Log(sllInfo, 'Check blob store "%" folder "%": folder exists', [fAppConfig.blobStores[i].name, fAppConfig.blobStores[i].path]);
}
/**
* @param namespacePath
* @return {object}
*/
function ns (namespacePath) {
let root = global
const parts = namespacePath.split('.')
for (let j = 0, subLn = parts.length; j < subLn; j++) {
const part = parts[j]
if (!root[part]) {
root[part] = {}
}
root = root[part]
}
return root
}
/**
* @type Session
* @deprecated Use `const Session = require('@unitybase/ub').Session`
*/
Object.defineProperty(global, 'Session', {
value: Session
})
// legacy 1.12
require('./modules/RLS')
// register mixin
const multitenancyImpl = require('./mixins/multitenancyMixin')
UB.registerMixinModule('multitenancy', multitenancyImpl)
const fsStorageImpl = require('./mixins/fsStorageMixin')
UB.registerMixinModule('fsStorage', fsStorageImpl)
const aclRlsStorageMixinImpl = require('./mixins/aclRlsStorageMixin')
UB.registerMixinModule('aclRlsStorage', aclRlsStorageMixinImpl)
const aclRlsMixinImpl = require('./mixins/aclRlsMixin')
UB.registerMixinModule('aclRls', aclRlsMixinImpl)
if (App.serverConfig.security.disabledAccounts) {
const reDisabledAccounts = new RegExp(App.serverConfig.security.disabledAccounts)
Session.on('login', function ubCheckAccountIsDisabled () {
if (reDisabledAccounts.test(Session.uData.login)) {
console.error('Login disabled by security.disabledAccounts option')
throw new Errors.ESecurityException('<<<Login disabled>>>')
}
})
}
if (App.serverConfig.application.localization && App.serverConfig.application.localization.irregularLangToLocaleMap) {
formatByPattern.addIrregularLangToLocales(App.serverConfig.application.localization.irregularLangToLocaleMap)
}
formatByPattern.setDefaultLang(App.defaultLang)
module.exports = UB