UNPKG

@5minds/processcube_engine

Version:

The ProcessCube Engine. Stores and executes BPMNs.

580 lines • 29.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); 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 __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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.ProcessDefinitionDatabaseAdapter = void 0; const crypto = __importStar(require("crypto")); const dayjs_1 = __importDefault(require("dayjs")); const inversify_1 = require("inversify"); const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep")); const sequelize_1 = require("sequelize"); const processcube_engine_sdk_1 = require("@5minds/processcube_engine_sdk"); const index_1 = require("../../Contracts/InternalMessages/index"); const IocRegistrations_1 = require("../../Contracts/IocRegistrations"); const EventAggregator_1 = __importDefault(require("../EventAggregator")); const EventMiddlewareHandler_1 = require("../EventMiddlewareHandler"); const BaseDatabaseAdapter_1 = require("./BaseDatabaseAdapter"); const SearchQueryBuilder_1 = require("./Lib/SearchQueryBuilder"); const SequelizeConnectionManager_1 = require("./Lib/SequelizeConnectionManager"); const Models = __importStar(require("./Models/index")); const index_2 = require("./index"); let staticLogger; let ProcessDefinitionDatabaseAdapter = class ProcessDefinitionDatabaseAdapter extends BaseDatabaseAdapter_1.DatabaseAdapter { eventMiddlewareHandler; constructor(eventMiddlewareHandler) { super(); this.eventMiddlewareHandler = eventMiddlewareHandler; } async initialize() { await super.initialize('database_adapter:process_definitions'); staticLogger = this.logger; } async persistProcessDefinition(identity, definition, overwriteExisting = true) { const definitionsToSave = Array.isArray(definition) ? definition : [definition]; const definitionIds = []; let allProcessModelIdsWithDefinitionId = []; const cronjobsByDefinition = {}; for (const definitionToSave of definitionsToSave) { const processModelIds = definitionToSave.processes.map((process) => process.id); const cronjobs = definitionToSave.processes .map((process) => process .getStartEvents() .filter((flowNode) => flowNode.eventType === processcube_engine_sdk_1.EventType.timerEvent && flowNode.timerEventDefinition?.timerType === processcube_engine_sdk_1.Model.Events.TimerType.timeCycle) .map((flowNode) => { return { processDefinitionId: definitionToSave.id, processModelId: process.id, startEventId: flowNode.id, enabled: flowNode.timerEventDefinition?.enabled, crontab: flowNode.timerEventDefinition?.value, }; })) .filter((cronjob) => cronjob.length > 0) .flat(); cronjobsByDefinition[definitionToSave.id] = cronjobs; await this.ensureProcessModelIdsAreUniqe(definitionToSave.id, processModelIds); const processDefinitionHash = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => this.storeProcessDefinition(identity, definitionToSave.id, processModelIds, definitionToSave.raw, overwriteExisting), 'persist process definition'); allProcessModelIdsWithDefinitionId = allProcessModelIdsWithDefinitionId.concat(processModelIds.map((processModelId) => { return { processModelId: processModelId, processModelName: definitionToSave.processes.find((process) => process.id === processModelId)?.name, definitionId: definitionToSave.id, hash: processDefinitionHash, }; })); definitionIds.push(definitionToSave.id); } const deploymentMessage = { processDefinitionId: definitionIds, cronjobs: cronjobsByDefinition, }; await new Promise((resolve) => EventAggregator_1.default.publish(index_1.eventAggregatorSettings.messagePaths.processDeployed, deploymentMessage, resolve)); for (const processModelIdWithDefinitionId of allProcessModelIdsWithDefinitionId) { await this.eventMiddlewareHandler.triggerEvent((0, lodash_clonedeep_1.default)({ eventType: processcube_engine_sdk_1.EngineEventType.OnProcessDeployed, timestamp: dayjs_1.default.utc().toDate(), processDefinitionId: processModelIdWithDefinitionId.definitionId, processDefinitionHash: processModelIdWithDefinitionId.hash, processModelId: processModelIdWithDefinitionId.processModelId, processModelName: processModelIdWithDefinitionId.processModelName, processInstanceId: '', correlationId: '', logLevel: processcube_engine_sdk_1.LogLevel.info, })); } } async getAllProcessDefinitions(offset = 0, limit = 0, includeCount = true, excludeDeleted = true) { const excludeDeletedCondition = excludeDeleted ? { deleted: false } : {}; // NOTE: Running 2 queries is actually more efficient here, because it takes full advantage of table indizes - which are not available for Joins. const rawDefinitions = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionModel.findAll({ where: { ...excludeDeletedCondition, updatedAt: { [sequelize_1.Op.eq]: sequelize_1.Sequelize.literal((0, SearchQueryBuilder_1.getProcessDefinitionUpdatedAtMaxFilter)(this.sequelizeInstance.getDialect(), this.config.schema)), }, }, ...(0, SearchQueryBuilder_1.buildPagination)(offset, limit), order: [['name', 'DESC']], }), 'get all process definitions'); const processModelAssociations = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionProcessModelAssociationModel.findAll({ where: { ...excludeDeletedCondition, }, }), 'get all process model associations'); for (const processDefinition of rawDefinitions) { processDefinition.processModelAssociations = processModelAssociations.filter((association) => association.processDefinitionId === processDefinition.name); } const processDefinitions = rawDefinitions.map(this.convertToProcessDefinitionRuntimeObject.bind(this)); return { processDefinitions: processDefinitions, totalCount: includeCount ? rawDefinitions.length : undefined, }; } async getByName(name, excludeDeleted = true) { const deleted = excludeDeleted ? { deleted: false } : {}; // Note: // For this use case, we only want to get the most up to date version of the process definition. const query = { limit: 1, where: { name: name, ...deleted, }, order: [['updatedAt', 'DESC']], include: [ { model: Models.ProcessDefinitionProcessModelAssociationModel, as: 'processModelAssociations', required: true, where: { processDefinitionId: name, }, }, ], }; const definitions = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionModel.findAll(query), `get process definition by name ${name}`); if (!definitions || definitions.length === 0) { throw new processcube_engine_sdk_1.NotFoundError(`Process definition with name \`${name}\` not found.`); } const definitonRuntime = this.convertToProcessDefinitionRuntimeObject(definitions[0]); return definitonRuntime; } async getByHash(hash, includeDeleted = false) { const whereClause = { hash: hash }; if (!includeDeleted) { whereClause.deleted = false; } const query = { where: whereClause, include: [ { model: Models.ProcessDefinitionProcessModelAssociationModel, as: 'processModelAssociations', required: true, }, ], }; const definition = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionModel.findOne(query), `get process definition by hash '${hash}'`); if (!definition) { throw new processcube_engine_sdk_1.NotFoundError(`Process definition with hash \`${hash}\` not found.`); } const definitonRuntime = this.convertToProcessDefinitionRuntimeObject(definition); return definitonRuntime; } async getByProcessModelId(processModelId) { if (!processModelId) { throw new processcube_engine_sdk_1.BadRequestError(`No processModelId provided to ProcessDefinitionAdapter.getByProcessModelId!`); } const notFoundError = new processcube_engine_sdk_1.NotFoundError(`Process definition for ProcessModel \`${processModelId}\` not found!`); const processDefinitionId = await this.getProcessDefinitionIdForProcessModelId(processModelId); if (!processDefinitionId) { throw notFoundError; } const definition = await this.getByName(processDefinitionId); if (!definition) { throw notFoundError; } return definition; } async getDefinitionsWithRunningInstances(processModelIds) { const whereClause = { state: processcube_engine_sdk_1.ProcessInstanceState.running, }; if (processModelIds) { const idsAsArray = Array.isArray(processModelIds) ? processModelIds : [processModelIds]; const sanitizedProcessModelIds = idsAsArray.filter((id) => id != null); if (sanitizedProcessModelIds.length > 0) { whereClause.processModelId = { [sequelize_1.Op.in]: idsAsArray, }; } } const runningProcessInstances = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessInstanceModel.findAll({ where: whereClause, attributes: ['processModelHash', 'processModelId'], group: ['processModelHash', 'processModelId'], }), 'get running Process Instances'); if (runningProcessInstances.length === 0) { return { processDefinitions: [], totalCount: 0, }; } const findProcessDefinitionsWithMatchingHashes = { where: { hash: { [sequelize_1.Op.in]: runningProcessInstances.map((processInstance) => processInstance.processModelHash).filter((entry, index, collection) => collection.indexOf(entry) === index), }, deleted: false, }, include: [ { model: Models.ProcessDefinitionProcessModelAssociationModel, as: 'processModelAssociations', required: true, }, ], }; const results = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionModel.findAll(findProcessDefinitionsWithMatchingHashes), 'query process definitions with running process instances'); const processDefinitions = results.map((result) => this.convertToProcessDefinitionRuntimeObject(result)); return { processDefinitions: processDefinitions, totalCount: results.length, }; } async getByProcessInstanceId(processInstanceId) { const processInstance = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessInstanceModel.findOne({ where: { processInstanceId: processInstanceId, }, }), `get ProcessInstance with ID '${processInstanceId}'`); const processDefinition = await this.getByHash(processInstance.processModelHash); return processDefinition; } async getHistoryByName(name) { const query = { where: { name: name, deleted: false, }, order: [['updatedAt', 'DESC']], include: [ { model: Models.ProcessDefinitionProcessModelAssociationModel, as: 'processModelAssociations', required: true, }, ], }; const definitions = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionModel.findAll(query), `get process definition history by name '${name}'`); const noDefinitionsFound = !definitions || definitions.length === 0; if (noDefinitionsFound) { throw new processcube_engine_sdk_1.NotFoundError(`Process definition with name \`${name}\` not found.`); } const definitonsRuntime = definitions.map(this.convertToProcessDefinitionRuntimeObject.bind(this)); return definitonsRuntime; } /** * Flags all stored XMLs with the given name, as deleted. * Doesn't actually delete anything, just ensures that the xmls will no longer be usable. */ async flagAsDeletedByName(name) { return (0, SequelizeConnectionManager_1.executeWithRetry)(async () => { const processDefinition = await Models.ProcessDefinitionModel.findOne({ where: { name: name, }, }); if (!processDefinition) { return; } const flagAsDeletedTransaction = await this.sequelizeInstance.transaction(); try { await Models.ProcessDefinitionProcessModelAssociationModel.update({ deleted: true }, { where: { processDefinitionId: name }, transaction: flagAsDeletedTransaction, }); await Models.CronjobModel.update({ deleted: true }, { where: { processDefinitionId: name }, transaction: flagAsDeletedTransaction, }); await Models.ProcessDefinitionModel.update({ deleted: true }, { where: { name: name }, transaction: flagAsDeletedTransaction, }); await flagAsDeletedTransaction.commit(); } catch (error) { await flagAsDeletedTransaction.rollback(); throw error; } const processModelIdsToRemove = await this.getProcessModelIdsForProcessDefinitionId(name); await this.publishProcessUndeployedNotification(processDefinition, processModelIdsToRemove); }, `set deleted flag for process definition by name '${name}'`); } async setIsExecutableFlag(processModelId, isExecutable) { const existingProcessModel = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionProcessModelAssociationModel.findOne({ where: { processModelId: processModelId }, }), `find ProcessModel '${processModelId}' in association table`); if (!existingProcessModel) { throw new processcube_engine_sdk_1.NotFoundError(`Process Model with ID ${processModelId} not found`); } return (0, SequelizeConnectionManager_1.executeWithRetry)(async () => { await Models.ProcessDefinitionProcessModelAssociationModel.update({ isExecutable: isExecutable }, { where: { processModelId: processModelId }, }); }, `set isExecutable flag for process model '${processModelId}' to '${isExecutable}'`); } async setIsSingletonFlag(processModelId, isSingleton) { const existingProcessModel = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionProcessModelAssociationModel.findOne({ where: { processModelId: processModelId }, }), `find ProcessModel '${processModelId}' in association table`); if (!existingProcessModel) { throw new processcube_engine_sdk_1.NotFoundError(`Process Model with ID ${processModelId} not found`); } return (0, SequelizeConnectionManager_1.executeWithRetry)(async () => { await Models.ProcessDefinitionProcessModelAssociationModel.update({ isSingleton: isSingleton }, { where: { processModelId: processModelId }, }); }, `set IsSingleton flag for process model '${processModelId}' to '${isSingleton}'`); } async removeProcessDefinitionsFlaggedAsDeleted() { const definitionsToDelete = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionModel.findAll({ attributes: ['name'], group: 'name', where: { deleted: true, }, order: [['name', 'DESC']], }), 'get all process definitions'); for (const processDefinition of definitionsToDelete) { await this.deleteByName(processDefinition.name); } } /** * Deletes all stored XMLs with the given name, as well as any associated data. */ async deleteByName(name) { return (0, SequelizeConnectionManager_1.executeWithRetry)(async () => { const processDefinition = await Models.ProcessDefinitionModel.findOne({ where: { name: name, }, }); if (!processDefinition) { return; } const deleteNotificationRequired = !processDefinition.deleted; const processModelIdsToRemove = await this.getProcessModelIdsForProcessDefinitionId(name); const deleteTransaction = await this.sequelizeInstance.transaction(); try { const deleteQuery = { where: { processDefinitionId: name }, transaction: deleteTransaction, }; await index_2.DataObjectInstanceDatabaseAdapter.deleteByProcessDefinitionId(name, deleteTransaction); await index_2.FlowNodeInstanceDatabaseAdapter.deleteByProcessDefinitionId(name, deleteTransaction); await index_2.ProcessInstanceDatabaseAdapter.deleteByProcessDefinitionId(name, deleteTransaction); await index_2.CronjobDatabaseAdapter.deleteByProcessDefinitionId(name, deleteTransaction); await Models.ProcessDefinitionProcessModelAssociationModel.destroy(deleteQuery); await Models.ProcessDefinitionModel.destroy({ where: { name: name }, transaction: deleteTransaction, }); await deleteTransaction.commit(); } catch (error) { await deleteTransaction.rollback(); throw error; } if (deleteNotificationRequired) { await this.publishProcessUndeployedNotification(processDefinition, processModelIdsToRemove); } }, `delete process definition by name '${name}'`); } async getProcessDefinitionIdForProcessModelId(processModelId) { const result = await (0, SequelizeConnectionManager_1.executeWithRetry)(async () => Models.ProcessDefinitionProcessModelAssociationModel.findOne({ where: { processModelId: processModelId, deleted: false, }, }), `Get Process Definition ID for Process Model ID ${processModelId}`); return result?.processDefinitionId; } async storeProcessDefinition(identity, processDefinitionId, processModelIds, xml, overwriteExisting = true) { if (!overwriteExisting) { const processDefinitionAlreadyExists = await this.processDefinitionExists(processDefinitionId); if (processDefinitionAlreadyExists) { throw new processcube_engine_sdk_1.ConflictError(`A Process Definition with the name \`${processDefinitionId}\` already exists.`); } } const newProcessDefinitionHash = this.hash(xml); // Note: // Unfortunately, sequelize doesn't have MIN/MAX operators for WHERE clauses. // So in order to get the latest matching entry, we have to sort by the creation date and // then cherry-pick the first entry. const findExistingDefinitionWithSameHashQuery = { where: { hash: newProcessDefinitionHash, }, order: [['updatedAt', 'DESC']], }; const existingDefinitions = await Models.ProcessDefinitionModel.findAll(findExistingDefinitionWithSameHashQuery); const existingDefinition = existingDefinitions.length > 0 ? existingDefinitions[0] : undefined; if (existingDefinition !== undefined) { existingDefinition.deleted = false; existingDefinition.changed('updatedAt', true); await existingDefinition.save(); } else { await Models.ProcessDefinitionModel.create({ name: processDefinitionId, xml: xml, hash: newProcessDefinitionHash, createdBy: identity.userId, }); } await Models.ProcessDefinitionProcessModelAssociationModel.destroy({ where: { processDefinitionId: processDefinitionId, }, }); const processModels = processModelIds.map((processModelId) => { return { processModelId: processModelId, processDefinitionId: processDefinitionId, }; }); await Models.ProcessDefinitionProcessModelAssociationModel.bulkCreate(processModels); return newProcessDefinitionHash; } async processDefinitionExists(processDefinitionId) { const findExistingDefinitionsQuery = { limit: 1, where: { name: processDefinitionId, deleted: false, }, order: [['updatedAt', 'DESC']], }; const existingDefinitions = await Models.ProcessDefinitionModel.count(findExistingDefinitionsQuery); return existingDefinitions > 0; } async ensureProcessModelIdsAreUniqe(expectedProcessDefinitionId, processModelIds) { for (const processModelId of processModelIds) { const processDefinitionId = await this.getProcessDefinitionIdForProcessModelId(processModelId); if (!processDefinitionId) { continue; } if (processDefinitionId !== expectedProcessDefinitionId) { const conflictError = new processcube_engine_sdk_1.ConflictError(`Cannot deploy definition \`${expectedProcessDefinitionId}\`! Model \`${processModelId}\` already exists in definition \`${processDefinitionId}\`.`); conflictError.additionalInformation = { processModelId: processModelId, alreadyExistingProcessDefinitionId: processDefinitionId, newProcessDefinitionId: expectedProcessDefinitionId, }; throw conflictError; } } } async getProcessModelIdsForProcessDefinitionId(processDefinitionId) { const result = await Models.ProcessDefinitionProcessModelAssociationModel.findAll({ where: { processDefinitionId: processDefinitionId, }, }); const processModelIds = result?.map((entry) => entry.processModelId); return processModelIds ?? []; } hash(input) { const hash = crypto.createHash('sha1'); hash.update(input); return hash.digest('hex'); } convertToProcessDefinitionRuntimeObject(dataModel) { const processDefinition = { name: dataModel.name, xml: dataModel.xml, hash: dataModel.hash, createdBy: dataModel.createdBy, createdAt: dataModel.createdAt, updatedAt: dataModel.updatedAt, processModelAssociations: dataModel.processModelAssociations.map(this.convertToProcessModelAssociationRuntimeObject), }; return processDefinition; } convertToProcessModelAssociationRuntimeObject(dataModel) { const processModelAssociation = { processDefinitionId: dataModel.processDefinitionId, processModelId: dataModel.processModelId, createdAt: dataModel.createdAt, updatedAt: dataModel.updatedAt, deleted: dataModel.deleted, isExecutable: dataModel.isExecutable, isSingleton: dataModel.isSingleton, }; return processModelAssociation; } async publishProcessUndeployedNotification(processDefinition, processModelIds) { try { const undeployedMessage = { processDefinitionId: processDefinition.name }; EventAggregator_1.default.publish(index_1.eventAggregatorSettings.messagePaths.processUndeployed, undeployedMessage); for (const processModelId of processModelIds) { await this.eventMiddlewareHandler.triggerEvent((0, lodash_clonedeep_1.default)({ eventType: processcube_engine_sdk_1.EngineEventType.OnProcessUndeployed, timestamp: dayjs_1.default.utc().toDate(), processModelId: processModelId, processModelName: undefined, // We don't have that info here, but since the model was deleted, it doesn't really matter anyway. processDefinitionHash: processDefinition.hash, processDefinitionId: processDefinition.name, processInstanceId: '', correlationId: '', logLevel: processcube_engine_sdk_1.LogLevel.info, })); } } catch (error) { staticLogger.warn(`Failed to log OnProcessUndeployed event to middlewares after deleting ProcessDefinition ${processDefinition.name}.`, { err: error, }); const middlewareError = new processcube_engine_sdk_1.InternalServerError(`Failed to log OnProcessUndeployed event to middlewares after deleting ProcessDefinition ${processDefinition.name}`); middlewareError.additionalInformation = { originalError: (0, processcube_engine_sdk_1.serializeJson)(error), }; throw middlewareError; } } }; exports.ProcessDefinitionDatabaseAdapter = ProcessDefinitionDatabaseAdapter; exports.ProcessDefinitionDatabaseAdapter = ProcessDefinitionDatabaseAdapter = __decorate([ (0, inversify_1.injectable)(), __param(0, (0, inversify_1.inject)(IocRegistrations_1.IocRegistrationKeys.internal.EventMiddlewareHandler)), __metadata("design:paramtypes", [EventMiddlewareHandler_1.EventMiddlewareHandler]) ], ProcessDefinitionDatabaseAdapter); //# sourceMappingURL=ProcessDefinitionAdapter.js.map