/**
* UnityBase domain object model (metadata) - in-memory representation of all *.meta files included in the application config.
*
* Developer should never create {@link UBDomain} class directly, but instead use a:
*
* - inside server-side methods - {@link App.domainInfo App.domainInfo} property
* - inside CLI scripts - using {@link UBConnection.getDomainInfo UBConnection.getDomainInfo} method
* - inside a browser - `UBConnection.domain` property
*
* Information about domain is used in many aspects of UnityBase:
*
* - database generation
* - documentation generation
* - forms generation
* - views generation etc.
* - transform UBQL -> SQL
* - etc
*
* @module @unitybase/base/UBDomain
*/
const _ = require('lodash')
/**
* Database connection config (w/o credential)
* @typedef {Object} DBConnectionConfig
* @property {string} name
* @property {string} dialect
* @property {Array<string>} supportLang
* @property {string} advSettings database specific settings
*/
/**
* @classdesc
* UnityBase domain object model.
* Construct new UBDomain instance based on getDomainInfo UB server method result
*
* Usage sample:
*
* // retrieve a localized caption of uba_user.name attribute
* domain.get('uba_user').attr('name').caption
*
* @class
* @param {Object} domainInfo getDomainInfo UB server method result
* @param {Object} domainInfo.domain raw entities collection
* @param {Object} domainInfo.entityMethods entities methods access rights for current user
* @param {Object} domainInfo.models information about domain models
* @param {Object} domainInfo.i18n entities localization to current user language
* @param {Object} domainInfo.forceMIMEConvertors list of registered server-side MIME converters for document type attribute content
*/
function UBDomain (domainInfo) {
let me = this
let entityCodes = Object.keys(domainInfo.domain)
let isV4API = (typeof domainInfo.entityMethods === 'undefined')
/**
* Hash of entities. Keys is entity name, value is UBEntity
* @type {Object<String, UBEntity>}
*/
this.entities = {}
/**
* Connection collection (for extended domain info only).
* @type {Array<DBConnectionConfig>}
*/
this.connections = domainInfo['connections']
entityCodes.forEach(function (entityCode) {
if (isV4API) {
let entity = domainInfo.domain[entityCode]
me.entities[entityCode] = new UBEntity(
entity,
entity.entityMethods || {},
entity.i18n,
entityCode,
me
)
} else {
me.entities[entityCode] = new UBEntity(
domainInfo.domain[entityCode],
domainInfo.entityMethods[entityCode] || {},
domainInfo.i18n[entityCode],
entityCode,
me
)
}
})
/**
* Models collection
* @type {Object<String, UBModel>}
*/
this.models = {}
let modelCodes = Object.keys(domainInfo.models)
modelCodes.forEach(function (modelCode) {
let m = domainInfo.models[modelCode]
me.models[modelCode] = new UBModel(m, modelCode)
})
/**
*
* @type {Object}
* @readonly
*/
this.forceMIMEConvertors = domainInfo.forceMIMEConvertors
}
/**
* Check all provided entity methods are accessible via RLS.
*
* If entity does not exist in domain or at last one of provided methods is not accessible - return false
*
* @param {String} entityCode
* @param {String|Array} methodNames
*/
UBDomain.prototype.isEntityMethodsAccessible = function (entityCode, methodNames) {
let entity = this.entities[entityCode]
if (!entity) return false
return Array.isArray(methodNames) ? entity.haveAccessToMethods(methodNames) : entity.haveAccessToMethod(methodNames)
}
/**
* Get entity by code
* @param {String} entityCode
* @param {Boolean} [raiseErrorIfNotExists=true] If `true`(default) and entity does not exists throw error
* @returns {UBEntity}
*/
UBDomain.prototype.get = function (entityCode, raiseErrorIfNotExists) {
let result = this.entities[entityCode]
if ((raiseErrorIfNotExists !== false) && !result) {
throw new Error('Entity with code "' + entityCode + '" does not exists or not accessible')
}
return result
}
/**
* Check entity present in domain & user has access right for at least one entity method
* @param {String} entityCode
* @returns {Boolean}
*/
UBDomain.prototype.has = function (entityCode) {
return !!this.entities[entityCode]
}
/**
* Iterates over domain entities and invokes `callBack` for each entity.
* The iteratee is invoked with three arguments: (UBEntity, entityName, UBDomain.entities)
* @param {Function} callBack
*/
UBDomain.prototype.eachEntity = function (callBack) {
return _.forEach(this.entities, callBack)
}
/**
* Filter entities by properties
* @example
*
* // sessionCachedEntites contains all entities with property cacheType equal Session
* var sessionCachedEntites = domain.filterEntities({cacheType: 'Session'});
*
* @param {Object|Function} config
* @returns {Array}
*/
UBDomain.prototype.filterEntities = function (config) {
if (_.isFunction(config)) {
return _.filter(this.entities, config)
} else {
return _.filter(this.entities, function (item) {
let res = true
for (let prop in config) {
if (config.hasOwnProperty(prop)) {
res = res && (item[prop] === config[prop])
}
}
return res
})
}
}
/**
* UnityBase base attribute data types
* @readonly
* @enum
*/
UBDomain.ubDataTypes = {
/** Small string. MSSQL: NVARCHAR, ORACLE: NVARCHAR2, POSTGRE: VARCHAR */
String: 'String',
/** 32-bite Integer. MSSQL: INT, ORACLE: INTEGER, POSTGRE: INTEGER */
Int: 'Int',
/** 64-bite Integer. MSSQL: BIGINT, ORACLE: NUMBER(19), POSTGRE: BIGINT */
BigInt: 'BigInt',
/** Double. MSSQL: FLOAT, ORACLE: NUMBER(19, 4), POSTGRE: NUMERIC(19, 4) */
Float: 'Float',
/** Currency. MSSQL: FLOAT, ORACLE: NUMBER(19, 2), POSTGRE: NUMERIC(19, 2) */
Currency: 'Currency',
/** Boolean. MSSQL: TINYINT, ORACLE: NUMBER(1), POSTGRE: SMALLINT */
Boolean: 'Boolean',
/** Date + Time in UTC (GMT+0) timezone. MSSQL: DATETIME, OARCLE: DATE, POSTGRE: TIMESTAMP WITH TIME ZONE */
DateTime: 'DateTime',
/** Long strint. MSSQL: NVARCHAR(MAX), ORACLE: CLOB, POSTGRE: TEXT */
Text: 'Text',
/** Alias for BigInt */
ID: 'ID',
/** Reference to enother entity. BigInt */
Entity: 'Entity',
/** Store a JSON with information about Document place in blob store */
Document: 'Document',
Many: 'Many',
/** Seconds since UNIX epoch, Int64. MSSQL: BIGINT, ORACLE: NUMBER(19), POSTGRE: BIGINT */
TimeLog: 'TimeLog',
/** Enumertion (see ubm_enum) */
Enum: 'Enum',
/** Bynary data. MSSQL: VARBINARY(MAX), ORACLE: BLOB, POSTGRE: BYTEA */
BLOB: 'BLOB',
/** Date (without time) in UTC (GMT+0) */
Date: 'Date'
}
UBDomain.prototype.ubDataTypes = UBDomain.ubDataTypes
/**
* Types of expressions in attribute mapping
* @readonly
* @enum
*/
UBDomain.ExpressionType = {
Field: 'Field',
Expression: 'Expression'
}
/**
* UnityBase base mixins
* @readonly
* @enum
*/
UBDomain.ubMixins = {
dataHistory: 'dataHistory',
mStorage: 'mStorage',
unity: 'unity',
treePath: 'treePath'
}
/**
* Service attribute names
* @readonly
* @enum
*/
UBDomain.ubServiceFields = {
dateFrom: 'mi_datefrom',
dateTo: 'mi_dateto'
}
/**
* Entity dataSource types
* @enum {String}
* @readonly
*/
UBDomain.EntityDataSourceType = {
Normal: 'Normal',
External: 'External',
System: 'System',
Virtual: 'Virtual'
}
/**
* @enum
*/
UBDomain.EntityCacheTypes = {
None: 'None',
Entity: 'Entity',
Session: 'Session',
SessionEntity: 'SessionEntity'
}
/**
* Priority to apply a mapping of a attributes/entities to the physical tables depending of connection dialect
*/
UBDomain.dialectsPriority = {
MSSQL2012: [ 'MSSQL2012', 'MSSQL', 'AnsiSQL' ],
MSSQL2008: [ 'MSSQL2008', 'MSSQL', 'AnsiSQL' ],
MSSQL: [ 'MSSQL', 'AnsiSQL' ],
Oracle11: [ 'Oracle11', 'Oracle', 'AnsiSQL' ],
Oracle10: [ 'Oracle10', 'Oracle', 'AnsiSQL' ],
Oracle9: [ 'Oracle9', 'Oracle', 'AnsiSQL' ],
Oracle: [ 'Oracle', 'AnsiSQL' ],
PostgreSQL: [ 'PostgreSQL', 'AnsiSQL' ],
AnsiSQL: [ 'AnsiSQL' ],
Firebird: [ 'Firebird', 'AnsiSQL' ],
SQLite3: [ 'SQLite3', 'AnsiSQL' ]
}
/**
* Return physical type by UBDataType
* @param {String} dataType
* @return {String}
*/
UBDomain.getPhysicalDataType = function (dataType) {
let ubDataTypes = UBDomain.ubDataTypes
let typeMap = {}
if (!this.physicalTypeMap) {
typeMap[ubDataTypes.Int] = 'int'
typeMap[ubDataTypes.Entity] = 'int'
typeMap[ubDataTypes.ID] = 'int'
typeMap[ubDataTypes.BigInt] = 'int'
typeMap[ubDataTypes.String] = 'string'
typeMap[ubDataTypes.Text] = 'string'
typeMap[ubDataTypes.Enum] = 'string'
typeMap[ubDataTypes.Float] = 'float'
typeMap[ubDataTypes.Currency] = 'float'
typeMap[ubDataTypes.Boolean] = 'boolean'
typeMap[ubDataTypes.Date] = 'date'
typeMap[ubDataTypes.DateTime] = 'date'
this.physicalTypeMap = typeMap
}
return this.physicalTypeMap[dataType] || 'auto'
}
/**
* Model (logical group of entities)
* @class
* @param cfg
* @param cfg.path
* @param cfg.needInit
* @param cfg.needLocalize
* @param cfg.order
* @param {string} [cfg.moduleName]
* @param {string} [cfg.moduleSuffix]
* @param {string} [cfg.clientRequirePath] if passed are used instead of moduleName + moduleSuffix
* @param {string} [cfg.realPublicPath]
* @param {string} modelCode
*/
function UBModel (cfg, modelCode) {
/**
* Model name as specified in application config
* @type {string}
*/
this.name = modelCode
this.path = cfg.path
if (cfg.needInit) {
/**
* `initModel.js` script is available in the public folder (should be injected by client)
* @type {boolean}
*/
this.needInit = cfg.needInit
}
if (cfg.needLocalize) {
/**
* `locale-Lang.js` script is available in the public folder (should be injected by client)
* @type {boolean}
*/
this.needLocalize = cfg.needLocalize
}
/**
* An order of model initialization (as it is provided in server domain config)
* @type {number}
*/
this.order = cfg.order
/**
* Module name for `require`
*/
this.moduleName = cfg.moduleName
// if (cfg.moduleSuffix && cfg.moduleName) {
// this.moduleName = this.moduleName + '/' + cfg.moduleSuffix
// }
/**
* The path for retrieve a model public accessible files (using clientRequire endpoint)
*
* @type {string}
*/
this.clientRequirePath = /* cfg.clientRequirePath
? cfg.clientRequirePath
: */(cfg.moduleSuffix && cfg.moduleName)
? this.moduleName + '/' + cfg.moduleSuffix
: (this.moduleName || this.path)
if (cfg.realPublicPath) {
/**
* Server-side domain only - the full path to model public folder (if any)
* @type {string}
*/
this.realPublicPath = cfg.realPublicPath
}
}
UBModel.prototype.needInit = false
UBModel.prototype.needLocalize = false
UBModel.prototype.realPublicPath = ''
/**
* Collection of attributes
* @class
*/
function UBEntityAttributes () {}
/**
* Return a JSON representation of all entity attributes
* @returns {Object}
*/
UBEntityAttributes.prototype.asJSON = function () {
let result = {}
_.forEach(this, function (prop, propName) {
if (prop.asJSON) {
result[propName] = prop.asJSON()
} else {
result[propName] = prop
}
})
return result
}
/** @class */
function UBEntityMapping (maping) {
/**
* @type {string}
*/
this.selectName = maping.selectName || ''
/** @type {string} */
this.execName = maping.execName || this.selectName
/** @type {string} */
this.pkGenerator = maping.pkGenerator
}
/**
* @class
* @param {Object} entityInfo
* @param {Object} entityMethods
* @param {Object} i18n
* @param {String} entityCode
* @param {UBDomain} domain
*/
function UBEntity (entityInfo, entityMethods, i18n, entityCode, domain) {
let me = this
let mixinNames, mixinInfo, i18nMixin, dialectProiority
if (i18n) {
_.merge(entityInfo, i18n)
}
/**
* Non enumerable (to prevent JSON.stringify circular ref) read only domain
* @property {UBDomain} domain
* @readonly
*/
Object.defineProperty(this, 'domain', {enumerable: false, value: domain})
/**
* @type {String}
* @readonly
*/
this.code = entityCode
/**
* Entity model name
* @type{String}
* @readonly
*/
this.modelName = entityInfo.modelName
/**
* Entity name
* @type {String}
* @readonly
*/
this.name = entityInfo.name
if (entityInfo.caption) this.caption = entityInfo.caption
if (entityInfo.description) this.description = entityInfo.description
if (entityInfo.documentation) this.documentation = entityInfo.documentation
if (entityInfo.descriptionAttribute) this.descriptionAttribute = entityInfo.descriptionAttribute
if (entityInfo.cacheType) this.cacheType = entityInfo.cacheType
if (entityInfo.dsType) this.dsType = entityInfo.dsType
/**
* Internal short alias
* @type {String}
* @readonly
*/
this.sqlAlias = entityInfo.sqlAlias
/**
* Data source connection name
* @type {String}
* @readonly
*/
this.connectionName = entityInfo.connectionName
/**
* This is a Full Text Search entity
* @type {boolean}
*/
this.isFTSDataTable = entityInfo.isFTSDataTable === true
/**
* Reference to connection definition (for extended domain only)
* @type {DBConnectionConfig}
* @readonly
*/
this.connectionConfig = (this.connectionName && this.domain.connections) ? _.find(this.domain.connections, {name: this.connectionName}) : undefined
/**
* Optional mapping of entity to physical data (for extended domain info only).
* Calculated from a entity mapping collection in accordance with application connection configuration
* @type {UBEntityMapping}
* @readonly
*/
this.mapping = undefined
if (entityInfo.mapping && Object.keys(entityInfo.mapping).length) {
dialectProiority = UBDomain.dialectsPriority[this.connectionConfig.dialect]
_.forEach(dialectProiority, function (dialect) {
if (entityInfo.mapping[dialect]) {
me.mapping = new UBEntityMapping(entityInfo.mapping[dialect])
return false
}
})
}
/**
* Optional dbKeys (for extended domain info)
* @type {Object}
*/
this.dbKeys = entityInfo.dbKeys && Object.keys(entityInfo.dbKeys).length ? entityInfo.dbKeys : undefined
/**
* Optional dbExtensions (for extended domain info)
* @type {Object}
*/
this.dbExtensions = entityInfo.dbExtensions && Object.keys(entityInfo.dbExtensions).length ? entityInfo.dbExtensions : undefined
/**
* Entity attributes collection
* @type {Object<string, UBEntityAttribute>}
*/
this.attributes = new UBEntityAttributes()
_.forEach(entityInfo.attributes, function (attributeInfo, attributeCode) {
me.attributes[attributeCode] = new UBEntityAttribute(attributeInfo, attributeCode, me)
})
mixinNames = Object.keys(entityInfo.mixins || {})
/**
* Collection of entity mixins
* @type {Object<String, UBEntityMixin>}
*/
this.mixins = {}
mixinNames.forEach(function (mixinCode) {
mixinInfo = entityInfo.mixins[mixinCode]
i18nMixin = (i18n && i18n.mixins ? i18n.mixins[mixinCode] : null)
switch (mixinCode) {
case 'mStorage':
me.mixins[mixinCode] = new UBEntityStoreMixin(mixinInfo, i18nMixin, mixinCode)
break
case 'dataHistory':
me.mixins[mixinCode] = new UBEntityHistoryMixin(mixinInfo, i18nMixin, mixinCode)
break
case 'aclRls':
me.mixins[mixinCode] = new UBEntityAclRlsMixin(mixinInfo, i18nMixin, mixinCode)
break
case 'fts':
me.mixins[mixinCode] = new UBEntityFtsMixin(mixinInfo, i18nMixin, mixinCode)
break
case 'als':
me.mixins[mixinCode] = new UBEntityAlsMixin(mixinInfo, i18nMixin, mixinCode)
break
default:
me.mixins[mixinCode] = new UBEntityMixin(mixinInfo, i18nMixin, mixinCode)
}
})
/**
* Entity methods, allowed for current logged-in user in format {method1: 1, method2: 1}. 1 mean method is allowed
* @type {Object<String, Number>}
* @readOnly
*/
this.entityMethods = entityMethods || {}
}
/**
* Entity caption
* @type {string}
*/
UBEntity.prototype.caption = ''
/**
* Entity description
* @type {string}
*/
UBEntity.prototype.description = ''
/**
* Documentation
* @type {string}
*/
UBEntity.prototype.documentation = ''
/**
* Name of attribute witch used as a display value in lookup
* @type {string}
*/
UBEntity.prototype.descriptionAttribute = ''
/**
* Indicate how entity content is cached on the client side.
*
* @type {UBDomain.EntityCacheTypes}
* @readonly
*/
UBEntity.prototype.cacheType = 'None'
/**
*
* @type {UBDomain.EntityDataSourceType}
*/
UBEntity.prototype.dsType = 'Normal'
/**
* Return an entity caption to display on UI
* @returns {string}
*/
UBEntity.prototype.getEntityCaption = function () {
return this.caption || this.description
}
/**
* Get entity attribute by code. Return undefined if attribute does not found
* @param {String} attributeCode
* @param {Boolean} [simple] Is do not complex attribute name. By default false.
* @returns {UBEntityAttribute}
*/
UBEntity.prototype.attr = function (attributeCode, simple) {
let res = this.attributes[attributeCode]
if (!res && !simple) {
res = this.getEntityAttribute(attributeCode)
}
return res
}
/**
* Get entity attribute by code. Throw error if attribute does not found.
* @param attributeCode
* @returns {UBEntityAttribute}
*/
UBEntity.prototype.getAttribute = function (attributeCode) {
let attr = this.attributes[attributeCode]
if (!attr) {
throw new Error(`Attribute ${this.code}.${attributeCode} doesn't exist`)
}
return attr
}
/**
* Call callBack function for each attribute.
* @param {Function} callBack
*/
UBEntity.prototype.eachAttribute = function (callBack) {
return _.forEach(this.attributes, callBack)
}
/**
* Get entity mixin by code. Returns "undefined" if the mixin is not found
* @param {String} mixinCode
* @returns {UBEntityMixin}
*/
UBEntity.prototype.mixin = function (mixinCode) {
return this.mixins[mixinCode]
}
/**
* Check the entity has mixin. Returns `true` if the mixin is exist and enabled
* @param {String} mixinCode
* @returns {Boolean}
*/
UBEntity.prototype.hasMixin = function (mixinCode) {
let mixin = this.mixins[mixinCode]
if (mixinCode === 'audit') {
return !mixin || (!!mixin && mixin.enabled)
}
return (!!mixin && mixin.enabled)
}
/**
* Check the entity has mixin. Throw error if mixin dose not exist or not enabled
* @param {String} mixinCode
*/
UBEntity.prototype.checkMixin = function (mixinCode) {
if (!this.hasMixin(mixinCode)) {
throw new Error('Entity ' + this.code + ' does not have mixin ' + mixinCode)
}
}
UBEntity.prototype.asJSON = function () {
let result = { code: this.code }
_.forEach(this, function (prop, propName) {
if (propName === 'domain') {
return
}
if (prop.asJSON) {
result[propName] = prop.asJSON()
} else {
result[propName] = prop
}
})
return result
}
/**
* Check current user have access to specified entity method
* @param {String} methodCode
* @returns {Boolean}
*/
UBEntity.prototype.haveAccessToMethod = function (methodCode) {
return (UB.isServer && process.isServer)
? App.els(this.code, methodCode)
: this.entityMethods[methodCode] === 1
}
/**
* Filter attributes by properties
* @param {Object|Function} config
* @returns {Array}
* example
*
* domain.get('uba_user').filterAttribute({dataType: 'Document'});
*
* return all attributes where property dataType equal Document
*/
UBEntity.prototype.filterAttribute = function (config) {
if (_.isFunction(config)) {
return _.filter(this.attributes, config)
} else {
return _.filter(this.attributes, function (item) {
let res = true
for (let prop in config) {
if (config.hasOwnProperty(prop)) {
res = res && (item[prop] === config[prop])
}
}
return res
})
}
}
/**
* Check current user have access to AT LAST one of specified methods
* @param {Array} methods
* @returns {boolean}
*/
UBEntity.prototype.haveAccessToAnyMethods = function (methods) {
let me = this
let fMethods = methods || []
let result = false
fMethods.forEach(function (methodCode) {
if (UB.isServer && process.isServer) {
result = result || App.els(me.code, methodCode)
} else {
result = result || me.entityMethods[ methodCode ] === 1
}
})
return result
}
/**
* Check current user have access to ALL of specified methods
* @param {Array<String>} methods Method names
* @returns {Boolean}
*/
UBEntity.prototype.haveAccessToMethods = function (methods) {
let me = this
let result = true
let fMethods = methods || []
fMethods.forEach(function (methodCode) {
if (UB.isServer && process.isServer) {
result = result && App.els(me.code, methodCode)
} else {
result = result && (me.entityMethods[ methodCode ] === 1)
}
})
return result
}
/**
* Convert UnityBase server dateTime response to Date object
* @private
* @param value
* @returns {Date}
*/
function iso8601Parse (value) {
return value ? new Date(value) : null
}
/**
* Convert UnityBase server date response to Date object.
* date response is a day with 00 time (2015-07-17T00:00Z), to get a real date we must add current timezone shift
* @private
* @param value
* @returns {Date}
*/
function iso8601ParseAsDate (value) {
let res = value ? new Date(value) : null
if (res) {
res.setTime(res.getTime() + res.getTimezoneOffset() * 60 * 1000)
}
return res
}
/**
* Convert UnityBase server Boolean response to Boolean (0 = false & 1 = trhe)
* @private
* @param v Value to convert
* @returns {Boolean|null}
*/
function booleanParse (v) {
if (typeof v === 'boolean') {
return v
}
if ((v === undefined || v === null || v === '')) {
return null
}
return (v === 1) || (v === '1')
}
/**
* Return array of conversion rules for raw server response data
* @param {Array<String>} fieldList
* @returns {Array<{index: number, convertFn: function}>}
*/
UBEntity.prototype.getConvertRules = function (fieldList) {
let me = this
let rules = []
let types = UBDomain.ubDataTypes
fieldList.forEach(function (fieldName, index) {
let attribute = me.attr(fieldName)
if (attribute) {
if (attribute.dataType === types.DateTime) {
rules.push({
index: index,
convertFn: iso8601Parse
})
} else if (attribute.dataType === types.Date) {
rules.push({
index: index,
convertFn: iso8601ParseAsDate
})
} else if (attribute.dataType === types.Boolean) {
rules.push({
index: index,
convertFn: booleanParse
})
}
}
})
return rules
}
/**
* Return description attribute name (`descriptionAttribute` metadata property)
* This property may be empty or valid(validation performed by server)
* If case property is empty - try to get attribute with code `caption`
*
* @return {String}
*/
UBEntity.prototype.getDescriptionAttribute = function () {
let result = this.descriptionAttribute || 'caption'
if (!this.attr(result)) {
throw new Error('Missing description attribute for entity ' + this.code)
}
return result
}
/**
* Return information about attribute and attribute entity. Understand complex attributes like firmID.firmType.code
* @param {String} attributeName
* @param {Number} [deep] If 0 - last, -1 - before last, > 0 - root. Default 0.
* @return {{ entity: String, attribute: Object, attributeCode: String }}
*/
UBEntity.prototype.getEntityAttributeInfo = function (attributeName, deep) {
let domainEntity = this
let attributeNameParts = attributeName.split('.')
let currentLevel = -(attributeNameParts.length - 1)
let complexAttr = []
let currentEntity = this.code
/** @type UBEntityAttribute */
let attribute
let key
if (deep && deep > 0) {
return { entity: currentEntity, attribute: domainEntity.attr(attributeNameParts[0]), attributeCode: attributeNameParts[0] }
}
while (domainEntity && attributeNameParts.length) {
if (domainEntity && attributeNameParts.length === 1) {
complexAttr = attributeNameParts[0].split('@')
if (complexAttr.length > 1) {
domainEntity = this.domain.get(complexAttr[1]) // real entity is text after @
attributeName = complexAttr[0]
}
return { entity: currentEntity, attribute: domainEntity.attr(attributeName), attributeCode: attributeName }
}
key = attributeNameParts.shift()
complexAttr = key.split('@')
if (complexAttr.length > 1) {
currentEntity = complexAttr[1]
domainEntity = this.domain.get(currentEntity) // real entity is text after @
key = complexAttr[0]
}
attribute = domainEntity.attr(key)
if (attribute) { // check that attribute exists in domainEntity
if (currentLevel === (deep || 0)) {
return { entity: currentEntity, attribute: attribute, attributeCode: key }
}
attributeName = attributeNameParts[0]
if (attribute.dataType === 'Enum' && attributeName === 'name') {
return { entity: currentEntity, attribute: attribute, attributeCode: key }
} else {
currentEntity = attribute.associatedEntity
domainEntity = attribute.getAssociatedEntity()
}
} else {
return undefined
}
currentLevel += 1
}
return undefined
}
/**
* Return Entity attribute. Understand complex attributes like firmID.firmType.code
* @param {String} attributeName
* @param {Number} [deep] If 0 - last, -1 - before last, > 0 - root. Default 0.
* @return {UBEntityAttribute}
*/
UBEntity.prototype.getEntityAttribute = function (attributeName, deep) {
let domainEntity = this
let attributeNameParts = attributeName.split('.')
let currentLevel = -(attributeNameParts.length - 1)
let complexAttr = []
let attribute
let key
if (deep && deep > 0) {
return domainEntity.attributes[attributeNameParts[0]]
}
// TODO: Сделать так же для других спец.символов, кроме @
while (domainEntity && attributeNameParts.length) {
if (domainEntity && attributeNameParts.length === 1) {
complexAttr = attributeNameParts[0].split('@')
if (complexAttr.length > 1) {
domainEntity = this.domain.get(complexAttr[1]) // real entity is text after @
attributeName = complexAttr[0]
}
return domainEntity.attributes[attributeName]
}
key = attributeNameParts.shift()
complexAttr = key.split('@')
if (complexAttr.length > 1) {
domainEntity = this.domain.get(complexAttr[1]) // real entity is text after @
key = complexAttr[0]
}
attribute = domainEntity.attributes[key]
if (attribute) { // check that attribute exists in domainEntity
if (currentLevel === (deep || 0)) {
return attribute
}
attributeName = attributeNameParts[0]
if (attribute.dataType === 'Enum') {
if (attributeName === 'name') { // WTF?
return attribute
} else {
domainEntity = this.domain.get('ubm_enum')
}
} else {
domainEntity = this.domain.get(attribute.associatedEntity)
}
} else {
return undefined
}
currentLevel += 1
}
return undefined
}
/**
* return attributes code list
* @param {Object|Function} [filter]
* @returns String[]
*/
UBEntity.prototype.getAttributeNames = function (filter) {
let attributes = []
if (filter) {
_.forEach(this.filterAttribute(filter), function (attr) {
attributes.push(attr.code)
})
return attributes
} else {
return Object.keys(this.attributes)
}
}
/**
* Return requirements entity code list for field list
* @param {String[]} [fieldList] (optional)
* @return {String[]}
*/
UBEntity.prototype.getEntityRequirements = function (fieldList) {
let result = []
fieldList = fieldList || this.getAttributeNames()
for (let i = 0, len = fieldList.length; i < len; ++i) {
let fieldNameParts = fieldList[i].split('.')
let attr = this.getEntityAttribute(fieldNameParts[0])
if (attr.dataType === 'Entity') {
if (fieldNameParts.length > 1) {
let tail = [fieldNameParts.slice(1).join('.')]
result = _.union(result, attr.getAssociatedEntity().getEntityRequirements(tail))
} else {
result = _.union(result, [attr.associatedEntity])
}
}
}
return result
}
/**
* Check the entity contains attribute(s) and throw error if not contains
* @param {String|Array<String>} attributeName
* @param {String} contextMessage
*/
UBEntity.prototype.checkAttributeExist = function (attributeName, contextMessage) {
let me = this
attributeName = !_.isArray(attributeName) ? [attributeName] : attributeName
_.forEach(attributeName, function (fieldName) {
if (!me.getEntityAttributeInfo(fieldName)) {
throw new Error(contextMessage + (contextMessage ? ' ' : '') +
'The entity "' + me.code + '" does not have attribute "' + fieldName + '"')
}
})
}
/**
* Return entity description.
* @returns {string}
*/
UBEntity.prototype.getEntityDescription = function () {
return this.description || this.caption
}
/** @class */
function UBEntityAttributeMapping (maping) {
/**
* @type {UBDomain.ExpressionType}
*/
this.expressionType = maping.expressionType
/** @type {string} */
this.expression = maping.expression
}
/**
* @param {Object} attributeInfo
* @param {String} attributeCode
* @param {UBEntity} entity
* @constructor
*/
function UBEntityAttribute (attributeInfo, attributeCode, entity) {
// i18n already merged by entity constructor
/**
* @type {String}
* @readonly
*/
this.code = attributeCode
/** @type {String}
* @readonly
*/
this.name = attributeInfo.name
/**
* Non enumerable (to prevent JSON.stringify circular ref) read only entity reference
* @property {UBEntity} entity
* @readonly
*/
Object.defineProperty(this, 'entity', {enumerable: false, value: entity})
/**
* Data type
* @type {UBDomain.ubDataTypes}
* @readonly
*/
this.dataType = attributeInfo.dataType || 'String'
/**
* Name of entity referenced by the attribute (for attributes of type `Many` - entity name from the AssociationManyData)
* @type {String}
* @readonly
*/
this.associatedEntity = attributeInfo.associatedEntity
/**
* @type {String}
* @readonly
*/
this.associationAttr = attributeInfo.associationAttr
/**
* @type {String}
* @readonly
*/
this.caption = attributeInfo.caption || ''
/**
* @type {String}
* @readonly
*/
this.description = attributeInfo.description || ''
/**
* @type {String}
* @readonly
*/
this.documentation = attributeInfo.documentation || ''
/**
* @type {Number}
* @readonly
*/
this.size = attributeInfo.size || 0
/**
* Attribute value can be empty or null
* @type {boolean}
* @readonly
*/
this.allowNull = (attributeInfo.allowNull !== false)
/**
* Allow order by clause by this attribute
* @type {boolean}
* @readonly
*/
this.allowSort = (attributeInfo.allowSort !== false)
/**
* @type {boolean}
* @readonly
*/
this.isUnique = (attributeInfo.isUnique === true)
/**
* @type{String}
* @readonly
*/
this.defaultValue = attributeInfo.defaultValue
/**
* Allow edit
* @type {Boolean}
* @readonly
*/
this.readOnly = (attributeInfo.readOnly === true)
/**
* @property {Boolean}
* @readonly
*/
this.isMultiLang = (attributeInfo.isMultiLang === true)
/**
* Possible for dataType=Entity - enable cascade delete on application serve level (not on database level)
* @type {Boolean}
* @readonly
*/
this.cascadeDelete = (attributeInfo.cascadeDelete === true)
/**
* Required for dataType=Enum - Group code from ubm_enum.eGroup
* @property {String} enumGroup
* @readonly
*/
this.enumGroup = attributeInfo.enumGroup
/**
* @type {Object}
* @readonly
*/
this.customSettings = attributeInfo.customSettings || {}
/**
* Required for dataType=Many - name of the many-to-many table. UB create system entity with this name and generate table during DDL generation
* @property {String}
* @readonly
*/
this.associationManyData = attributeInfo.associationManyData
/**
* Applicable to attribute with dataType=Document - name of store from storeConfig application config section. If emtpy - store with isDefault=true will be used
* @type{String}
* @readonly
*/
this.storeName = attributeInfo.storeName
/**
* Applicable for dataType=Entity. If false DDL generator will bypass foreign key generation on the database level
* @type {boolean}
*/
this.generateFK = attributeInfo.generateFK !== false
/**
* If true - client should shows this attribute in auto-build forms and in '*' select fields
* @type {boolean}
*/
this.defaultView = attributeInfo.defaultView !== false
/**
* Optional mapping of atribute to phisical data (for extended domain info only).
* Calculated from a entity mapping collection in accordance with application connection confiduration
* @type {UBEntityAttributeMapping}
* @readonly
*/
this.mapping = undefined
let me = this
if (attributeInfo.mapping && Object.keys(attributeInfo.mapping).length) {
let dialectsPriority = UBDomain.dialectsPriority[this.entity.connectionConfig.dialect]
_.forEach(dialectsPriority, function (dialect) {
if (attributeInfo.mapping[dialect]) {
me.mapping = new UBEntityAttributeMapping(attributeInfo.mapping[dialect])
return false // break loop
}
})
}
/**
* @property {String} physicalDataType
* @readonly
*/
this.physicalDataType = UBDomain.getPhysicalDataType(this.dataType || 'String')
}
/**
* Return associated entity. Return null if attribute type is not Entity.
* @returns {UBEntity}
*/
UBEntityAttribute.prototype.getAssociatedEntity = function () {
return this.associatedEntity ? this.entity.domain.get(this.associatedEntity) : null
}
UBEntityAttribute.prototype.asJSON = function () {
let result = {}
_.forEach(this, function (prop, propName) {
if (propName === 'entity') {
return
}
if (prop.asJSON) {
result[propName] = prop.asJSON()
} else {
result[propName] = prop
}
})
return result
}
/**
* Contains all properties defined in mixin section of a entity metafile
* @class
* @protected
* @param {Object} mixinInfo
* @param {Object} i18n
* @param {String} mixinCode
*/
function UBEntityMixin (mixinInfo, i18n, mixinCode) {
/**
* Mixin code
* @type {String}
*/
this.code = mixinCode
_.assign(this, mixinInfo)
if (i18n) {
_.assign(this, i18n)
}
}
UBEntityMixin.prototype.enabled = true
/**
* Mixin for persisting entity to a database
* @class
* @extends UBEntityMixin
* @param mixinInfo
* @param i18n
* @param mixinCode
*/
function UBEntityStoreMixin (mixinInfo, i18n, mixinCode) {
UBEntityMixin.apply(this, arguments)
}
UBEntityStoreMixin.prototype = Object.create(UBEntityMixin.prototype)
UBEntityStoreMixin.prototype.constructor = UBEntityStoreMixin
// defaults
/**
* Is `simpleAudit` enabled
* @type {boolean}
*/
UBEntityStoreMixin.prototype.simpleAudit = false
/**
* Use a soft delete
* @type {boolean}
*/
UBEntityStoreMixin.prototype.safeDelete = false
/**
* Historical data storage mixin
* @class
* @extends UBEntityMixin
* @param mixinInfo
* @param i18n
* @param mixinCode
* @constructor
*/
function UBEntityHistoryMixin (mixinInfo, i18n, mixinCode) {
UBEntityMixin.apply(this, arguments)
}
UBEntityHistoryMixin.prototype = Object.create(UBEntityMixin.prototype)
UBEntityHistoryMixin.prototype.constructor = UBEntityHistoryMixin
/**
* A history storage strategy
* @type {string}
*/
UBEntityHistoryMixin.prototype.historyType = 'common'
/**
* Access control list mixin
* @class
* @extends UBEntityMixin
* @param mixinInfo
* @param i18n
* @param mixinCode
*/
function UBEntityAclRlsMixin (mixinInfo, i18n, mixinCode) {
UBEntityMixin.apply(this, arguments)
}
UBEntityAclRlsMixin.prototype = Object.create(UBEntityMixin.prototype)
UBEntityAclRlsMixin.prototype.constructor = UBEntityAclRlsMixin
// defaults
UBEntityAclRlsMixin.prototype.aclRlsUseUnityName = false
UBEntityAclRlsMixin.prototype.aclRlsSelectionRule = 'exists'
/**
* Full text search mixin
* @class
* @extends UBEntityMixin
* @param mixinInfo
* @param i18n
* @param mixinCode
*/
function UBEntityFtsMixin (mixinInfo, i18n, mixinCode) {
UBEntityMixin.apply(this, arguments)
}
UBEntityFtsMixin.prototype = Object.create(UBEntityMixin.prototype)
UBEntityFtsMixin.prototype.constructor = UBEntityFtsMixin
/**
* scope
* @type {string}
*/
UBEntityFtsMixin.prototype.scope = 'connection' // sConnection
/**
* Data provider type
* @type {string}
*/
UBEntityFtsMixin.prototype.dataProvider = 'mixin'// dcMixin
/**
* Attribute level security mixin
* @param mixinInfo
* @param i18n
* @param mixinCode
* @constructor
* @extends UBEntityMixin
*/
function UBEntityAlsMixin (mixinInfo, i18n, mixinCode) {
UBEntityMixin.apply(this, arguments)
}
UBEntityAlsMixin.prototype = Object.create(UBEntityMixin.prototype)
UBEntityAlsMixin.prototype.constructor = UBEntityAlsMixin
/**
* Is optimistic
* @type {boolean}
*/
UBEntityAlsMixin.prototype.alsOptimistic = true
UBDomain.UBEntity = UBEntity
UBDomain.UBModel = UBModel
UBDomain.UBEntity.UBEntityAttribute = UBEntityAttribute
module.exports = UBDomain