UNPKG

@jescrich/nestjs-workflow

Version:
381 lines 19.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var WorkflowService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkflowService = void 0; const common_1 = require("@nestjs/common"); const core_1 = require("@nestjs/core"); const entity_service_1 = require("./entity.service"); const client_1 = require("./kafka/client"); let WorkflowService = WorkflowService_1 = class WorkflowService { definition; kafkaClient; logger = new common_1.Logger(WorkflowService_1.name); actionsOnStatusChanged = new Map(); actionsOnEvent = new Map(); entityService = null; moduleRef; constructor(definition, injectedEntityService) { this.definition = definition; this.logger.log(`Initializing workflow: ${this.definition.name}`, this.definition.name); this.entityService = injectedEntityService || null; } async onModuleInit() { this.configureActions(); this.configureConditions(); await this.initializeKakfaConsumers(); } async emit(params) { const { event, urn, payload } = params; const result = await this.transition({ event, urn, payload }); return result; } async transition(params) { const { event, urn, payload } = params; let currentEvent = event; try { this.logger.log(`Event: ${event}`, urn); let entity = await this.loadEntity(urn); if (!entity || entity === null) { this.logger.error(`Element not found`, urn); throw new common_1.BadRequestException(`Entity not found`, urn); } let entityCurrentState = this.getEntityStatus(entity); if (this.definition.states.finals.includes(entityCurrentState)) { this.logger.warn(`Entity: ${urn} is in a final status. Accepting transitions due to a retry mechanism.`, urn); } let transitionEvent; let transition; let message = ''; do { transitionEvent = this.definition.transitions.find((transition) => { const events = Array.isArray(transition.event) ? transition.event : [transition.event]; const states = Array.isArray(transition.from) ? transition.from : [transition.from]; return currentEvent && events.includes(currentEvent) && states.includes(entityCurrentState); }); if (!transitionEvent) { throw new Error(`Unable to find transition event for Event: ${currentEvent} and Status: ${entityCurrentState}`); } const nextStatus = transitionEvent.to; const possibleTransitions = this.definition.transitions.filter((t) => (Array.isArray(t.from) ? t.from.includes(entityCurrentState) : t.from === entityCurrentState) && t.to === nextStatus); this.logger.log(`Possible transitions for ${urn}: ${JSON.stringify(possibleTransitions)}`, urn); for (const t of possibleTransitions) { this.logger.log(`Checking conditional transition from ${entityCurrentState} to ${nextStatus}`, urn); if (!t.conditions || (t.conditions && t.conditions.every((condition) => { const result = condition(entity, payload); this.logger.log(`Condition ${condition.name || 'anonymous'} result: ${result}`, urn); return result; }))) { transition = t; break; } else { this.logger.log(`Condition not met for transition from ${entityCurrentState} to ${nextStatus}`, urn); } } if (!transition) { this.logger.warn(`There's no valid transition from ${entityCurrentState} to ${nextStatus} or the condition is not met.`); if (this.definition.fallback) { this.logger.log(`Falling back to the default transition`, urn); entity = await this.definition.fallback(entity, currentEvent, payload); } return entity; } this.logger.log(`Executing transition from ${entityCurrentState} to ${nextStatus}`, urn); let failed; if (this.actionsOnEvent.has(currentEvent)) { const actions = this.actionsOnEvent.get(currentEvent); if (actions && actions.length > 0) { this.logger.log(`Executing actions for event ${transition.event}`, urn); for (const action of actions) { this.logger.log(`Executing action ${action.name}`, urn); try { entity = await action({ entity, payload }); } catch (error) { this.logger.error(`Action ${action.name} failed: ${error.message}`, urn); failed = true; break; } } } } ({ failed, Element: entity, message, } = await this.executeInlineActions(transition, entity, currentEvent, message, payload, entityCurrentState, nextStatus, urn)); if (failed) { this.logger.log(`Transition failed. Setting status to failed. ${message}`, urn); await this.updateEntityStatus(entity, this.definition.states.failed); this.logger.log(`Element transitioned to failed status. ${message}`, urn); break; } entity = await this.updateEntityStatus(entity, nextStatus); this.logger.log(`Element transitioned from ${entityCurrentState} to ${nextStatus} ${message}`, urn); const statusChangeKey = `${entityCurrentState}-${nextStatus}`; if (this.actionsOnStatusChanged.has(statusChangeKey)) { const actions = this.actionsOnStatusChanged.get(statusChangeKey); if (actions && actions.length > 0) { this.logger.log(`Executing actions for status change from ${entityCurrentState} to ${nextStatus}`, urn); for (const action of actions) { this.logger.log(`Executing action ${action.action.name}`, urn); try { entity = await action.action({ entity, payload }); } catch (error) { this.logger.error(`Action ${action.action.name} failed: ${error.message}`, urn); failed = action.failOnError; break; } } } } if (failed) { this.logger.log(`Transition has succeded by a post on status change event has failed. ${message}`, urn); await this.updateEntityStatus(entity, this.definition.states.failed); this.logger.log(`Element transitioned to failed status. ${message}`, urn); break; } if (this.isInIdleStatus(entity)) { this.logger.log(`Element: ${urn} is idle in ${nextStatus} status. Waiting for external event...`); break; } if (this.isInFailedStatus(entity)) { this.logger.log(`Element: ${urn} is in a final state. Workflow completed.`); break; } currentEvent = this.nextEvent(entity); entityCurrentState = this.getEntityStatus(entity); this.logger.log(`Next event: ${currentEvent ?? 'none'} Next status: ${entityCurrentState}`, urn); } while (currentEvent); return entity; } catch (error) { const message = `An error occurred while transitioning the Element ${error?.message ?? ''}`; throw new Error(`Element: ${urn} Event: ${event} - ${message}.`); } } async executeInlineActions(transition, entity, currentEvent, message, payload, currentStatus, nextStatus, urn) { if (!transition.actions) { return { failed: false, Element: entity, message }; } const actions = await transition.actions; let failed = false; try { for (const action of actions) { entity = await action(entity, payload); if (!entity) { throw new Error(`Transition from ${currentStatus} to ${nextStatus} has failed. Error: Result is null.`); } } } catch (error) { this.logger.error(`Entity workflow has failed. Error: ${error?.message}`, urn); message = error?.message; failed = true; } return { failed, Element: entity, message }; } nextEvent(entity) { const status = this.getEntityStatus(entity); const nextTransitions = this.definition.transitions.filter((transition) => (Array.isArray(transition.from) ? transition.from.includes(status) : transition.from === status) && transition.to !== this.definition.states.failed); if (nextTransitions && nextTransitions.length > 1) { for (const transition of nextTransitions) { const transitionEvent = this.definition.transitions.find((t) => t.event === transition.event); if (transitionEvent) { const transitionVector = this.definition.transitions.find((t) => t.to === transitionEvent.to); if (transitionVector && transitionVector.conditions) { let allConditionsMet = true; for (const condition of transition.conditions || []) { const conditionResult = condition(entity); this.logger.log(`Condition ${condition.name || 'unnamed'} result:`, conditionResult); if (!conditionResult) { allConditionsMet = false; } } if (allConditionsMet) { if (Array.isArray(transition.event)) { throw new Error('Multiple transition events are not allowed in a non-idle state'); } return transition.event; } else { this.logger.log(`Conditions not met for transition ${transition.event}`); } } } } } else { if (nextTransitions && nextTransitions.length === 1) { if (Array.isArray(nextTransitions[0].event)) { throw new Error('Multiple transition events are not allowed in a non-idle state'); } return nextTransitions[0].event; } } return null; } isInIdleStatus(entity) { const status = this.getEntityStatus(entity); if (!status) { throw new Error('Entity status is not defined. Unable to determine if the entity is idle or not.'); } return this.definition.states.idles.includes(status); } isInFailedStatus(entity) { const status = this.getEntityStatus(entity); if (!status) { throw new Error('Entity status is not defined. Unable to determine if the entity is idle or not.'); } return status === this.definition.states.failed; } configureActions() { try { if (this.definition.actions) { for (const action of this.definition.actions) { const instance = this.moduleRef.get(action, { strict: false }); if (instance && Reflect.getMetadata('isWorkflowAction', action)) { const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(instance)); for (const method of methods) { const event = Reflect.getMetadata('onEvent', instance, method); const statusChanged = Reflect.getMetadata('onStatusChanged', instance, method); if (event) { const methodParams = Reflect.getMetadata('design:paramtypes', instance, method); if (!methodParams || methodParams.length !== 1 || !methodParams[0].name.includes('Object')) { throw new Error(`Action method ${method} must have signature (params: { entity: T, payload?: P | T | object | string })`); } this.validateActionMethod(instance, method); if (!this.actionsOnEvent.has(event)) { this.actionsOnEvent.set(event, []); } this.actionsOnEvent.get(event)?.push(instance[method].bind(instance)); } if (statusChanged) { const methodParams = Reflect.getMetadata('design:paramtypes', instance, method); if (!methodParams || methodParams.length !== 1 || !methodParams[0].name.includes('Object')) { throw new Error(`Action method ${method} must have signature (params: { entity: T, payload?: P | T | object | string })`); } const from = Reflect.getMetadata('from', instance, method); const to = Reflect.getMetadata('to', instance, method); const key = `${from}-${to}`; if (!this.actionsOnStatusChanged.has(key)) { this.actionsOnStatusChanged.set(key, []); } this.actionsOnStatusChanged.get(key)?.push({ action: instance[method].bind(instance), failOnError: Reflect.getMetadata('failOnError', instance, method), }); } } } } } this.logger.log(`Initialized with ${this.actionsOnEvent.size} actions on events`); this.logger.log(`Initialized with ${this.actionsOnStatusChanged.size} actions on status changes`); this.logger.log(`Initialized with ${this.definition.transitions.length} transitions`); this.logger.log(`Initialized with ${this.definition.conditions?.length} conditions`); } catch (e) { this.logger.error('Error trying to initialize workflow actions', e); throw e; } } configureConditions() { } async initializeKakfaConsumers() { if (!this.definition.kafka) { this.logger.log('No Kafka events defined.'); return; } if (!this.kafkaClient) { this.logger.error('Kafka client not found, have you ever specified the Kafka module in the imports?'); return; } for (const kafkaDef of this.definition.kafka?.events) { this.kafkaClient.consume(kafkaDef.topic, this.definition.name + 'consumer', async (params) => { const { key, event } = params; this.logger.log(`Kafka Event received on topic ${kafkaDef.topic} with key ${key}`, key); try { this.emit({ event: kafkaDef.event, urn: key, payload: event }); this.logger.log(`Kafka Event emmited successfuly`, key); } catch (e) { this.logger.error(`Kafka Event fail to process`, key); } }); this.logger.log('Initializing topic consumption'); } } validateActionMethod = (instance, method) => { const originalMethod = instance[method]; instance[method] = function (...args) { if (args.length !== 1) { throw new Error(`Action method ${method} must be called with exactly one parameter`); } const param = args[0]; if (!param || typeof param !== 'object') { throw new Error(`Action method ${method} parameter must be an object`); } if (!('entity' in param)) { throw new Error(`Action method ${method} parameter must have an 'entity' property`); } return originalMethod.apply(this, args); }; }; async loadEntity(urn) { if (this.entityService) { const e = this.entityService.load(urn); return e ?? null; } return this.definition.entity.load(urn); } getEntityStatus(entity) { if (this.entityService) { return this.entityService.status(entity); } return this.definition.entity.status(entity); } async updateEntityStatus(entity, status) { if (this.entityService) { return this.entityService.update(entity, status); } return this.definition.entity.update(entity, status); } getEntityUrn(entity) { if (this.entityService) { return this.entityService.urn(entity); } return this.definition.entity.urn(entity); } }; exports.WorkflowService = WorkflowService; __decorate([ (0, common_1.Inject)(), __metadata("design:type", client_1.KafkaClient) ], WorkflowService.prototype, "kafkaClient", void 0); __decorate([ (0, common_1.Inject)(), __metadata("design:type", core_1.ModuleRef) ], WorkflowService.prototype, "moduleRef", void 0); exports.WorkflowService = WorkflowService = WorkflowService_1 = __decorate([ (0, common_1.Injectable)(), __param(1, (0, common_1.Optional)()), __metadata("design:paramtypes", [Object, entity_service_1.EntityService]) ], WorkflowService); //# sourceMappingURL=service.js.map