UNPKG

@5minds/processcube_engine

Version:

The ProcessCube Engine. Stores and executes BPMNs.

292 lines • 15.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProcessTokenService = void 0; const ajv_1 = __importDefault(require("ajv")); const inversify_1 = require("inversify"); const path_1 = __importDefault(require("path")); const piscina_1 = __importDefault(require("piscina")); const processcube_engine_sdk_1 = require("@5minds/processcube_engine_sdk"); const IocRegistrations_1 = require("../Contracts/IocRegistrations"); const Configurator_1 = __importDefault(require("../Tools/Configurator")); const FlowNodeInstanceAdapter_1 = require("../Tools/DatabaseAdaptersSequelize/FlowNodeInstanceAdapter"); let ProcessTokenService = class ProcessTokenService { flowNodeInstanceDatabaseAdapter; runtimeExpressionWorkers; config; constructor(flowNodeInstanceDatabaseAdapter) { this.flowNodeInstanceDatabaseAdapter = flowNodeInstanceDatabaseAdapter; this.config = Configurator_1.default.runtimeExpressions() ?? {}; this.runtimeExpressionWorkers = new piscina_1.default({ filename: path_1.default.resolve(__dirname, './worker/RuntimeExpressionWorker.js'), maxThreads: this.config.workerPoolSize, idleTimeout: 1000, closeTimeout: 100, concurrentTasksPerWorker: 1, }); } async dispose() { await this.runtimeExpressionWorkers.close({ force: true }); } getAvailableParametersForExpressions() { return [ 'engine', 'environment', 'correlationId', 'correlationMetadata', 'currentDataObject', 'currentFlowNode', 'dataObjects', 'identity', 'processInstanceMetadata', 'token', 'flowNodeExecutionCount', 'processModelVersion', ]; } /** * Gets the result of the last Flow Node Instance executed for a Process Instance. * Should only be used for Process Instances that were finished successfully. */ async getFinalResult(processInstanceId) { const queryResult = await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: processInstanceId, state: [processcube_engine_sdk_1.FlowNodeInstanceState.finished, processcube_engine_sdk_1.FlowNodeInstanceState.error, processcube_engine_sdk_1.FlowNodeInstanceState.terminated], }, 0, 1, { sortBy: processcube_engine_sdk_1.FlowNodeInstanceSortableColumns.createdAt, sortDir: 'DESC' }); if (queryResult.flowNodeInstances.length === 0) { throw new processcube_engine_sdk_1.InternalServerError(`Failed to determine the final result of Process Instance \`${processInstanceId}\`, because no finished Flow Node Instance could be found.`); } const finalFlowNodeInstance = queryResult.flowNodeInstances.pop(); return { flowNodeInstanceId: finalFlowNodeInstance.flowNodeInstanceId, flowNodeId: finalFlowNodeInstance.flowNodeId, result: finalFlowNodeInstance.endToken ?? {}, }; } async validateSchema(dataContract, processedParams, result) { const ajv = new ajv_1.default(); // type inference is not supported for JTDDataType yet const schema = JSON.parse(dataContract); const validate = ajv.compile(schema); const isValid = validate(result); if (!isValid) { const validateError = new processcube_engine_sdk_1.BadRequestError('The given data set did not fulfill the Data Contract'); validateError.additionalInformation = { originalError: validate.errors, dataContract: dataContract, correlationId: processedParams.correlationId, processInstanceId: processedParams.processInstanceMetadata?.id, processModelId: processedParams.currentFlowNode?.processModelId, flowNodeId: processedParams.currentFlowNode?.id, }; throw validateError; } return isValid; } async executeRuntimeExpression(expression, params, dataContract) { const scriptToRun = this.sanitizeFlowNodeExpression(expression); const processedParams = await this.getParametersForRuntimeExpression(expression, params); const flowNodeExecutionCount = await this.getFlowNodeExecutionCount(params); const processModelVersion = processedParams.currentFlowNode?.processModelVersion; try { const context = { engine: { id: Configurator_1.default.application().id, name: Configurator_1.default.application().name, }, environment: Configurator_1.default.custom(), correlationId: processedParams.correlationId, correlationMetadata: processedParams.correlationMetadata, currentDataObject: processedParams.currentDataObject, currentFlowNode: processedParams.currentFlowNode, dataObjects: processedParams.dataObjects, processModel: processedParams.processModel, identity: processedParams.identity, processInstanceMetadata: processedParams.processInstanceMetadata, token: processedParams.token, flowNodeExecutionCount: flowNodeExecutionCount, processModelVersion: processModelVersion, }; if (processedParams.additionalProperties) { Object.assign(context, processedParams.additionalProperties); } const result = await this.executeTimeboxedRuntimeExpression(scriptToRun, context); const resultIsAnObject = result != undefined && typeof result === 'object' && (result.serialize || result.toString() === '[object Object]' || Array.isArray(result)); if (!processedParams.allowNonObjectResults && !resultIsAnObject) { const invalidResponseTypeError = new processcube_engine_sdk_1.BadRequestError('Runtime Expression must return an Array or an Object.'); invalidResponseTypeError.additionalInformation = { flowNodeId: processedParams.currentFlowNode?.id, flowNodeName: processedParams.currentFlowNode?.name, processModelId: processedParams.currentFlowNode?.processModelId, processModelName: processedParams.currentFlowNode?.processModelName, processInstanceId: processedParams.processInstanceMetadata?.id, expression: expression, resultType: typeof result, }; throw invalidResponseTypeError; } if (dataContract !== undefined) { await this.validateSchema(dataContract, processedParams, result); } return result; } catch (error) { error.category = 'process'; error.message = `Failure in process: ${error.message}`; throw error; } } async getFlowNodeExecutionCount(params) { if (params.processInstanceMetadata?.id === undefined) { return null; } const flowNodeExecutionCount = await this.flowNodeInstanceDatabaseAdapter.getFlowNodeExecutionCount(params.currentFlowNode?.id, params.processInstanceMetadata?.id); return flowNodeExecutionCount; } async getParametersForRuntimeExpression(expression, params) { const tokenParams = { token: await this.buildTokenHistoryForProcessInstance({ processInstanceId: params.processInstanceMetadata?.id, currentToken: params.token?.current, currentFlowNodeId: params.currentFlowNode?.id, previousFlowNodeId: params.previousFlowNodeId, }, expression), }; return this.mergeRuntimeExpressionParameters(params, tokenParams); } mergeRuntimeExpressionParameters(runtimeExpressionParametersA, runtimeExpressionParametersB) { const mergedTokenParameters = { token: { current: runtimeExpressionParametersA?.token?.current ?? runtimeExpressionParametersB?.token?.current, history: Object.assign({}, runtimeExpressionParametersB?.token?.history ?? {}, runtimeExpressionParametersA?.token?.history ?? {}), }, }; return Object.assign({}, runtimeExpressionParametersB ?? {}, runtimeExpressionParametersA ?? {}, mergedTokenParameters); } async buildTokenHistoryForProcessInstance(params, expression) { // Check if token history is needed based on expression const tokenHistoryRegex = /token[\s\n]*\.[\s\n]*history/; const tokenHistoryNeeded = expression && tokenHistoryRegex.test(expression); if (expression && !tokenHistoryNeeded) { return { current: params.currentToken ?? {}, history: {}, }; } const referencedFlowNodeIds = this.extractFlowNodesFromExpression(expression); // If no specific Flow Node IDs are referenced, retrieve all finished, error, or terminated nodes const queryFilter = { processInstanceId: params.processInstanceId, state: [processcube_engine_sdk_1.FlowNodeInstanceState.finished, processcube_engine_sdk_1.FlowNodeInstanceState.error, processcube_engine_sdk_1.FlowNodeInstanceState.terminated], }; if (referencedFlowNodeIds.length > 0) { queryFilter.flowNodeId = referencedFlowNodeIds; } const queryResult = await this.flowNodeInstanceDatabaseAdapter.query(queryFilter, 0, 0, { sortBy: processcube_engine_sdk_1.FlowNodeInstanceSortableColumns.createdAt, sortDir: 'ASC' }); const flowNodeInstances = queryResult.flowNodeInstances; if (flowNodeInstances.length === 0) { const dummyTokenHistory = { history: {}, current: params.currentToken, }; dummyTokenHistory.history[params.currentFlowNodeId] = params.currentToken; return dummyTokenHistory; } const flowNodeToUseAsCurrent = params.previousFlowNodeId && flowNodeInstances.some((entry) => entry.flowNodeId === params.previousFlowNodeId) ? flowNodeInstances.filter((entry) => entry.flowNodeId === params.previousFlowNodeId).pop() : flowNodeInstances.pop(); const lastResult = flowNodeToUseAsCurrent.endToken ?? {}; const tokenHistory = { history: {}, current: lastResult, }; for (const flowNodeInstance of flowNodeInstances) { const flowNodeInstanceResult = flowNodeInstance.endToken ?? {}; tokenHistory.history[flowNodeInstance.flowNodeId] = flowNodeInstanceResult; } tokenHistory.history[flowNodeToUseAsCurrent.flowNodeId] = lastResult; if (params.currentToken) { tokenHistory.current = params.currentToken; } if (params.currentFlowNodeId && params.currentToken) { tokenHistory.history[params.currentFlowNodeId] = params.currentToken; } return tokenHistory; } extractFlowNodesFromExpression(expression) { const flowNodePattern = /token\.history\.(\w+)\./g; const flowNodes = []; let match; while ((match = flowNodePattern.exec(expression)) !== null) { flowNodes.push(match[1]); } return flowNodes; } sanitizeFlowNodeExpression(tokenExpression) { const newlineOrSemicolonRegex = /[\n;]/; const trimmedTokenExpression = tokenExpression.trim(); const expressionLines = trimmedTokenExpression .split(newlineOrSemicolonRegex) .map((expressionLine) => expressionLine.trim()) .filter((expressionLine) => expressionLine !== ''); const isSingleLineExpression = expressionLines.length === 1; if (isSingleLineExpression) { const containsReturnOrThrowStatement = trimmedTokenExpression.startsWith('return ') || tokenExpression.startsWith('throw '); return containsReturnOrThrowStatement ? trimmedTokenExpression : `return ${trimmedTokenExpression}`; } const containsReturnOrThrowStatement = expressionLines.some((expressionLine) => expressionLine.startsWith('return ') || expressionLine.startsWith('throw ')); const endOfSelfInvokingFunctionRegex = /}\)();?/; const isSelfInvokingFunction = expressionLines.length > 0 && expressionLines[0].replace(/ /g, '').startsWith('(()=>{') && endOfSelfInvokingFunctionRegex.test(expressionLines[expressionLines.length - 1].replace(/ /g, '')); return !isSelfInvokingFunction && containsReturnOrThrowStatement ? trimmedTokenExpression : `return ${trimmedTokenExpression}`; } async executeTimeboxedRuntimeExpression(script, context) { const timeoutInMiliseconds = this.config.timeoutInMiliseconds < 100 ? 100 : this.config.timeoutInMiliseconds; const controller = new AbortController(); let timeout; const timeoutPromise = new Promise((_, reject) => { timeout = setTimeout(() => { reject(new processcube_engine_sdk_1.RequestTimeoutError(`Runtime Expression timed out after ${timeoutInMiliseconds}ms. This is an error with the expression itself. Ensure your Script does not contain blockers like infinity loops.`)); controller.abort(); }, timeoutInMiliseconds); }); const workerPromise = new Promise(async (resolve, reject) => { try { const result = await this.runtimeExpressionWorkers.run({ script: script, parameters: context, }, { signal: controller.signal }); resolve(result); } catch (error) { reject(error); } finally { clearTimeout(timeout); } }); return Promise.race([workerPromise, timeoutPromise]); } }; exports.ProcessTokenService = ProcessTokenService; exports.ProcessTokenService = ProcessTokenService = __decorate([ (0, inversify_1.injectable)(), __param(0, (0, inversify_1.inject)(IocRegistrations_1.IocRegistrationKeys.internal.FlowNodeInstanceDatabaseAdapter)), __metadata("design:paramtypes", [FlowNodeInstanceAdapter_1.FlowNodeInstanceDatabaseAdapter]) ], ProcessTokenService); //# sourceMappingURL=ProcessTokenService.js.map