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