@5minds/processcube_engine
Version:
The ProcessCube Engine. Stores and executes BPMNs.
292 lines • 15.7 kB
JavaScript
;
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