Приклади серверних скриптів
В цьому розділі описано приклади скриптів для обробки даних на сервері.
Серверні скрипти — це механізм для виконання спеціального JavaScript коду на сервері. Як зазвичай, вони містять бізнес-логіку, яка не залежить від актуальної версії моделі та не вимагає створення окремих доменних моделей UB.
Оскільки скрипти виконуються на сервері, існує багато можливостей:
- реалізація спеціальної бізнес-логіки, такої як валідації чи агрегація
- керування даними БД, такими як вибір/вставка/оновлення/видалення
- робота з файлами
- виклик зовнішніх API
- відправлення електронних листів
Кожен скрипт форми повинен бути модулем CommonJS, тобто він повинен експортувати об'єкт або функцію і може вимагати інші модулі:
// import standard module
const UB = require('@unitybase/ub')
// export several functions for using outside
module.exports = {
myMethod1() {
},
myMethod2(arg1, arg2, arg3) {
}
}
// export single function
module.exports = function(a, b) {
return a + b
}
У серверних скриптах ми можемо використовувати весь серверний API UB. Нижче описані популярні приклади використання цього API.
1. Приклади використання серверного API платформи UB
const {Repository, DataStore, App, UBAbort} = require('@unitybase/ub')
// select some document id
const documentID = Repository('dfx_Document')
.attrs('ID')
.limit(1)
.selectScalar()
// select document by id
const document = Repository('dfx_Document')
.attrs('docNumber', 'stateID.code', 'authorID', 'attrValues.organization')
.selectById(documentID, {
'stateID.code': 'stateCode',
'attrValues.organization': 'organizationID'
})
// select document attachments
const documentAttachments = Repository('dfx_DocumentAttachment')
.attrs('ID', 'description', 'attachment', 'original')
.where('documentID', '=', documentID)
.where('documentItemID', 'isNull')
.where('attrID', 'isNull')
.selectAsObject()
// select custom file attribute with the `fileAttributeCode` code
const customFile = Repository('dfx_DocumentAttachment')
.attrs('ID', 'description', 'attachment', 'original')
.where('documentID', '=', documentID)
.where('documentItemID', 'isNull')
.where('attrID.code', '=', 'fileAttributeCode')
.selectSingle()
// create an organization
const createdOrganization = DataStore('org_organization').insertAsObject({
fieldList: ['ID', 'code'],
execParams: {
code: 'myOrganization',
name: 'My Organization',
fullName: 'My First Created Organization'
}
})
// update an organization
const updatedOrganization = DataStore('org_organization').updateAsObject({
fieldList: ['ID', 'code'],
execParams: {
ID: createdOrganization.ID,
name: 'Updated Name'
},
__skipOptimisticLock: true
})
// update document custom attributes
DataStore('dfx_Document').run('update', {
execParams: {
ID: documentID,
attrValues: JSON.stringify({
integerAttr: 1,
strAttr: 'asd',
refAttr: createdOrganization,
booleanAttr: false
})
},
__skipOptimisticLock: true
})
// select content of Document Image
const docImage = App.blobStores.getContent({
ID: documentID,
entity: 'dfx_Document',
attribute: 'docImage'
})
// set Document Image content
DataStore('dfx_Document').run('update', {
execParams: {
ID: documentID,
docImage: JSON.stringify(
App.blobStores.putContent({
entity: 'dfx_Document',
attribute: 'docImage',
ID: documentID,
origName: 'file.txt'
}, 'txtFileContent')
)
},
__skipOptimisticLock: true
})
// execute document action
DataStore('dfx_Document').run('execAction', {
execParams: {
ID: documentID,
actionCode: 'actionCode'
},
__skipOptimisticLock: true
})
// invoke API method
const result = DataStore('msg_ExternalApi').runWithResult('invokeMethod', {
execParams: {
code: 'myAPI',
methodName: 'someMethod',
methodArgs: {
a: 1,
b: 2
}
}
})
// throw an Error
if (!documentID) {
throw new UBAbort(`<<<Cannot process document with ID=${document} since it does not exist>>>`)
}
2. Приклади серверних модулів
Серверний модуль з кодом copyUtils експортує об'єкт з 3 функціями:
- copyTableAttrs - для копіювання колекцій документів
- copyFileAttrs - для копіювання файлів документів
- copyRichTextAttrs - для копіювання форматованого тексту
copyUtils
const {App, Repository, DataStore, UBAbort} = require('@unitybase/ub')
class SignatureCopier {
static get BLOB_ATTRS() {
return ['signature']
}
constructor() {
this._dataStore = DataStore('dfx_DocumentSignature')
this._signaturesByInstanceID = new Map()
}
/**
* Query DB for signatures, capable to query all signatures of a document or a document item,
* or a file attribute, etc. - depending on query configuration.
*
* Loaded signatures are cached for copying.
*
* @param {string} imageEntity
* @param {number} [documentID]
* @param {number|null} [documentItemID]
* @param {number} [instanceID]
* @param {string[]} [fileAttrCodes]
* If specified, filter signatures only for specified file attributes.
* @param {string[]} [tableAttrCodes]
* If specified, filter signatures only for document items belonging to specified table attribute.
*/
loadSignatures({
imageEntity,
documentID,
documentItemID,
instanceID,
fileAttrCodes,
tableAttrCodes
}) {
const signatures = Repository('dfx_DocumentSignature')
.attrs(
'ID', 'imageAttribute', 'imageMD5', 'imageEntity',
...SignatureCopier.BLOB_ATTRS,
'timeStamp', 'signer', 'revision', 'instanceID', 'serial', 'organizationCode')
.where('imageEntity', '=', imageEntity)
.whereIf(Number.isInteger(documentID), 'documentID', '=', documentID)
.whereIf(Number.isInteger(instanceID), 'instanceID', '=', instanceID)
.whereIf(documentItemID === null, 'documentItemID', 'isNull')
.whereIf(Number.isInteger(documentItemID), 'documentItemID', '=', documentItemID)
.whereIf(Array.isArray(fileAttrCodes), 'docAttachID.attrID.code', 'in', fileAttrCodes)
.whereIf(Array.isArray(tableAttrCodes), 'documentItemID.attrID.code', 'in', tableAttrCodes)
.misc({__skipRls: true})
.orderBy('ID')
.selectAsObject()
for (const signature of signatures) {
let list = this._signaturesByInstanceID.get(signature.instanceID)
if (!list) {
list = []
this._signaturesByInstanceID.set(signature.instanceID, list)
}
list.push(signature)
}
}
/**
* Copy signature for a set of blob attribute for one record.
* Record must be already copied.
*
* @param {object} srcInstance Source instance, must contain ID and all blob attributes.
* @param {object} newInstance The copy, must contain ID and all blob attributes.
* @param {string[]} blobAttrs
* @param {number} newDocID
* @param {number|null} newDocumentItemID
*/
copySignatures(srcInstance, newInstance, blobAttrs, newDocID, newDocumentItemID) {
const signatures = this._signaturesByInstanceID.get(srcInstance.ID)
if (!signatures) {
return
}
for (const attr of blobAttrs) {
const srcInstanceBlobInfo = srcInstance[attr]
const newInstanceBlobInfo = newInstance[attr]
if (newInstanceBlobInfo && srcInstanceBlobInfo) {
const srcBlobRevision = JSON.parse(srcInstanceBlobInfo).revision
for (const signature of signatures) {
if (signature.imageAttribute === attr && srcBlobRevision === signature.revision) {
this._createSignature(
signature, {
documentID: newDocID,
instanceID: newInstance.ID,
documentItemID: newDocumentItemID,
revision: JSON.parse(newInstanceBlobInfo).revision
}
)
}
}
}
}
}
_createSignature(signature, {documentID, instanceID, documentItemID, revision}) {
const signatureID = this._dataStore.generateID()
this._dataStore.run('insert', {
entity: 'dfx_DocumentSignature',
execParams: {
ID: signatureID,
documentID,
instanceID,
documentItemID,
revision,
imageEntity: signature.imageEntity,
imageAttribute: signature.imageAttribute,
imageMD5: signature.imageMD5,
timeStamp: signature.timeStamp,
signer: signature.signer,
serial: signature.serial,
organizationCode: signature.organizationCode,
signature: copyBlob('dfx_DocumentSignature', 'signature', signature, signatureID)
},
__skipRls: true
})
}
}
class FileAttrCopier {
constructor() {
this._dataStore = DataStore('dfx_DocumentAttachment')
this._idMap = new Map()
}
mapID(srcID, newID) {
this._idMap.set(srcID, newID)
}
/**
* Query DB for file attributes and group by row ID (documentID / documentItemID)
* Loaded signatures are cached for copying.
*
* @param {number} documentID
* @param {number|null} [documentItemID]
* @param {string[]|null} [fileAttrCodes]
* @param {string[]} [tableAttrCodes]
* @param {boolean} copySignatures
* @param {number} newDocID
*/
copy({
documentID,
documentItemID,
fileAttrCodes,
tableAttrCodes,
copySignatures
}, newDocID) {
const FILE_BLOB_ATTRS = ['attachment', 'original']
const attachments = Repository('dfx_DocumentAttachment')
.attrs('ID', 'description', ...FILE_BLOB_ATTRS, 'attrID', 'documentItemID')
.where('documentID', '=', documentID)
.whereIf(documentItemID === null, 'documentItemID', 'isNull')
.whereIf(Number.isInteger(documentItemID), 'documentItemID', '=', documentItemID)
.whereIf(Array.isArray(fileAttrCodes), 'attrID.code', 'in', fileAttrCodes)
.whereIf(fileAttrCodes === null, 'attrID', 'isNull')
.whereIf(Array.isArray(tableAttrCodes), 'documentItemID.attrID.code', 'in', tableAttrCodes)
.where('attachment', 'notNull')
.misc({__skipRls: true})
.orderBy('ID')
.selectAsObject()
const signatureCopier = new SignatureCopier()
if (copySignatures) {
signatureCopier.loadSignatures({
imageEntity: 'dfx_DocumentAttachment',
documentID,
documentItemID,
fileAttrCodes,
tableAttrCodes
})
}
for (const attachment of attachments) {
const attachmentID = this._dataStore.generateID()
const newDocumentItemID = this._idMap.get(attachment.documentItemID) || null
this._dataStore.run('insert', {
fieldList: ['ID', ...FILE_BLOB_ATTRS],
execParams: {
ID: attachmentID,
attrID: attachment.attrID,
documentID: newDocID,
documentItemID: newDocumentItemID,
description: attachment.description,
attachment: copyBlob('dfx_DocumentAttachment', 'attachment', attachment, attachmentID),
original: copyBlob('dfx_DocumentAttachment', 'original', attachment, attachmentID)
},
__skipRls: true
})
if (copySignatures) {
signatureCopier.copySignatures(
attachment,
this._dataStore.getAsJsObject()[0],
FILE_BLOB_ATTRS,
newDocID,
newDocumentItemID
)
}
}
}
}
class RichTextAttrCopier {
constructor() {
this._dataStore = DataStore('dfx_DocumentRichText')
this._idMap = new Map()
}
mapID(srcID, newID) {
this._idMap.set(srcID, newID)
}
/**
* Query DB for rich text attributes and group by row ID (documentID / documentItemID)
*
* @param {number} documentID
* @param {number|null} [documentItemID]
* @param {string[]|null} [fileAttrCodes]
* @param {string[]} [tableAttrCodes]
* @param {boolean} copySignatures
* @param {number} newDocID
*/
copy({
documentID,
documentItemID,
richTextAttrCodes,
tableAttrCodes
}, newDocID) {
const richTextAttrs = Repository('dfx_DocumentRichText')
.attrs('ID', 'content', 'attrID', 'documentID', 'documentItemID')
.where('documentID', '=', documentID)
.where('content', 'notNull')
.whereIf(documentItemID === null, 'documentItemID', 'isNull')
.whereIf(Number.isInteger(documentItemID), 'documentItemID', '=', documentItemID)
.whereIf(Array.isArray(richTextAttrCodes), 'attrID.code', 'in', richTextAttrCodes)
.whereIf(richTextAttrCodes === null, 'attrID', 'isNull')
.whereIf(Array.isArray(tableAttrCodes), 'documentItemID.attrID.code', 'in', tableAttrCodes)
.misc({__skipRls: true})
.orderBy('ID')
.selectAsObject()
for (const richTextItem of richTextAttrs) {
const richTextID = this._dataStore.generateID()
const newDocumentItemID = this._idMap.get(richTextItem.documentItemID) || null
this._dataStore.run('insert', {
execParams: {
ID: richTextID,
attrID: richTextItem.attrID,
documentID: newDocID,
documentItemID: newDocumentItemID,
content: copyBlob('dfx_DocumentRichText', 'content', richTextItem, richTextID)
},
__skipRls: true
})
}
}
}
function copyBlob(entity, attribute, sourceObj, destID) {
const blobInfo = sourceObj[attribute]
if (!blobInfo) {
return null
}
const content = App.blobStores.getContent(
{ID: sourceObj.ID, entity, attribute},
{encoding: 'bin'}
)
const destBlobInfo = App.blobStores.putContent(
{ID: destID, entity, attribute, origName: JSON.parse(blobInfo).origName},
content
)
return JSON.stringify(destBlobInfo)
}
/**
* Copy document collections
*
* @param {number} docID
* @param {number} newDocID
* @param {string[]} tableAttrCodes
*/
function copyTableAttrs(docID, newDocID, tableAttrCodes) {
if (!Array.isArray(tableAttrCodes) || tableAttrCodes.length === 0) {
return
}
const ds = DataStore('dfx_DocumentItem')
const fileAttrCopier = new FileAttrCopier()
const richTextCopier = new RichTextAttrCopier()
const documentItems = Repository('dfx_DocumentItem')
.attrs('ID', 'attrID', 'attrValues')
.where('documentID', '=', docID)
.where('attrID.code', 'in', tableAttrCodes)
.orderBy('ID')
.misc({__skipRls: true})
.selectAsObject()
for (const docItem of documentItems) {
const newDocumentItemID = ds.generateID()
ds.run('insert', {
execParams: {
ID: newDocumentItemID,
documentID: newDocID,
attrID: docItem.attrID,
attrValues: docItem.attrValues
},
__skipRls: true
})
fileAttrCopier.mapID(docItem.ID, newDocumentItemID)
richTextCopier.mapID(docItem.ID, newDocumentItemID)
}
fileAttrCopier.copy({
documentID: docID,
tableAttrCodes,
copySignatures: true
}, newDocID)
richTextCopier.copy({
documentID: docID,
tableAttrCodes
}, newDocID)
}
/**
* Copy document files
*
* @param {number} docID
* @param {number} newDocID
* @param {string[]} fileAttrCodes
*/
function copyFileAttrs(docID, newDocID, fileAttrCodes) {
const fileAttrCopier = new FileAttrCopier()
fileAttrCopier.copy({
documentID: docID,
fileAttrCodes,
copySignatures: true
}, newDocID)
}
/**
* Copy document rich texts
*
* @param {number} docID
* @param {number} newDocID
* @param {string[]} fileAttrCodes
*/
function copyRichTextAttrs(docID, newDocID, richTextAttrCodes) {
const richTextAttrCopier = new RichTextAttrCopier()
richTextAttrCopier.copy({
documentID: docID,
richTextAttrCodes,
copySignatures: true
}, newDocID)
}
module.exports = {
copyTableAttrs,
copyFileAttrs,
copyRichTextAttrs
}
Серверний модуль з кодом documentEvents експортує об'єкт з методом для легкого оновлення атрибутів документа всередині обробника події типу Скрипт.
documentEvents
const {DataStore} = require('@unitybase/ub')
module.exports = {
updateDocumentAttributes(event, execParams) {
const ds = DataStore('dfx_Document')
ds.run('update', {
fieldList: Object.keys(execParams),
execParams,
__skipRls: true,
__skipOptimisticLock: true
})
// Make original method know the document property change, so that it return it to client
global.dfx_Document.updateEventStore(event, ds)
}
}
Серверний модуль з кодом processVariables, який інкапсулює логіку управління змінними процесу
processVariable
const {DataStore} = require('@unitybase/ub')
module.exports = {
get(processID) {
const {processVariables} = DataStore('bpm_Process').runWithResult('queryProcessVariables', {
execParams: {
ID: event.processInstanceID
}
})
const result = {}
for (const {name, value} of JSON.parse(processVariables)) {
result[name] = value
}
return result
},
update(processID, name, value) {
DataStore('bpm_Process').run('updateVariable', {
execParams: {
ID: processID,
varName: name,
value
}
})
},
delete(processID, name) {
DataStore('bpm_Process').run('deleteVariable', {
execParams: {
ID: processID,
varName: name
}
})
}
}
Серверний модуль з кодом exchangeService, який інкапсулює логіку виклику зовнішнього API
Для отримання результатів запиту зовнішнього API в серверному скрипті необхідно в конструкторі API створювати прив'язку "параметрів відповіді".
exchangeService
const {DataStore} = require('@unitybase/ub')
module.exports = {
/**
* @returns {object<string, number>}
*/
getRates(from, to, amount) {
return DataStore('msg_ExternalApi').runWithResult('invokeMethod', {
execParams: {
code: 'exchangeService',
methodName: 'getRates'
}
})
},
/**
* @param {string} from
* @param {string} to
* @param {string} amount
* @returns {number}
*/
convert(from, to, amount) {
return DataStore('msg_ExternalApi').runWithResult('invokeMethod', {
execParams: {
code: 'exchangeService',
methodName: 'convert',
methodArgs: {
from,
to,
amount
}
}
})
}
}
Серверний модуль з кодом transformDataUtils, який містить допоміжні методи для перетворення деяких даних
transformDataUtils
const {Repository} = require('@unitybase/ub')
module.exports = {
getIdByCode(entity, ID) {
return Repository(entity)
.attrs('ID')
.where('code', '=', ID)
.selectScalar()
},
normalizeBoolean(value) {
return value === true || value === 'true' || value === 1 || value === '1'
}
}
SumTable - підрахунок суми числового атрибута з таблиці
const {App,Repository,DataStore} = require('@unitybase/ub')
module.exports = {
sumTable(tableAttributeCode, AttributeCode, docID) {
const TableID= UB.Repository('frm_Attribute')
.attrs('ID')
.where('code', '=', tableAttributeCode)
.selectScalar()
const sumArrt = 'attrValues.'+AttributeCode
const TableData= UB.Repository('dfx_DocumentItem')
.attrs(sumArrt)
.where('documentID', '=', docID)
.where('attrID', '=', TableID)
.selectAsArrayOfValues()
var numberArray = TableData.map(Number)
var sum = numberArray.reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0)
return sum
}
}
Скрипт в обробнику "Скрипт" типу документа
const table = require('#SumTable') //виклик серверного модуля
const {App,Repository,DataStore} = require('@unitybase/ub')
module.exports = ({event, eventHandler, eventVariables}) => {
//виклик функції з бібліотеки серверних модулів з параметрами: ms16 - код табличного атрибута, ms03 - код атрибута типу "Число" в таблиці
const tableData= table.sumTable('ms16','ms03',event.docID)
const ds = DataStore('dfx_Document')
// update document subject
ds.run('update', {
fieldList: [
'attrValues',
'mi_modifyDate',
'mi_modifyUser'
],
execParams: {
ID: event.docID,
attrValues : JSON.stringify({
"ms05":tableData, //встановлення суми атрибутів таблиці в атрибут документа з кодом ms05
"ms02":JSON.stringify(event)
})
},
__skipRls: true,
__skipOptimisticLock: true
})
global.dfx_Document.updateEventStore(event, ds)
}
3. Перетворення даних перед формуванням документа за шаблоном
Приклад серверного скрипта для перетворення даних перед формуванням документа за шаблоном. Скрипт додається в шаблоні документа.
const {Repository} = require('@unitybase/ub')
const {formatByPattern} = require('@unitybase/cs-shared')
module.exports = {
transformTemplateData({templateData, dataEntityName, dataID}) {
// assign value of custom attribute `currentDate` to the current date
templateData['attrValues.currentDate'] = formatByPattern.formatDate(new Date(), 'dateTimeFull')
if (dataEntityName === 'dfx_Document') {
// assign value of custom attribute `attrSum` to the sum of the `intAttr` column of the `tableAttr` table attribute
templateData['attrValues.attrSum'] = Repository('dfx_DocumentItem')
.attrs('attrValues.intAttr')
.where('documentID', '=', dataID)
.where('attrID.code', '=', 'tableAttr')
.selectAsArrayOfValues()
.reduce((acc, cur) => acc + cur, 0)
}
}
}
4. Нормалізація даних при IDP розпізнаванні
Приклад серверного скрипту для перетворення розпізнаних даних IDP. Скрипт додається в шаблон IDP.
// require our server module described above
const transformDataUtils = require('#transformDataUtils')
module.exports = {
transformResult({resultValues, rawResult}) {
const docAttrValues = resultValues.execParams.attrValues
// normalize boolean attribute - convert 1,'1' to true, otherwise to false
docAttrValues.booleanAttr = transformDataUtils.normalizeBoolean(docAttrValues.booleanAttr)
// find organization ID by its code and delete from data this already not needed code
docAttrValues.orgID = transformDataUtils.getIdByCode('org_organization', docAttrValues.orgCode)
delete docAttrValues.orgCode
}
}
5. Обробник подій Скрипт типу документа
Приклад серверного скрипту в обробнику типу документа Скрипт. В скрипті використовується скрипт з бібліотеки серверних модулів.
// require our server module described above
const documentEvents = require('#documentEvents')
module.exports = ({event, eventHandler, eventVariables}) => {
// nullify some attributes values
documentEvents.updateDocumentAttributes(event, {
ID: event.docID,
attrValues: JSON.stringify({
firstName: null,
lastName: null,
birthDate: null
})
})
}
6. Виклик кастомного серверного модуля з BPM-процесу
Приклад виконання скрипта з бізнес-логікою із сервісної задачі. Код нижче отримує змінні процесу з ідентифікатором документа та ідентифікатором процесу і передає їх серверному модулю з назвою getCurrencyRate.
Приклад виклику всередині сервісної задачі бізнес-процесу:
JSON.stringify({
entity: 'bpm_ExtTaskService',
method: 'callServerModule',
execParams: {
code: 'getCurrencyRate',
params: {
documentID: execution.getVariable('documentID'),
processID: execution.getVariable('processID')
}
}
})
Приклад серверного модуля з кодом getCurrencyRate:
const {DataStore, Repository} = require('@unitybase/ub')
// require out helper modules
const exchangeService = require('#exchangeService')
const processVariablesService = require('#processVariables')
module.exports = ({documentID, processID}) => {
// get the amount from process variables
const processVariables = processVariablesService.get(processID)
const {amount} = processVariables
// get the currencies convert `from` and `to`` from document attributes
const {from, to} = Repository('dfx_Document')
.attrs('attrValues.from', 'attrValues.to')
.selectById(documentID, {
'attrValues.from': 'from',
'attrValues.to': 'to'
})
// call method of a server module which calls API internally
const conversationResult = exchangeService.convert(from, to, amount)
// assign result as document attribute
DataStore('dfx_Document').run('update', {
execParams: {
attrValues: JSON.stringify({
exchangeValue: conversationResult
})
},
__skipRls: true,
__skipOptimisticLock: true
})
}
7. Виклик кастомного серверного модуля з шаблону звіту (UB Report)
const {Repository, Session, UBAbort} = require('@unitybase/ub')
const {serverScriptsManager} = require('@unitybase/dom')
exports.reportCode = {
buildReport: function(reportParams) {
console.log('asd')
const serverScriptModuleId = Repository('dom_ServerModule')
.attrs('ID')
.where('code', '=', 'demo1') //demo1 - server module code
.selectScalar()
if (!serverScriptModuleId) {
throw new UBAbort('<<<dom_serverScripts.errors.noScriptModuleWithCode>>>', 'demo1')
}
const serverModule = serverScriptsManager.requireModule({
objectEntity: 'dom_ServerModule',
objectID: serverScriptModuleId,
innerID: 'default'
})
reportParams.result = serverModule.getAnswer() //getAnswer- function in server module
return this.buildHTML(reportParams)
}
}