@jescrich/nestjs-workflow
Version:
Workflow and State Machines for NestJS
381 lines • 19.8 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var 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
;