JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 290
  • Score
    100M100P100Q98171F
  • License MIT

Workflow and State Machines for NestJS

Package Exports

  • @jescrich/nestjs-workflow
  • @jescrich/nestjs-workflow/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@jescrich/nestjs-workflow) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

NestJS Workflow & State Machine

A flexible workflow engine built on top of NestJS framework, enabling developers to create, manage, and execute complex workflows in their Node.js applications.

Features

  • Workflow Definitions: Define workflows using a simple, declarative syntax
  • State Management: Track and persist workflow states
  • Event-Driven Architecture: Built on NestJS's event system for flexible workflow triggers
  • Transition Rules: Configure complex transition conditions between workflow states
  • Extensible: Easily extend with custom actions, conditions, and triggers
  • TypeScript Support: Full TypeScript support with strong typing
  • Integration Friendly: Seamlessly integrates with existing NestJS applications

Installation

npm install @jescrich/nestjs-workflow

Or using yarn:

yarn add @jescrich/nestjs-workflow

Quick Start

Module Registration

import { Module } from '@nestjs/common';
import { WorkflowModule } from '@jescrich/nestjs-workflow';

// Register a workflow
@Module({
  imports: [
    WorkflowModule.register({
      name: 'simpleworkflow',
      definition: orderWorkflowDefinition,
    }),
  ],
})
export class AppModule {}

// Inject and use in a service
@Injectable()
class OrderService {
    constructor(
        @Inject('simpleworkflow')
        private readonly orderWorkflow: Workflow<Order, OrderEvent>) {}

    async submitOrder(urn: string) {
        return await this.orderWorkflow.emit({ urn, event: OrderEvent.Submit });
    }
}

@Module({
  imports: [
    WorkflowModule.forRoot({
      storage: {
        type: 'memory'
      }
    }),
  ],
})
export class AppModule {}

Define a Workflow

import { WorkflowDefinition } from '@jescrich/nestjs-workflow';

// Define your entity and state/event enums
export enum OrderEvent {
  Create = 'order.create',
  Submit = 'order.submit',
  Update = 'order.update',
  Complete = 'order.complete',
  Fail = 'order.fail',
  Cancel = 'order.cancel',
}

export enum OrderStatus {
  Pending = 'pending',
  Processing = 'processing',
  Completed = 'completed',
  Failed = 'failed',
}

export class Order {
  urn: string;
  name: string;
  price: number;
  items: string[];
  status: OrderStatus;
}

// Create workflow definition
const orderWorkflowDefinition = (entity: Order): WorkflowDefinition<Order, any, OrderEvent, OrderStatus> => {
  return {
    FinalStates: [OrderStatus.Completed, OrderStatus.Failed],
    IdleStates: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
    Transitions: [
      {
        from: OrderStatus.Pending,
        to: OrderStatus.Processing,
        event: OrderEvent.Submit,
        conditions: [(entity: Order, payload: any) => entity.price > 10],
      },
      {
        from: OrderStatus.Pending,
        to: OrderStatus.Pending,
        event: OrderEvent.Update,
        actions: [
          (entity: Order, payload: any) => {
            entity.price = payload.price;
            entity.items = payload.items;
            return Promise.resolve(entity);
          },
        ],
      },
      {
        from: OrderStatus.Processing,
        to: OrderStatus.Completed,
        event: OrderEvent.Complete,
      },
      {
        from: OrderStatus.Processing,
        to: OrderStatus.Failed,
        event: OrderEvent.Fail,
      },
    ],
    FailedState: OrderStatus.Failed,
    Entity: {
      new: () => new Order(),
      update: async (entity: Order, status: OrderStatus) => {
        entity.status = status;
        return entity;
      },
      load: async (urn: string) => {
        return entity;
      },
      status: (entity: Order) => entity.status,
      urn: function (entity: Order): string {
        return entity.urn;
      },
    },
  };
};

Use the Workflow in a Service

import { Injectable } from '@nestjs/common';
import { WorkflowService } from '@jescrich/nestjs-workflow';
import { Order, OrderEvent, OrderStatus } from './order.model';

@Injectable()
export class OrderService {
  constructor(
    private readonly workflowService: WorkflowService<Order, any, OrderEvent, OrderStatus>,
  ) {}
  
  async createOrder() {
    const order = new Order();
    order.urn = 'urn:order:123';
    order.name = 'Order 123';
    order.price = 100;
    order.items = ['Item 1', 'Item 2', 'Item 3'];
    order.status = OrderStatus.Pending;
    
    // Initialize workflow with order entity
    const workflowDefinition = orderWorkflowDefinition(order);
    const workflow = new WorkflowService<Order, any, OrderEvent, OrderStatus>(workflowDefinition);
    
    return order;
  }
  
  async submitOrder(urn: string) {
    // Emit an event to trigger workflow transition
    const result = await this.workflowService.emit({ 
      urn: urn, 
      event: OrderEvent.Submit 
    });
    
    return result;
  }
  
  async updateOrder(urn: string, price: number, items: string[]) {
    // Emit an event with payload to update the order
    const result = await this.workflowService.emit({
      urn: urn,
      event: OrderEvent.Update,
      payload: {
        price: price,
        items: items,
      },
    });
    
    return result;
  }
}

Configuration Options

The WorkflowModule.forRoot() method accepts the following configuration options:

interface WorkflowModuleOptions {
  storage: {
    type: 'memory' | 'database';
    options?: any;
  };
}

Configuring Actions and Conditions

NestJS Workflow provides two different approaches for configuring actions and conditions in your workflows:

1. Inline Functions in Transitions

You can define actions and conditions directly in the transition definition as shown in the example above:

{
  from: OrderStatus.Pending,
  to: OrderStatus.Processing,
  event: OrderEvent.Submit,
  conditions: [(entity: Order, payload: any) => entity.price > 10],
  actions: [
    (entity: Order, payload: any) => {
      // Perform action
      return Promise.resolve(entity);
    },
  ],
}

2. Using Decorators (Class-based approach)

For more complex workflows, you can use a class-based approach with decorators:

import { Injectable } from '@nestjs/common';
import { WorkflowAction, OnEvent, OnStatusChanged } from '@jescrich/nestjs-workflow';

@Injectable()
@WorkflowAction()
export class OrderActions {
  // Handler triggered on specific event
  @OnEvent({ event: OrderEvent.Submit })
  execute(params: { entity: Order; payload: any }) {
    const { entity, payload } = params;
    entity.price = entity.price * 100;
    return Promise.resolve(entity);
  }

  // Handler triggered when status changes
  @OnStatusChanged({ from: OrderStatus.Pending, to: OrderStatus.Processing })
  onStatusChanged(params: { entity: Order; payload: any }) {
    const { entity, payload } = params;
    entity.name = 'Status changed to processing';
    return Promise.resolve(entity);
  }
}

Then include these action classes in your workflow definition:

const definition: WorkflowDefinition<Order, any, OrderEvent, OrderStatus> = {
  Actions: [OrderActions],
  // ...other properties
  Transitions: [
    {
      from: OrderStatus.Pending,
      to: OrderStatus.Processing,
      event: OrderEvent.Submit,
    },
    // Other transitions
  ],
  // ...
};

Execution Order with @OnEvent

You can control the execution order of multiple handlers for the same event:

@Injectable()
@WorkflowAction()
export class OrderActions {
  @OnEvent({ event: OrderEvent.Submit, order: 1 })
  firstHandler(params: { entity: Order; payload: any }) {
    // Executes first
    return Promise.resolve(params.entity);
  }

  @OnEvent({ event: OrderEvent.Submit, order: 2 })
  secondHandler(params: { entity: Order; payload: any }) {
    // Executes second
    return Promise.resolve(params.entity);
  }
}

Error Handling with @OnStatusChanged

By default, if a status change handler fails, the workflow will transition to the failed state:

@OnStatusChanged({ from: OrderStatus.Pending, to: OrderStatus.Processing })
onStatusChanged(params: { entity: Order; payload: any }) {
  // If this throws an error, the workflow will move to the failed state
}

You can disable this behavior by setting failOnError: false:

@OnStatusChanged({ 
  from: OrderStatus.Pending, 
  to: OrderStatus.Processing, 
  failOnError: false 
})
onStatusChanged(params: { entity: Order; payload: any }) {
  // If this throws an error, the workflow will continue to the next state
}

Remember to register your action classes as providers in your module:

@Module({
  imports: [
    WorkflowModule.register({
      name: 'orderWorkflow',
      definition,
    }),
  ],
  providers: [OrderActions],
})
export class OrderModule {}

Advanced Usage

For more advanced usage, including custom actions, conditions, and event handling, please check the documentation.