@5minds/processcube_engine
Version:
The ProcessCube Engine. Stores and executes BPMNs.
708 lines • 51.8 kB
JavaScript
"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.ExecuteProcessService = void 0;
const inversify_1 = require("inversify");
const processcube_engine_sdk_1 = require("@5minds/processcube_engine_sdk");
const index_1 = require("../Contracts/InternalMessages/index");
const IocRegistrations_1 = require("../Contracts/IocRegistrations");
const index_2 = require("../Tools/DatabaseAdaptersSequelize/index");
const EventAggregator_1 = __importDefault(require("../Tools/EventAggregator"));
const IamService_1 = require("../Tools/Iam/IamService");
const IdentityService_1 = require("../Tools/Iam/IdentityService");
const ProcessDefinitionMediator_1 = require("../Tools/ProcessDefinitionMediator");
let ExecuteProcessService = class ExecuteProcessService {
flowNodeInstanceDatabaseAdapter;
processDefinitionMediator;
processInstanceDatabaseAdapter;
processInstanceFactory;
iamService;
identityService;
logger;
constructor(flowNodeInstanceRepository, processDefinitionMediator, processInstanceRepository, processInstanceFactory, iamService, identityService) {
this.flowNodeInstanceDatabaseAdapter = flowNodeInstanceRepository;
this.logger = new processcube_engine_sdk_1.Logger('execute_process_service');
this.processDefinitionMediator = processDefinitionMediator;
this.processInstanceDatabaseAdapter = processInstanceRepository;
this.processInstanceFactory = processInstanceFactory;
this.iamService = iamService;
this.identityService = identityService;
}
async start(identity, options) {
return new Promise(async (resolve, reject) => {
try {
await this.validateStartRequest(identity, options);
const processInstance = this.processInstanceFactory({ processInstanceId: options.processInstanceId });
// We must wait for the "ProcessStartedNotification", or this Use Case might resolve, before the Process Instance was actually created.
const subscription = EventAggregator_1.default.subscribe(index_1.eventAggregatorSettings.messagePaths.processStarted, (message) => {
if (message.processInstanceId === processInstance.getProcessInstanceId()) {
EventAggregator_1.default.unsubscribe(subscription);
resolve(message);
}
});
// This UseCase is designed to resolve immediately after the ProcessInstance was started, so we must not await the execution here.
processInstance.execute(identity, options);
}
catch (error) {
this.logger.error('An error occured during process execution!', {
err: error,
});
reject(error);
}
});
}
async startAndAwaitEndEvent(identity, options) {
await this.validateStartRequest(identity, options);
const processInstance = this.processInstanceFactory({ processInstanceId: options.processInstanceId });
try {
const executionPromise = processInstance.execute(identity, options);
const eventSubscriptionPromise = this.waitForAnyEndEvent(processInstance);
const results = await Promise.all([executionPromise, eventSubscriptionPromise]);
return results[1];
}
catch (error) {
this.logger.error('An error occured during process execution!', {
err: error,
});
const isBpmnError = error.constructor?.name === processcube_engine_sdk_1.BpmnError.name;
// Errors from the error provider and ErrorEndEvents are thrown as they are.
// Everything else is thrown as an InternalServerError.
if (isBpmnError || (0, processcube_engine_sdk_1.isEngineError)(error)) {
throw error;
}
const errorToThrow = new processcube_engine_sdk_1.InternalServerError(error.message);
errorToThrow.additionalInformation = {
processInstanceId: processInstance.getProcessInstanceId(),
correlationId: processInstance.getCorrelationId(),
err: error,
};
throw errorToThrow;
}
}
async findAndResumeInterruptedProcessInstances() {
await this.flowNodeInstanceDatabaseAdapter.cleanupOrphanedFlowNodeInstances();
this.logger.info('Resuming unfinished ProcessInstances.');
const runningProcessInstances = await this.processInstanceDatabaseAdapter.getMinimalRunningProcessInstances();
if (runningProcessInstances.length === 0) {
this.logger.info(`No unfinished Process Instances found.`);
return;
}
this.logger.info(`Found a grand total of ${runningProcessInstances.length} running Process Instances.`);
const runningSubProcessInstances = runningProcessInstances.filter((processInstance) => processInstance.parentProcessInstanceId != undefined);
if (runningSubProcessInstances.length > 0) {
this.logger.info(`Skipping ${runningSubProcessInstances.length} Child Process Instances, which must be resumed through the Parent Process Instance.`);
await this.finishOrphanedChildProcessIntances(runningSubProcessInstances, runningProcessInstances);
}
const resumableProcessInstancIds = runningProcessInstances.filter((processInstance) => !processInstance.parentProcessInstanceId).map((instance) => instance.processInstanceId);
this.logger.info(`Found a grand total of ${resumableProcessInstancIds.length} resumable Process Instances.`);
const resumableProcessInstances = await this.getBatchOfRunningInstances(resumableProcessInstancIds, resumableProcessInstancIds.length, 250);
if (resumableProcessInstances.length === 0) {
this.logger.info('No resumable ProcessInstances found.');
return;
}
this.logger.debug(`Starting resuming instances NOW...`);
let resumedProcessInstanceCount = 0;
for (const resumableInstance of resumableProcessInstances) {
// Cannot await this, since all Process Instances need to be resumed simultaneously.
this.resumeProcessInstance(resumableInstance).catch((error) => this.logger.error(`Resuming Process instance ${resumableInstance.processInstanceId} encountered an error: ${error?.message}`, { err: error }));
resumedProcessInstanceCount++;
if (resumedProcessInstanceCount % 150 === 0) {
this.logger.info(`Resumed ${resumedProcessInstanceCount} of ${resumableProcessInstances.length} ProcessInstances.`);
// Small buffer to prevent the RAM from exploding when resuming large amounts of processes
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
this.logger.info(`Finished resuming ${resumedProcessInstanceCount} ProcessInstances.`);
}
async retryProcessInstance(identity, processInstanceId, flowNodeInstanceId, newStartToken, newProcessDefinitionHash) {
if (flowNodeInstanceId) {
this.logger.info(`Attempting to retry ProcessInstance ${processInstanceId} at FlowNodeInstance ${flowNodeInstanceId}`);
const flowNodeInstance = (await this.flowNodeInstanceDatabaseAdapter.query({ flowNodeInstanceId: flowNodeInstanceId })).flowNodeInstances[0];
if (!flowNodeInstance) {
throw new processcube_engine_sdk_1.BadRequestError(`FlowNodeInstance with ID \`${flowNodeInstanceId}\` not found!`);
}
}
else {
this.logger.info(`Attempting to retry ProcessInstance ${processInstanceId}`);
}
const processInstanceToRetry = (await this.processInstanceDatabaseAdapter.getProcessInstances([processInstanceId]))[0];
if (!processInstanceToRetry) {
throw new processcube_engine_sdk_1.NotFoundError(`ProcessInstance with ID \`${processInstanceId}\` not found.`);
}
const processInstanceNeedsMigration = newProcessDefinitionHash && newProcessDefinitionHash !== processInstanceToRetry.hash;
if (processInstanceNeedsMigration) {
await this.ensureProcessInstanceBpmnCanBeMigrated(identity, processInstanceToRetry, newProcessDefinitionHash, flowNodeInstanceId);
}
let rootProcessInstanceInTree = { ...processInstanceToRetry };
if (processInstanceToRetry.parentProcessInstanceId) {
rootProcessInstanceInTree = await this.getRootProcessOfProcessInstanceTree(processInstanceToRetry);
}
const childProcessInstances = flowNodeInstanceId
? await this.processInstanceDatabaseAdapter.getProcessInstances(await this.processInstanceDatabaseAdapter.getAllDescendantProcessInstanceIds(rootProcessInstanceInTree.processInstanceId))
: await this.getRetryableChildProcessInstances([rootProcessInstanceInTree.processInstanceId]);
const allProcessInstancesToReset = childProcessInstances.concat([rootProcessInstanceInTree]);
const processInstanceSets = await Promise.all(allProcessInstancesToReset.map(async (processInstanceToReset) => {
const queryResults = await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: processInstanceToReset.processInstanceId });
const flowNodeInstances = queryResults.flowNodeInstances;
return {
processInstance: processInstanceToReset,
flowNodeInstances: flowNodeInstances,
};
}));
const failedProcessInstanceSets = processInstanceSets.filter((processInstanceSet) => processInstanceSet.processInstance.state === processcube_engine_sdk_1.ProcessInstanceState.error || processInstanceSet.processInstance.state === processcube_engine_sdk_1.ProcessInstanceState.terminated);
for (const processInstanceSet of failedProcessInstanceSets) {
this.ensureProcessInstanceCanBeRestarted(processInstanceSet.processInstance);
}
if (flowNodeInstanceId) {
await this.resetProcessInstanceToFlowNodeInstance(flowNodeInstanceId, processInstanceId, processInstanceSets);
}
else {
await Promise.all(processInstanceSets.map(async (set) => {
return this.resetProcessInstance(set.processInstance, set.flowNodeInstances);
}));
}
if (processInstanceNeedsMigration) {
await this.migrateProcessInstanceBpmn(processInstanceToRetry, newProcessDefinitionHash);
if (rootProcessInstanceInTree.processInstanceId === processInstanceToRetry.processInstanceId) {
// This is necessary to ensure that the process instance will use the new XML when restarting.
// Only required when retrying a single instance, or when triggering the Retry at the top of of the Tree.
rootProcessInstanceInTree = (await this.processInstanceDatabaseAdapter.getProcessInstances([processInstanceId]))[0];
}
}
if (newStartToken) {
if (flowNodeInstanceId) {
await this.flowNodeInstanceDatabaseAdapter.updateStartToken([flowNodeInstanceId], newStartToken);
}
else {
const failedFlowNodeInstances = processInstanceSets.flatMap((pi) => pi.flowNodeInstances.filter((fni) => fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated));
const flowNodeInstancesToUpdateStartToken = failedFlowNodeInstances.filter((fni) => (fni.flowNodeType !== processcube_engine_sdk_1.BpmnType.callActivity || !childProcessInstances.some((pi) => pi.processInstanceId === fni.childProcessInstanceId)) &&
fni.flowNodeType !== processcube_engine_sdk_1.BpmnType.subProcess);
const flowNodeInstanceIds = flowNodeInstancesToUpdateStartToken.map((fni) => fni.flowNodeInstanceId);
await this.flowNodeInstanceDatabaseAdapter.updateStartToken(flowNodeInstanceIds, newStartToken);
}
}
rootProcessInstanceInTree.state = processcube_engine_sdk_1.ProcessInstanceState.running;
this.logger.info(`Resuming ProcessInstance with instance ID ${rootProcessInstanceInTree.processInstanceId} and Definition ID ${rootProcessInstanceInTree.processDefinitionId}`);
// Must not await this, or the API request will hang until the Process Instance finishes.
this.resumeProcessInstance(rootProcessInstanceInTree).catch((error) => this.logger.error(`Retrying Process instance ${rootProcessInstanceInTree.processInstanceId} encountered an error: ${error?.message}`, { err: error }));
}
/**
* This function is too strict. It verifies, that all possible paths towards `flowNodeInstanceId` have not been altered.
* It should only verify, that the actually executed path has not been altered.
* https://5minds.atlassian.net/browse/PCE-1474
*/
async ensureProcessInstanceBpmnCanBeMigrated(identity, processInstance, newProcessDefinitionHash, flowNodeInstanceId) {
const flowNodeInstanceToRestartAt = (await this.flowNodeInstanceDatabaseAdapter.query({
flowNodeInstanceId: flowNodeInstanceId ?? undefined,
processInstanceId: processInstance.processInstanceId,
}, 0, 1, {
sortBy: processcube_engine_sdk_1.FlowNodeInstanceSortableColumns.finishedAt,
sortDir: 'DESC',
})).flowNodeInstances[0];
if (!flowNodeInstanceToRestartAt) {
throw new processcube_engine_sdk_1.BadRequestError(`Process Instance with ID \`${processInstance.processInstanceId}\` does not contain a FlowNodeInstance with ID \`${flowNodeInstanceId}\`.`);
}
const newProcessDefinition = await this.processDefinitionMediator.getByHash(identity, newProcessDefinitionHash);
const newProcessModel = newProcessDefinition.processes.find((process) => process.id === processInstance.processModelId && process.isExecutable);
if (!newProcessModel) {
throw new processcube_engine_sdk_1.BadRequestError(`Process Model with ID \`${processInstance.processModelId}\` is no longer flagged as executable.`);
}
const newProcessModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(newProcessModel);
const processDefinition = await this.processDefinitionMediator.getByHash(identity, processInstance.hash);
const processModel = processDefinition.processes.find((process) => process.id === processInstance.processModelId);
const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(processModel);
const visitedFlowNodeIds = [];
function compareSubProcessFlows(flowNode, newFlowNode) {
const subProcessEndEvents = flowNode.flowNodes.filter((fn) => fn.bpmnType === processcube_engine_sdk_1.BpmnType.endEvent);
const newSubProcessEndEvents = newFlowNode.flowNodes.filter((fn) => fn.bpmnType === processcube_engine_sdk_1.BpmnType.endEvent);
if (subProcessEndEvents.length !== newSubProcessEndEvents.length) {
return false;
}
const subProcessIsEqual = subProcessEndEvents.every((endEvent) => {
const newEndEvent = newSubProcessEndEvents.find((ee) => ee.id === endEvent.id);
if (!newEndEvent) {
return false;
}
return compareFlows(endEvent, newEndEvent);
});
return subProcessIsEqual;
}
function compareFlows(flowNode, newFlowNode) {
if (visitedFlowNodeIds.includes(flowNode.id)) {
return true;
}
visitedFlowNodeIds.push(flowNode.id);
if (flowNode.id !== newFlowNode.id || flowNode.bpmnType !== newFlowNode.bpmnType) {
return false;
}
if (flowNode.dataOutputAssociations?.length !== newFlowNode.dataOutputAssociations?.length ||
flowNode.dataOutputAssociations?.some((doa) => newFlowNode.dataOutputAssociations.findIndex((ndoa) => ndoa.id === doa.id && ndoa.targetRef === doa.targetRef) == -1)) {
return false;
}
if (flowNode.bpmnType === processcube_engine_sdk_1.BpmnType.subProcess) {
const subProcessIsEqual = compareSubProcessFlows(flowNode, newFlowNode);
if (!subProcessIsEqual) {
return false;
}
}
const precedingFlowNodes = processModelFacade.getPreviousFlowNodesFor(flowNode, false, true) ?? [];
const newPrecedingFlowNodes = newProcessModelFacade.getPreviousFlowNodesFor(newFlowNode, false, true) ?? [];
if (precedingFlowNodes.length !== newPrecedingFlowNodes.length) {
return false;
}
const precedingFlowsAreEqual = precedingFlowNodes.every((flowNode) => {
const newFlowNode = newPrecedingFlowNodes.find((newFlowNode) => newFlowNode.id === flowNode.id);
if (newFlowNode) {
return compareFlows(flowNode, newFlowNode);
}
return false;
});
return precedingFlowsAreEqual;
}
const newFlowNodeToRestartAt = newProcessModelFacade.getFlowNodeById(flowNodeInstanceToRestartAt.flowNodeId, true);
if (!newFlowNodeToRestartAt) {
throw new processcube_engine_sdk_1.BadRequestError(`Process Definition with ID \`${processInstance.processDefinitionId}\` does not contain a FlowNode with ID \`${flowNodeInstanceToRestartAt.flowNodeId}\`.`);
}
const flowNodeToRestartAt = processModelFacade.getFlowNodeById(flowNodeInstanceToRestartAt.flowNodeId, true);
if (!compareFlows(flowNodeToRestartAt, newFlowNodeToRestartAt)) {
throw new processcube_engine_sdk_1.BadRequestError(`Can't update Process Model for Process Instance \`${flowNodeInstanceToRestartAt.processInstanceId}\`, because the path before Flow Node \`${flowNodeInstanceToRestartAt.flowNodeId}\` or some DataOutputAssociation has been altered.`);
}
}
async migrateProcessInstanceBpmn(processInstance, newProcessDefinitionHash) {
const rootProcessInstance = await this.getRootProcessOfProcessInstanceTree(processInstance, true);
const allEffectedProcessInstanceIds = await this.processInstanceDatabaseAdapter.getAllDescendantProcessInstanceIds(rootProcessInstance.processInstanceId, processcube_engine_sdk_1.BpmnType.subProcess);
allEffectedProcessInstanceIds.push(rootProcessInstance.processInstanceId);
await Promise.all(allEffectedProcessInstanceIds.map((pi) => this.processInstanceDatabaseAdapter.changeProcessDefinition(pi, newProcessDefinitionHash)));
const newProcessDefinition = await this.processDefinitionMediator.getByHash(this.identityService.getInternalIdentity(), newProcessDefinitionHash);
const newProcessModel = newProcessDefinition.processes.find((process) => process.id === processInstance.processModelId && process.isExecutable);
const newProcessModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(newProcessModel);
const executedFlowNodes = await this.flowNodeInstanceDatabaseAdapter.getFlowNodeInstancesForProcessInstanceIds(allEffectedProcessInstanceIds);
const flowNodesToUpdate = newProcessModel
.getAllFlowNodes()
.map((flowNode) => {
return {
...flowNode,
lane: newProcessModelFacade.getLaneForFlowNode(flowNode.id)?.name ?? null,
};
})
.filter((flowNode) => {
const executedFlowNode = executedFlowNodes.find((executedFlowNode) => executedFlowNode.flowNodeId === flowNode.id);
return executedFlowNode != null && (executedFlowNode.flowNodeName != flowNode.name || executedFlowNode.flowNodeLane != flowNode.lane);
});
await this.flowNodeInstanceDatabaseAdapter.updateFlowNodesInProcessInstance(allEffectedProcessInstanceIds, flowNodesToUpdate);
}
async validateStartRequest(requestingIdentity, options) {
if (!options?.processModelId) {
throw new processcube_engine_sdk_1.BadRequestError('Must provide a value for options.processModelId!');
}
const processDefinition = await this.processDefinitionMediator.getByProcessModelId(requestingIdentity, options.processModelId);
const processModel = processDefinition?.processes?.find((process) => process.id === options.processModelId);
if (!processModel) {
throw new processcube_engine_sdk_1.NotFoundError(`ProcessModel \`${options.processModelId}\` not found.`);
}
if (processModel.dataObjectReferences.length > 0) {
processModel.dataObjectReferences.forEach((dataObjectReference) => {
if (dataObjectReference.initialValue == undefined) {
return;
}
let initialValue;
try {
initialValue = JSON.parse(dataObjectReference.initialValue);
}
catch { }
const resultIsAnObject = initialValue != undefined && typeof initialValue === 'object' && (initialValue.serialize || initialValue.toString() === '[object Object]' || Array.isArray(initialValue));
if (!resultIsAnObject) {
throw new processcube_engine_sdk_1.BadRequestError(`The given initial value for data object ${dataObjectReference.id} is not a valid JSON.`);
}
});
}
const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(processModel);
const startEvents = processModelFacade.getStartEvents(false);
const endEvents = processModelFacade.getEndEvents(false);
if (startEvents.length > 1 && !options.startEventId) {
throw new processcube_engine_sdk_1.BadRequestError('Must select a StartEvent with which to start the Process.');
}
if (options.startEventId != undefined) {
const matchingStartEvent = startEvents.find((flowNode) => flowNode.id === options.startEventId);
if (!matchingStartEvent) {
throw new processcube_engine_sdk_1.NotFoundError(`StartEvent with ID \`${options.startEventId}\` not found.`);
}
}
const startEventToUse = options.startEventId ? processModelFacade.getStartEventById(options.startEventId) : processModelFacade.getSingleStartEvent();
const userIsAdmin = this.iamService.checkIfUserIsSuperAdmin(requestingIdentity);
const userIsObserver = this.iamService.checkIfUserIsObserver(requestingIdentity);
const flowNodeLane = processModelFacade.getLaneForFlowNode(startEventToUse.id)?.name;
if (!userIsAdmin && userIsObserver && flowNodeLane && !this.iamService.checkIfIdentityHasClaim(requestingIdentity, flowNodeLane)) {
const forbiddenError = new processcube_engine_sdk_1.ForbiddenError(`Observers are not allowed to start processes without the corresponding lane claim.`);
forbiddenError.additionalInformation = {
processModelId: options.processModelId,
startEventId: startEventToUse.id,
requestingUserId: requestingIdentity.userId,
requestingUserEmail: requestingIdentity.userEmail,
requestingUserName: requestingIdentity.userName,
};
throw forbiddenError;
}
if (options.endEventId) {
const noMatchingEndEvent = !endEvents.some((flowNode) => flowNode.id === options.endEventId);
if (noMatchingEndEvent) {
throw new processcube_engine_sdk_1.NotFoundError(`EndEvent with ID \`${options.endEventId}\` not found.`);
}
}
if (!processModel.isExecutable) {
throw new processcube_engine_sdk_1.BadRequestError(`The process model \`${processModel.id}\` is not executable.`);
}
if (processModel.isSingleton) {
const singletonProcessInstances = await this.processInstanceDatabaseAdapter.countRunningInstancesForModel(processModel.id);
if (singletonProcessInstances > 0) {
throw new processcube_engine_sdk_1.BadRequestError(`The ProcessModel \`${options.processModelId}\` is a Singleton and already running in instance \`${singletonProcessInstances[0]}\``);
}
}
}
async waitForAnyEndEvent(processInstance) {
return new Promise((resolve) => processInstance.onProcessEnded(resolve));
}
async getBatchOfRunningInstances(resumableInstanceIds, totalCount, chunkSize = 250) {
const processInstanceIdChunk = resumableInstanceIds.splice(0, chunkSize);
const loadedProcessInstances = await this.processInstanceDatabaseAdapter.getProcessInstances(processInstanceIdChunk);
if (resumableInstanceIds.length > 0) {
return loadedProcessInstances.concat(await this.getBatchOfRunningInstances(resumableInstanceIds, totalCount, chunkSize));
}
else {
return loadedProcessInstances;
}
}
async getRootProcessOfProcessInstanceTree(processInstance, onlyEmbedded = false) {
if (!processInstance.parentProcessInstanceId || (onlyEmbedded && !processInstance.embeddedProcessModelId)) {
return processInstance;
}
const parentProcessInstances = await this.processInstanceDatabaseAdapter.getProcessInstances([processInstance.parentProcessInstanceId]);
if (parentProcessInstances?.length === 0) {
throw new processcube_engine_sdk_1.NotFoundError(`Parent ProcessInstance \`${processInstance.parentProcessInstanceId}\` of child \`${processInstance.processInstanceId}\` not found.`);
}
const parentProcessInstance = parentProcessInstances[0];
return this.getRootProcessOfProcessInstanceTree(parentProcessInstance, onlyEmbedded);
}
async getRetryableChildProcessInstances(parentProcessInstanceIds) {
const childProcessInstances = await this.processInstanceDatabaseAdapter.getRetryableChildProcessInstances(parentProcessInstanceIds);
if (childProcessInstances.length === 0) {
return [];
}
const childIds = childProcessInstances.map((instance) => instance.processInstanceId);
const childResults = await this.getRetryableChildProcessInstances(childIds);
return childProcessInstances.concat(childResults);
}
async resetProcessInstanceToFlowNodeInstance(flowNodeInstanceId, processInstanceId, processInstanceSets) {
const processInstanceSetToReset = processInstanceSets.find((piSet) => piSet.processInstance.processInstanceId === processInstanceId);
const processInstance = processInstanceSetToReset.processInstance;
const flowNodeInstances = processInstanceSetToReset.flowNodeInstances;
const flowNodeInstance = flowNodeInstances.find((fniEntry) => fniEntry.flowNodeInstanceId === flowNodeInstanceId);
if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.parallelGateway ||
flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.inclusiveGateway ||
flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.exclusiveGateway ||
flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.eventBasedGateway ||
flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.complexGateway ||
flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.boundaryEvent) {
throw new processcube_engine_sdk_1.BadRequestError(`Resetting a ProcessInstance to a ${flowNodeInstance.flowNodeType.replace('bpmn:', '')} is currently not supported.`);
}
const bpmnModelParser = new processcube_engine_sdk_1.BpmnModelParser();
const processDefinition = await bpmnModelParser.parseXmlToObjectModel(processInstance.xml);
const process = processDefinition.processes.find((processEntry) => processEntry.id === flowNodeInstance.processModelId);
const previousParallelGateways = this.getPreviousParallelizingGateways(flowNodeInstanceId, flowNodeInstances);
const isRunningInParallel = this.isFlowNodeInParallizedBranch(process, previousParallelGateways);
if (isRunningInParallel) {
throw new processcube_engine_sdk_1.BadRequestError(`Resetting to Flow Node Instances running in parallel branches is currently not supported.`);
}
const flowNodeInstancesToDelete = await this.getFlowNodeInstancesAfterFlowNodeInstance(flowNodeInstance, flowNodeInstances, process);
const processInstanceIdsToDelete = this.getAllChildProcessIds(flowNodeInstancesToDelete, processInstanceSets);
const setsToCompletelyReset = this.getNonDescendantProcessInstanceSets(processInstanceSetToReset, processInstanceSets);
if (setsToCompletelyReset.length > 0) {
await Promise.all(setsToCompletelyReset.map(async (set) => {
return this.resetProcessInstance(set.processInstance, set.flowNodeInstances);
}));
}
if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.callActivity) {
processInstanceIdsToDelete.push(flowNodeInstance.childProcessInstanceId);
}
else if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess) {
processInstanceIdsToDelete.push(flowNodeInstance.childProcessInstanceId);
}
const flowNodeInstanceIdsToReset = [];
if (flowNodeInstance.flowNodeInstanceId === flowNodeInstanceId && flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess) {
flowNodeInstanceIdsToReset.push({ flowNodeInstanceId: flowNodeInstance.flowNodeInstanceId, isUnstartedSubProcess: true });
}
else {
flowNodeInstanceIdsToReset.push(flowNodeInstance.flowNodeInstanceId);
}
flowNodeInstancesToDelete.push(...this.getBoundaryEventsForFlowNodeInstances([...flowNodeInstancesToDelete, flowNodeInstance], flowNodeInstances));
const flowNodeInstanceIdsToDelete = flowNodeInstancesToDelete.map((fni) => fni.flowNodeInstanceId);
this.logger.debug(`Resetting Process Instance ${processInstanceId} will delete the following Flow Node Instances:`, { flowNodeInstancesToDelete: flowNodeInstanceIdsToDelete });
await this.processInstanceDatabaseAdapter.reset(processInstance.processInstanceId, flowNodeInstanceIdsToReset, flowNodeInstanceIdsToDelete, processInstanceIdsToDelete);
}
getNonDescendantProcessInstanceSets(processInstanceSet, processInstanceSets) {
const processInstanceIdBlacklist = [processInstanceSet.processInstance.processInstanceId];
let nonDescendantSets = [...processInstanceSets];
let nonDescendantSetsLength = 0;
while (nonDescendantSetsLength !== nonDescendantSets.length) {
nonDescendantSetsLength = nonDescendantSets.length;
nonDescendantSets = nonDescendantSets.filter((set) => {
if (processInstanceIdBlacklist.includes(set.processInstance.processInstanceId)) {
return false;
}
else if (processInstanceIdBlacklist.includes(set.processInstance.parentProcessInstanceId)) {
processInstanceIdBlacklist.push(set.processInstance.processInstanceId);
return false;
}
return true;
});
}
return nonDescendantSets;
}
getAllChildProcessIds(flowNodeInstances, processInstanceSets) {
const processInstanceIds = flowNodeInstances
.filter((fni) => fni.flowNodeType === processcube_engine_sdk_1.BpmnType.callActivity || fni.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess)
.map((fni) => fni.childProcessInstanceId);
const processInstanceSetsForChildProcesses = processInstanceSets.filter((processInstanceSet) => processInstanceIds.includes(processInstanceSet.processInstance.processInstanceId));
for (const processInstanceSet of processInstanceSetsForChildProcesses) {
const childProcessIds = this.getAllChildProcessIds(processInstanceSet.flowNodeInstances, processInstanceSets);
processInstanceIds.push(...childProcessIds);
}
return processInstanceIds;
}
isFlowNodeInParallizedBranch(process, gatewayInstances) {
const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(process);
const parallelGatewayModels = process.getFlowNodesByType(processcube_engine_sdk_1.BpmnType.parallelGateway);
const inclusiveGatewayModels = process.getFlowNodesByType(processcube_engine_sdk_1.BpmnType.inclusiveGateway);
const gatewayModels = parallelGatewayModels.concat(inclusiveGatewayModels);
const usedGatewayModels = gatewayModels.filter((gw) => gatewayInstances.some((gwi) => gwi.flowNodeId === gw.id));
const splitGateways = usedGatewayModels.filter((gw) => gw.gatewayDirection === processcube_engine_sdk_1.Model.Gateways.GatewayDirection.Diverging);
const splitGatewayInstances = gatewayInstances.filter((gwi) => splitGateways.some((gw) => gwi.flowNodeId === gw.id));
for (const splitGatewayInstance of splitGatewayInstances) {
const splitGatewayModel = splitGateways.find((gw) => gw.id === splitGatewayInstance.flowNodeId);
const joinGateway = processModelFacade.findJoinGatewayAfterSplitGateway(splitGatewayModel, splitGatewayModel.bpmnType);
if (!joinGateway) {
return true;
}
const joinGatewayInstance = gatewayInstances.find((gwi) => gwi.flowNodeId === joinGateway.id);
if (!joinGatewayInstance || joinGatewayInstance.state !== processcube_engine_sdk_1.FlowNodeInstanceState.finished) {
return true;
}
}
return false;
}
getPreviousParallelizingGateways(flowNodeInstanceId, allFlowNodeInstances) {
const flowNodeInstance = allFlowNodeInstances.find((fni) => fni.flowNodeInstanceId === flowNodeInstanceId);
if (!flowNodeInstance.previousFlowNodeInstanceId) {
return [];
}
const gateways = [];
if (flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.parallelGateway || flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.inclusiveGateway) {
gateways.push(flowNodeInstance);
}
const previousFlowNodeInstanceIds = flowNodeInstance.previousFlowNodeInstanceId.split(';');
for (const previousFlowNodeInstanceId of previousFlowNodeInstanceIds) {
gateways.push(...this.getPreviousParallelizingGateways(previousFlowNodeInstanceId, allFlowNodeInstances));
}
return gateways;
}
ensureProcessInstanceCanBeRestarted(processInstance) {
if (processInstance.state !== processcube_engine_sdk_1.ProcessInstanceState.error && processInstance.state !== processcube_engine_sdk_1.ProcessInstanceState.terminated) {
const errorMessage = `ProcessInstance with ID ${processInstance.processInstanceId} can not be restarted, because it is not in an 'error' or 'terminated' state`;
const error = new processcube_engine_sdk_1.BadRequestError(errorMessage);
this.logger.error(errorMessage, {
err: error,
});
throw error;
}
}
async getFlowNodeInstancesAfterFlowNodeInstance(flowNodeInstance, allFlowNodeInstances, processModel) {
const flowNodeInstancesAfterFlowNodeInstance = allFlowNodeInstances.filter((fni) => {
if (fni.previousFlowNodeInstanceId === flowNodeInstance.flowNodeInstanceId) {
return true;
}
if (fni.previousFlowNodeInstanceId?.includes(';')) {
return fni.previousFlowNodeInstanceId.split(';').some((previousFlowNodeInstanceId) => previousFlowNodeInstanceId === flowNodeInstance.flowNodeInstanceId);
}
return false;
});
const flowNodeInstanceIsSubProcess = flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess;
if (flowNodeInstancesAfterFlowNodeInstance.length === 0 && !flowNodeInstanceIsSubProcess) {
return [];
}
const flowNodeInstancesToReset = [...flowNodeInstancesAfterFlowNodeInstance];
for (const flowNodeInstanceEntry of flowNodeInstancesAfterFlowNodeInstance) {
const flowNodeInstanceAfterCurrentFlowNodeInstance = await this.getFlowNodeInstancesAfterFlowNodeInstance(flowNodeInstanceEntry, allFlowNodeInstances, processModel);
flowNodeInstancesToReset.push(...flowNodeInstanceAfterCurrentFlowNodeInstance);
}
const subProcessInstancesToReset = flowNodeInstancesAfterFlowNodeInstance.filter((fni) => fni.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess && (fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated || fni.state === processcube_engine_sdk_1.FlowNodeInstanceState.error));
if (flowNodeInstanceIsSubProcess) {
subProcessInstancesToReset.push(flowNodeInstance);
}
if (subProcessInstancesToReset.length > 0) {
const followingFlowNodeInstancesForSubProcess = await this.getFollowingFlowNodeInstancesForSubProcesses(subProcessInstancesToReset, processModel);
flowNodeInstancesToReset.push(...followingFlowNodeInstancesForSubProcess);
}
return flowNodeInstancesToReset;
}
async getFollowingFlowNodeInstancesForSubProcesses(subProcessInstances, processModel) {
const childFlowNodeInstances = (await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: subProcessInstances.map((spi) => spi.childProcessInstanceId) })).flowNodeInstances;
const processModelFacade = new processcube_engine_sdk_1.ProcessModelFacade(processModel);
return (await Promise.all(subProcessInstances.map((spi) => this.getFollowingFlowNodeInstancesForSubProcess(spi, childFlowNodeInstances, processModelFacade, processModel)))).flat();
}
async getFollowingFlowNodeInstancesForSubProcess(subProcessInstance, childFlowNodeInstances, processModelFacade, processModel) {
const subProcessActivity = processModelFacade.getFlowNodeById(subProcessInstance.flowNodeId, true);
const startEventIds = subProcessActivity.flowNodes.filter((fn) => fn.bpmnType === processcube_engine_sdk_1.BpmnType.startEvent).map((se) => se.id);
const startEventInstancesForSubProcessModel = childFlowNodeInstances.filter((fni) => startEventIds.includes(fni.flowNodeId));
let startEventInstance;
if (startEventInstancesForSubProcessModel.length > 1) {
startEventInstance = startEventInstancesForSubProcessModel.sort(function (a, b) {
const distanceA = Math.abs(subProcessInstance.startedAt.getTime() - a.startedAt.getTime());
const distanceB = Math.abs(subProcessInstance.startedAt.getTime() - b.startedAt.getTime());
return distanceA - distanceB;
})[0];
}
else if (startEventInstancesForSubProcessModel.length === 1) {
startEventInstance = startEventInstancesForSubProcessModel[0];
}
const followingFlowNodeInstancesForSubProcess = await this.getFlowNodeInstancesAfterFlowNodeInstance(startEventInstance, childFlowNodeInstances, processModel);
followingFlowNodeInstancesForSubProcess.push(startEventInstance);
return followingFlowNodeInstancesForSubProcess;
}
async resetProcessInstance(processInstance, flowNodeInstances) {
const flowNodeInstancesToReset = await this.getFlowNodeInstancesToReset(flowNodeInstances, processInstance.processInstanceId);
const flowNodeInstanceIdsToReset = flowNodeInstancesToReset.map((flowNodeInstance) => flowNodeInstance.flowNodeInstanceId);
await this.processInstanceDatabaseAdapter.reset(processInstance.processInstanceId, flowNodeInstanceIdsToReset);
}
async getFlowNodeInstancesToReset(flowNodeInstances, processInstanceId) {
const erroredFlowNodeInstances = flowNodeInstances.filter((flowNodeInstance) => flowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || flowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated);
const flowNodeInstancesToReset = erroredFlowNodeInstances.filter((flowNodeInstance) => this.shouldResetFlowNode(flowNodeInstance, flowNodeInstances));
const failedSubProcesses = flowNodeInstancesToReset.filter((flowNodeInstance) => flowNodeInstance.flowNodeType === processcube_engine_sdk_1.BpmnType.subProcess);
if (failedSubProcesses.length > 0) {
flowNodeInstancesToReset.push(...(await this.getFlowNodeInstanceIdsInSubProcesses(processInstanceId)));
}
flowNodeInstancesToReset.push(...this.getBoundaryEventsForFlowNodeInstances(flowNodeInstancesToReset, flowNodeInstances));
return flowNodeInstancesToReset;
}
async getFlowNodeInstanceIdsInSubProcesses(processInstanceId) {
const failedFlowNodeInstancesInFailedSubProcesses = await this.flowNodeInstanceDatabaseAdapter.query({
parentProcessInstanceId: processInstanceId,
state: [processcube_engine_sdk_1.FlowNodeInstanceState.error, processcube_engine_sdk_1.FlowNodeInstanceState.terminated],
});
return failedFlowNodeInstancesInFailedSubProcesses.flowNodeInstances;
}
getBoundaryEventsForFlowNodeInstances(flowNodeInstances, allFlowNodeInstances) {
const boundaryEventsForFlowNodeInstances = flowNodeInstances.flatMap((flowNodeInstance) => {
return allFlowNodeInstances.filter((possibleBoundaryEvent) => {
return possibleBoundaryEvent.flowNodeType === processcube_engine_sdk_1.BpmnType.boundaryEvent && possibleBoundaryEvent.previousFlowNodeInstanceId === flowNodeInstance.multiInstanceMetadataId;
});
});
return boundaryEventsForFlowNodeInstances;
}
shouldResetFlowNode(flowNodeInstanceToReset, flowNodeInstances) {
const errorBoundaryEventInstances = this.getErrorBoundaryEventInstancesForFlowNodeInstance(flowNodeInstanceToReset, flowNodeInstances);
const errorWasHandledByBoundaryEvent = errorBoundaryEventInstances.some((boundaryEventInstance) => {
return boundaryEventInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.finished;
});
return !errorWasHandledByBoundaryEvent;
}
getErrorBoundaryEventInstancesForFlowNodeInstance(flowNodeInstance, allFlowNodeInstances) {
return allFlowNodeInstances.filter((possibleBoundaryEvent) => {
return (possibleBoundaryEvent.eventType === processcube_engine_sdk_1.EventType.errorEvent &&
possibleBoundaryEvent.flowNodeType === processcube_engine_sdk_1.BpmnType.boundaryEvent &&
possibleBoundaryEvent.previousFlowNodeInstanceId === flowNodeInstance.flowNodeInstanceId);
});
}
async resumeProcessInstance(processInstance) {
const flowNodeInstances = (await this.flowNodeInstanceDatabaseAdapter.query({ processInstanceId: processInstance.processInstanceId })).flowNodeInstances;
const hasActiveFlowNodeInstances = flowNodeInstances.some((entry) => {
return entry.state === processcube_engine_sdk_1.FlowNodeInstanceState.running || entry.state === processcube_engine_sdk_1.FlowNodeInstanceState.suspended;
});
const lastFlowNodeInstance = flowNodeInstances.sort((a, b) => {
if (a.startedAt && b.startedAt) {
return a.startedAt > b.startedAt ? -1 : b.startedAt > a.startedAt ? 1 : 0;
}
if (a.startedAt == b.startedAt) {
return b.flowNodeInstanceId.localeCompare(a.flowNodeInstanceId);
}
})[0];
const lastInstanceHasFailed = lastFlowNodeInstance != null && (lastFlowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || lastFlowNodeInstance.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated);
const hasReachedAnEndEvent = flowNodeInstances.some((entry) => entry.flowNodeType === processcube_engine_sdk_1.BpmnType.endEvent);
const processInstanceIsOrphaned = (!hasActiveFlowNodeInstances && hasReachedAnEndEvent) || lastInstanceHasFailed;
if (processInstanceIsOrphaned) {
this.logger.warn(`ProcessInstance ${processInstance.processInstanceId} is not active anymore. It is likely something went wrong during final state transition.`);
this.logger.warn(`Setting orphaned ProcessInstance ${processInstance.processInstanceId} state to "finished", so it won't show up again.`);
return this.finishOrphanedProcessInstance(flowNodeInstances, processInstance);
}
try {
const newProcessInstance = this.processInstanceFactory({ processInstanceId: processInstance.processInstanceId });
const ownerIdentity = processInstance.startedByRootAccessToken
? this.identityService.getRootAccessIdentity()
: await this.identityService.getIdentity({
token: processInstance.ownerToken,
userId: processInstance.ownerId,
skipValidation: true,
});
await newProcessInstance.resume(ownerIdentity, processInstance, flowNodeInstances);
}
catch (error) {
// Errors from our error provider and ErrorEndEvents are thrown as they are.
// Everything else is thrown as an InternalServerError.
const isPresetError = error.code && error.name;
if (isPresetError) {
throw error;
}
else {
throw new processcube_engine_sdk_1.InternalServerError(error.message);
}
}
}
async finishOrphanedChildProcessIntances(runningSubProcessInstances, runningProcessInstances) {
this.logger.info('Checking for orphaned Child Process Instances...');
for (const subProcessInstance of runningSubProcessInstances) {
const parentIsActive = runningProcessInstances.some((processInstance) => processInstance.processInstanceId === subProcessInstance.parentProcessInstanceId);
if (parentIsActive) {
continue;
}
this.logger.warn(`Parent of running ProcessInstance ${subProcessInstance.processInstanceId} has finished. It is likely something went wrong during final state transition.`);
this.logger.warn(`Setting orphaned Child ProcessInstance ${subProcessInstance.processInstanceId} state to "terminated", so it won't show up again.`);
const terminationError = new processcube_engine_sdk_1.BadRequestError(`Process was terminated, because its parent is no longer active.`);
terminationError.additionalInformation = {
processInstanceWasTerminated: true,
};
await this.processInstanceDatabaseAdapter.terminate(subProcessInstance.processInstanceId, terminationError, undefined, true);
}
}
async finishOrphanedProcessInstance(flowNodeInstances, processInstance) {
const finalFlowNodeInstance = this.getFinalFlowNodeInstanceForOrphanedProcessInstance(flowNodeInstances);
const finalToken = finalFlowNodeInstance?.endToken ?? {};
const processFinishedWithError = finalFlowNodeInstance?.state === processcube_engine_sdk_1.FlowNodeInstanceState.error || finalFlowNodeInstance?.state === processcube_engine_sdk_1.FlowNodeInstanceState.terminated;
if (processFinishedWithError) {
const errorToUse = finalFlowNodeInstance.error ?? new processcube_engine_sdk_1.BadRequestError('Process was terminated.');
errorToUse.additionalInformation = {
processInstanceWasTerminated: true,
};
await this.processInstanceDatabaseAdapter.finishWithError(processInstance.processInstanceId, errorToUse, undefined, true);
}
else {
await this.processInstanceDatabaseAdapter.finish(processInstance.processInstanceId, {
endEventId: finalFlowNodeInstance?.flowNodeId,
endEventType: finalFlowNodeInstance?.eventType,
endEventToken: finalToken,
}, true);
}
}
getFinalFlowNodeInstanceFor