nestjs-serverless-workflow 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +396 -0
- package/dist/adapter/index.d.ts +2 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +2 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter/lambda.adapater.d.ts +4 -0
- package/dist/adapter/lambda.adapater.d.ts.map +1 -0
- package/dist/adapter/lambda.adapater.js +63 -0
- package/dist/adapter/lambda.adapater.js.map +1 -0
- package/dist/event-bus/index.d.ts +3 -0
- package/dist/event-bus/index.d.ts.map +1 -0
- package/dist/event-bus/index.js +3 -0
- package/dist/event-bus/index.js.map +1 -0
- package/dist/event-bus/sqs/sqs.emitter.d.ts +6 -0
- package/dist/event-bus/sqs/sqs.emitter.d.ts.map +1 -0
- package/dist/event-bus/sqs/sqs.emitter.js +4 -0
- package/dist/event-bus/sqs/sqs.emitter.js.map +1 -0
- package/dist/event-bus/types/broker-publisher.interface.d.ts +5 -0
- package/dist/event-bus/types/broker-publisher.interface.d.ts.map +1 -0
- package/dist/event-bus/types/broker-publisher.interface.js +2 -0
- package/dist/event-bus/types/broker-publisher.interface.js.map +1 -0
- package/dist/event-bus/types/index.d.ts +3 -0
- package/dist/event-bus/types/index.d.ts.map +1 -0
- package/dist/event-bus/types/index.js +3 -0
- package/dist/event-bus/types/index.js.map +1 -0
- package/dist/event-bus/types/workflow-event.interface.d.ts +7 -0
- package/dist/event-bus/types/workflow-event.interface.d.ts.map +1 -0
- package/dist/event-bus/types/workflow-event.interface.js +2 -0
- package/dist/event-bus/types/workflow-event.interface.js.map +1 -0
- package/dist/exception/index.d.ts +2 -0
- package/dist/exception/index.d.ts.map +1 -0
- package/dist/exception/index.js +2 -0
- package/dist/exception/index.js.map +1 -0
- package/dist/exception/unretriable.exception.d.ts +4 -0
- package/dist/exception/unretriable.exception.d.ts.map +1 -0
- package/dist/exception/unretriable.exception.js +7 -0
- package/dist/exception/unretriable.exception.js.map +1 -0
- package/dist/workflow/decorators/default.decorator.d.ts +3 -0
- package/dist/workflow/decorators/default.decorator.d.ts.map +1 -0
- package/dist/workflow/decorators/default.decorator.js +9 -0
- package/dist/workflow/decorators/default.decorator.js.map +1 -0
- package/dist/workflow/decorators/event.decorator.d.ts +5 -0
- package/dist/workflow/decorators/event.decorator.d.ts.map +1 -0
- package/dist/workflow/decorators/event.decorator.js +20 -0
- package/dist/workflow/decorators/event.decorator.js.map +1 -0
- package/dist/workflow/decorators/index.d.ts +6 -0
- package/dist/workflow/decorators/index.d.ts.map +1 -0
- package/dist/workflow/decorators/index.js +6 -0
- package/dist/workflow/decorators/index.js.map +1 -0
- package/dist/workflow/decorators/params.decorator.d.ts +3 -0
- package/dist/workflow/decorators/params.decorator.d.ts.map +1 -0
- package/dist/workflow/decorators/params.decorator.js +19 -0
- package/dist/workflow/decorators/params.decorator.js.map +1 -0
- package/dist/workflow/decorators/with-retry.decorator.d.ts +4 -0
- package/dist/workflow/decorators/with-retry.decorator.d.ts.map +1 -0
- package/dist/workflow/decorators/with-retry.decorator.js +9 -0
- package/dist/workflow/decorators/with-retry.decorator.js.map +1 -0
- package/dist/workflow/decorators/workflow.decorator.d.ts +6 -0
- package/dist/workflow/decorators/workflow.decorator.d.ts.map +1 -0
- package/dist/workflow/decorators/workflow.decorator.js +8 -0
- package/dist/workflow/decorators/workflow.decorator.js.map +1 -0
- package/dist/workflow/index.d.ts +7 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/providers/index.d.ts +4 -0
- package/dist/workflow/providers/index.d.ts.map +1 -0
- package/dist/workflow/providers/index.js +4 -0
- package/dist/workflow/providers/index.js.map +1 -0
- package/dist/workflow/providers/ochestrator.service.d.ts +32 -0
- package/dist/workflow/providers/ochestrator.service.d.ts.map +1 -0
- package/dist/workflow/providers/ochestrator.service.js +183 -0
- package/dist/workflow/providers/ochestrator.service.js.map +1 -0
- package/dist/workflow/providers/router.factory.d.ts +7 -0
- package/dist/workflow/providers/router.factory.d.ts.map +1 -0
- package/dist/workflow/providers/router.factory.js +18 -0
- package/dist/workflow/providers/router.factory.js.map +1 -0
- package/dist/workflow/providers/router.service.d.ts +16 -0
- package/dist/workflow/providers/router.service.d.ts.map +1 -0
- package/dist/workflow/providers/router.service.js +79 -0
- package/dist/workflow/providers/router.service.js.map +1 -0
- package/dist/workflow/providers/saga.service.d.ts +34 -0
- package/dist/workflow/providers/saga.service.d.ts.map +1 -0
- package/dist/workflow/providers/saga.service.js +159 -0
- package/dist/workflow/providers/saga.service.js.map +1 -0
- package/dist/workflow/types/entity.interface.d.ts +33 -0
- package/dist/workflow/types/entity.interface.d.ts.map +1 -0
- package/dist/workflow/types/entity.interface.js +2 -0
- package/dist/workflow/types/entity.interface.js.map +1 -0
- package/dist/workflow/types/index.d.ts +7 -0
- package/dist/workflow/types/index.d.ts.map +1 -0
- package/dist/workflow/types/index.js +9 -0
- package/dist/workflow/types/index.js.map +1 -0
- package/dist/workflow/types/retry.interface.d.ts +24 -0
- package/dist/workflow/types/retry.interface.d.ts.map +1 -0
- package/dist/workflow/types/retry.interface.js +10 -0
- package/dist/workflow/types/retry.interface.js.map +1 -0
- package/dist/workflow/types/saga.interface.d.ts +144 -0
- package/dist/workflow/types/saga.interface.d.ts.map +1 -0
- package/dist/workflow/types/saga.interface.js +24 -0
- package/dist/workflow/types/saga.interface.js.map +1 -0
- package/dist/workflow/types/shared.type.d.ts +5 -0
- package/dist/workflow/types/shared.type.d.ts.map +1 -0
- package/dist/workflow/types/shared.type.js +2 -0
- package/dist/workflow/types/shared.type.js.map +1 -0
- package/dist/workflow/types/transition-event.interface.d.ts +16 -0
- package/dist/workflow/types/transition-event.interface.d.ts.map +1 -0
- package/dist/workflow/types/transition-event.interface.js +2 -0
- package/dist/workflow/types/transition-event.interface.js.map +1 -0
- package/dist/workflow/types/workflow-definition.interface.d.ts +60 -0
- package/dist/workflow/types/workflow-definition.interface.d.ts.map +1 -0
- package/dist/workflow/types/workflow-definition.interface.js +2 -0
- package/dist/workflow/types/workflow-definition.interface.js.map +1 -0
- package/dist/workflow/utils/index.d.ts +2 -0
- package/dist/workflow/utils/index.d.ts.map +1 -0
- package/dist/workflow/utils/index.js +2 -0
- package/dist/workflow/utils/index.js.map +1 -0
- package/dist/workflow/utils/retry-backoff.d.ts +35 -0
- package/dist/workflow/utils/retry-backoff.d.ts.map +1 -0
- package/dist/workflow/utils/retry-backoff.js +68 -0
- package/dist/workflow/utils/retry-backoff.js.map +1 -0
- package/dist/workflow/workflow.module.d.ts +13 -0
- package/dist/workflow/workflow.module.d.ts.map +1 -0
- package/dist/workflow/workflow.module.js +27 -0
- package/dist/workflow/workflow.module.js.map +1 -0
- package/package.json +154 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Thomas Do (tung-dnt)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://joseescrich.com/logos/nestjs-workflow.png">
|
|
3
|
+
<source media="(prefers-color-scheme: light)" srcset="https://joseescrich.com/logos/nestjs-workflow-light.png">
|
|
4
|
+
<img src="https://joseescrich.com/logos/nestjs-workflow.png" alt="NestJS Workflow Logo" width="200" style="margin-bottom:20px">
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
7
|
+
# NestJS Workflow & State Machine
|
|
8
|
+
|
|
9
|
+
A flexible workflow engine built on top of NestJS framework, enabling developers to create, manage, and execute complex workflows in their Node.js applications.
|
|
10
|
+
|
|
11
|
+
## 🎯 Live Examples & Demos
|
|
12
|
+
|
|
13
|
+
Explore fully functional examples with **interactive visual demos** in our dedicated examples repository:
|
|
14
|
+
|
|
15
|
+
### 👉 **[View Examples Repository](https://github.com/@nestjs-serverless-workflow-examples)**
|
|
16
|
+
|
|
17
|
+
The repository includes three comprehensive real-world examples:
|
|
18
|
+
|
|
19
|
+
1. **🚀 User Onboarding Workflow** - Multi-step verification, KYC/AML compliance, risk assessment
|
|
20
|
+
2. **📦 Order Processing System** - Complete e-commerce lifecycle with payment retry logic
|
|
21
|
+
3. **📊 Kafka-Driven Inventory** - Real-time event-driven inventory management with Kafka integration
|
|
22
|
+
|
|
23
|
+
Each example features:
|
|
24
|
+
|
|
25
|
+
- ✨ **Interactive Visual Mode** - See workflows in action with real-time state visualization
|
|
26
|
+
- 🎮 **Interactive Controls** - Manually trigger transitions and explore different paths
|
|
27
|
+
- 🤖 **Automated Scenarios** - Pre-built test cases demonstrating various workflow paths
|
|
28
|
+
- 📝 **Full Source Code** - Production-ready implementations you can adapt
|
|
29
|
+
|
|
30
|
+
**[➡️ Get Started with Examples](https://github.com/@nestjs-serverless-workflow-examples#-quick-start)**
|
|
31
|
+
|
|
32
|
+
## Table of Contents
|
|
33
|
+
|
|
34
|
+
- [Features](#features)
|
|
35
|
+
- [Stateless Architecture](#stateless-architecture)
|
|
36
|
+
- [Installation](#installation)
|
|
37
|
+
- [Quick Start](#quick-start)
|
|
38
|
+
- [Module Registration](#module-registration)
|
|
39
|
+
- [Define a Workflow](#define-a-workflow)
|
|
40
|
+
- [Message Format](#message-format)
|
|
41
|
+
- [Configuring Actions and Conditions](#configuring-actions-and-conditions)
|
|
42
|
+
- [Complete Example with Kafka Integration](#complete-example-with-kafka-integration)
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **🌲 Tree-Shakable**: Modular architecture with subpath exports ensures minimal bundle size
|
|
47
|
+
- **Workflow Definitions**: Define workflows using a simple, declarative syntax
|
|
48
|
+
- **State Management**: Track and persist workflow states
|
|
49
|
+
- **Event-Driven Architecture**: Built on NestJS's event system for flexible workflow triggers
|
|
50
|
+
- **Transition Rules**: Configure complex transition conditions between workflow states
|
|
51
|
+
- **Extensible**: Easily extend with custom actions, conditions, and triggers
|
|
52
|
+
- **TypeScript Support**: Full TypeScript support with strong typing
|
|
53
|
+
- **Integration Friendly**: Seamlessly integrates with existing NestJS applications
|
|
54
|
+
- **Message Broker Integration**: Easily integrate with SQS, Kafka, RabbitMQ, and more
|
|
55
|
+
- **Stateless Design**: Lightweight implementation with no additional storage requirements
|
|
56
|
+
- **Serverless Ready**: Optimized for AWS Lambda with automatic timeout handling
|
|
57
|
+
|
|
58
|
+
## 📚 Documentation
|
|
59
|
+
|
|
60
|
+
Comprehensive documentation is available:
|
|
61
|
+
- **[Getting Started](./docs/getting-started.md)** - Installation and basic usage
|
|
62
|
+
- **[Workflow Module](./docs/workflow.md)** - State machines and transitions
|
|
63
|
+
- **[Event Bus](./docs/event-bus.md)** - Message broker integration
|
|
64
|
+
- **[Adapters](./docs/adapters.md)** - Runtime environment adapters
|
|
65
|
+
- **[API Documentation](./docs/)** - Full API reference
|
|
66
|
+
|
|
67
|
+
Online documentation: https://@nestjs-serverless-workflow.github.io/libraries/docs/workflow/intro
|
|
68
|
+
|
|
69
|
+
# Stateless Architecture
|
|
70
|
+
|
|
71
|
+
## NestJS Workflow is designed with a stateless architecture, which offers several key benefits
|
|
72
|
+
|
|
73
|
+
Benefits of Stateless Design
|
|
74
|
+
|
|
75
|
+
- Simplicity: No additional database or storage configuration required
|
|
76
|
+
- Domain-Driven: State is maintained within your domain entities where it belongs
|
|
77
|
+
- Lightweight: Minimal overhead and dependencies
|
|
78
|
+
- Scalability: Easily scales horizontally with your application
|
|
79
|
+
- Flexibility: Works with any persistence layer or storage mechanism
|
|
80
|
+
- Integration: Seamlessly integrates with your existing data model and repositories
|
|
81
|
+
- The workflow engine doesn't maintain any state itself - instead, it operates on your domain entities, reading their current state and applying transitions according to your defined rules. This approach aligns with domain-driven design principles by keeping the state with the entity it belongs to.
|
|
82
|
+
|
|
83
|
+
This stateless design means you can:
|
|
84
|
+
|
|
85
|
+
Use your existing repositories and data access patterns
|
|
86
|
+
Persist workflow state alongside your entity data
|
|
87
|
+
Avoid complex synchronization between separate state stores
|
|
88
|
+
Maintain transactional integrity with your domain operations
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
// Example of how state is part of your domain entity
|
|
92
|
+
export class Order {
|
|
93
|
+
id: string;
|
|
94
|
+
items: OrderItem[];
|
|
95
|
+
totalAmount: number;
|
|
96
|
+
status: OrderStatus; // The workflow state is a property of your entity
|
|
97
|
+
|
|
98
|
+
// Your domain logic here
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The workflow engine simply reads and updates this state property according to your defined transitions, without needing to maintain any separate state storage.
|
|
103
|
+
|
|
104
|
+
## Installation
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm install nestjs-serverless-workflow
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or using bun:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun add nestjs-serverless-workflow
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or using yarn:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
yarn add nestjs-serverless-workflow
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Peer Dependencies
|
|
123
|
+
|
|
124
|
+
This library requires the following peer dependencies:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Optional Dependencies** (only if you need specific features):
|
|
131
|
+
|
|
132
|
+
- For AWS Lambda adapter: `@types/aws-lambda`
|
|
133
|
+
- For SQS integration: `@aws-sdk/client-sqs`
|
|
134
|
+
- For DynamoDB: `@aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb`
|
|
135
|
+
|
|
136
|
+
## Quick Start
|
|
137
|
+
|
|
138
|
+
### 🎮 Try the Interactive Demos First
|
|
139
|
+
|
|
140
|
+
Before diving into code, experience workflows visually with our interactive demos:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Quick demo setup
|
|
144
|
+
git clone https://github.com/@nestjs-serverless-workflow-examples.git
|
|
145
|
+
cd nestjs-workflow-examples/01-user-onboarding
|
|
146
|
+
npm install && npm run demo
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
You'll see an interactive workflow visualization like this:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
╔══════════════╗ ┌──────────────┐ ┌────────────────┐
|
|
153
|
+
║ REGISTERED ║ --> │EMAIL_VERIFIED│ --> │PROFILE_COMPLETE│
|
|
154
|
+
╚══════════════╝ └──────────────┘ └────────────────┘
|
|
155
|
+
(current) ↓ ↓
|
|
156
|
+
┌──────────────┐ ┌─────────────────┐
|
|
157
|
+
│ SUSPENDED │ │IDENTITY_VERIFIED│
|
|
158
|
+
└──────────────┘ └─────────────────┘
|
|
159
|
+
↓
|
|
160
|
+
╔══════════╗
|
|
161
|
+
║ ACTIVE ║
|
|
162
|
+
╚══════════╝
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**[🚀 Explore All Examples](https://github.com/@nestjs-serverless-workflow-examples)**
|
|
166
|
+
|
|
167
|
+
### How It Works
|
|
168
|
+
|
|
169
|
+
When you configure SQS integration:
|
|
170
|
+
|
|
171
|
+
1. The workflow engine will connect to the specified SQS queue
|
|
172
|
+
2. It will subscribe to the topics you've defined in the `events` array
|
|
173
|
+
3. When a message arrives on a subscribed topic, the workflow engine will:
|
|
174
|
+
- Map the topic to the corresponding workflow event
|
|
175
|
+
- Extract the entity URN from the message
|
|
176
|
+
- Load the entity using your defined `entity.load` function
|
|
177
|
+
- Emit the mapped workflow event with the Kafka message as payload
|
|
178
|
+
|
|
179
|
+
### Complete Example with SQS Integration
|
|
180
|
+
|
|
181
|
+
## Using Subpath Exports
|
|
182
|
+
|
|
183
|
+
The package uses modern subpath exports for better tree-shaking. Import only what you need:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { WorkflowModule } from 'nestjs-serverless-workflow/workflow';
|
|
187
|
+
import { IBrokerPublisher } from 'nestjs-serverless-workflow/event-bus';
|
|
188
|
+
import { LambdaEventHandler } from 'nestjs-serverless-workflow/adapter';
|
|
189
|
+
import { UnretriableException } from 'nestjs-serverless-workflow/exception';
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This ensures that your bundle only includes the parts of the library you actually use, resulting in smaller bundle sizes.
|
|
193
|
+
|
|
194
|
+
## Quick Start
|
|
195
|
+
|
|
196
|
+
````typescript
|
|
197
|
+
import { Module } from '@nestjs/common';
|
|
198
|
+
import { WorkflowModule, Workflow, OnEvent, Payload, Entity } from 'nestjs-serverless-workflow/workflow';
|
|
199
|
+
import { OrderEntityService } from './order-entity.service';
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// Define your entity and state/event enums
|
|
203
|
+
export enum OrderEvent {
|
|
204
|
+
Create = 'order.create',
|
|
205
|
+
Submit = 'order.submit',
|
|
206
|
+
Complete = 'order.complete',
|
|
207
|
+
Fail = 'order.fail',
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export enum OrderStatus {
|
|
211
|
+
Pending = 'pending',
|
|
212
|
+
Processing = 'processing',
|
|
213
|
+
Completed = 'completed',
|
|
214
|
+
Failed = 'failed',
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export class Order {
|
|
218
|
+
id: string;
|
|
219
|
+
name: string;
|
|
220
|
+
price: number;
|
|
221
|
+
items: string[];
|
|
222
|
+
status: OrderStatus;
|
|
223
|
+
}
|
|
224
|
+
@Workflow({
|
|
225
|
+
states: {
|
|
226
|
+
finals: [OrderStatus.Completed, OrderStatus.Failed],
|
|
227
|
+
idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
|
|
228
|
+
failed: OrderStatus.Failed,
|
|
229
|
+
},
|
|
230
|
+
transitions: [
|
|
231
|
+
// Your transitions here
|
|
232
|
+
{
|
|
233
|
+
from: OrderStatus.Pending,
|
|
234
|
+
to: OrderStatus.Processing,
|
|
235
|
+
event: OrderEvent.Submit,
|
|
236
|
+
conditions: [(entity: Order, payload: any) => entity.price > 10],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
from: OrderStatus.Processing,
|
|
240
|
+
to: OrderStatus.Completed,
|
|
241
|
+
event: OrderEvent.Complete,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
from: OrderStatus.Processing,
|
|
245
|
+
to: OrderStatus.Failed,
|
|
246
|
+
event: OrderEvent.Fail,
|
|
247
|
+
}
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
})
|
|
251
|
+
class OrderWorkflowDefinition {
|
|
252
|
+
@OnEvent(OrderEvent.Submit)
|
|
253
|
+
async onSubmit(@Entity entity: Order, @Payload(YourClassValidatorDto) submitData): Promise<Order> {
|
|
254
|
+
// Custom logic on submit event
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Module({
|
|
259
|
+
imports: [
|
|
260
|
+
WorkflowModule.register({
|
|
261
|
+
providers: [
|
|
262
|
+
{
|
|
263
|
+
provide: OrderWorkflowDefinition,
|
|
264
|
+
useFactory: (orderEntityService: OrderEntityService, eventEmitter: EventEmitter2) => {
|
|
265
|
+
return new OrderWorkflowDefinition(orderEntityService, eventEmitter);
|
|
266
|
+
},
|
|
267
|
+
inject: [OrderEntityService, EventEmitter2]
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
}),
|
|
271
|
+
],
|
|
272
|
+
})
|
|
273
|
+
export class AppModule {}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Message Format
|
|
277
|
+
|
|
278
|
+
The Kafka messages should include the entity URN so that the workflow engine can load the correct entity. For example:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"urn": "order-123",
|
|
283
|
+
"price": 150,
|
|
284
|
+
"items": ["Item 1", "Item 2"]
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
With this setup, your workflow will automatically react to Kafka messages and trigger the appropriate state transitions based on your workflow definition.
|
|
289
|
+
|
|
290
|
+
### Benefits of Using EntityService
|
|
291
|
+
|
|
292
|
+
Using a dedicated EntityService provides several advantages:
|
|
293
|
+
|
|
294
|
+
1. **Separation of Concerns**: Keep entity management logic separate from workflow definitions
|
|
295
|
+
2. **Dependency Injection**: Leverage NestJS dependency injection for your entity operations
|
|
296
|
+
3. **Reusability**: Use the same EntityService across multiple workflows
|
|
297
|
+
4. **Testability**: Easier to mock and test your entity operations
|
|
298
|
+
5. **Database Integration**: Cleanly integrate with your database through repositories
|
|
299
|
+
|
|
300
|
+
This approach is particularly useful for complex applications where entities are stored in databases and require sophisticated loading and persistence logic.
|
|
301
|
+
|
|
302
|
+
## 📚 Examples & Learning Resources
|
|
303
|
+
|
|
304
|
+
### Interactive Examples Repository
|
|
305
|
+
The best way to learn is by exploring our **[comprehensive examples repository](https://github.com/@nestjs-serverless-workflow-examples)** which includes:
|
|
306
|
+
|
|
307
|
+
#### 1. User Onboarding Workflow Example
|
|
308
|
+
Demonstrates a real-world user registration and verification system:
|
|
309
|
+
- Progressive profile completion with automatic transitions
|
|
310
|
+
- Multi-factor authentication flows
|
|
311
|
+
- Risk assessment integration
|
|
312
|
+
- Compliance checks (KYC/AML)
|
|
313
|
+
- States: `REGISTERED` → `EMAIL_VERIFIED` → `PROFILE_COMPLETE` → `IDENTITY_VERIFIED` → `ACTIVE`
|
|
314
|
+
|
|
315
|
+
#### 2. E-Commerce Order Processing Example
|
|
316
|
+
Complete order lifecycle management system:
|
|
317
|
+
- Payment processing with retry logic
|
|
318
|
+
- Inventory reservation and management
|
|
319
|
+
- Multi-state shipping workflows
|
|
320
|
+
- Refund and return handling
|
|
321
|
+
- States: `CREATED` → `PAYMENT_PENDING` → `PAID` → `PROCESSING` → `SHIPPED` → `DELIVERED`
|
|
322
|
+
|
|
323
|
+
#### 3. Message-Driven Inventory Management
|
|
324
|
+
Event-driven inventory system with Message Brokers integration:
|
|
325
|
+
- Real-time stock level updates via Message Broker events
|
|
326
|
+
- Automatic reorder triggering
|
|
327
|
+
- Quality control and quarantine workflows
|
|
328
|
+
- Multi-warehouse support
|
|
329
|
+
- Special states for `QUARANTINE`, `AUDITING`, `DAMAGED`, `EXPIRED`
|
|
330
|
+
|
|
331
|
+
### Running the Examples
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# Clone the examples repository
|
|
335
|
+
git clone https://github.com/@nestjs-serverless-workflow-examples.git
|
|
336
|
+
cd nestjs-workflow-examples
|
|
337
|
+
|
|
338
|
+
# Install all examples
|
|
339
|
+
npm run install:all
|
|
340
|
+
|
|
341
|
+
# Run interactive demos with visual workflow diagrams
|
|
342
|
+
npm run demo:user-onboarding # User onboarding demo
|
|
343
|
+
npm run demo:order-processing # Order processing demo
|
|
344
|
+
npm run demo:kafka-inventory # Kafka inventory demo
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The interactive demos feature:
|
|
348
|
+
- **ASCII-art workflow visualization** showing current state and possible transitions
|
|
349
|
+
- **Real-time state updates** as you interact with the workflow
|
|
350
|
+
- **Menu-driven interface** to trigger events and explore different paths
|
|
351
|
+
- **Automated scenarios** to demonstrate various workflow patterns
|
|
352
|
+
|
|
353
|
+
## Advanced Usage
|
|
354
|
+
For more advanced usage, including custom actions, conditions, and event handling, please check the documentation and explore the examples repository.
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
State Machine flow chart
|
|
358
|
+
```mermaid
|
|
359
|
+
flowchart TD
|
|
360
|
+
Start["emit(event, urn, payload)"]
|
|
361
|
+
LoadEntity["loadEntity(urn)"]
|
|
362
|
+
Found{"entity found?"}
|
|
363
|
+
GetStatus["getEntityStatus(entity)"]
|
|
364
|
+
FindTransition["find matching TransitionEvent (event + state)"]
|
|
365
|
+
NoTransition{"transitionEvent found?"}
|
|
366
|
+
Fallback["call definition.fallback(...) if configured and return"]
|
|
367
|
+
ReturnEntity["return entity"]
|
|
368
|
+
DetermineNext["select concrete transition by evaluating conditions"]
|
|
369
|
+
EventActions["run actionsOnEvent(event) sequentially"]
|
|
370
|
+
InlineActions["run transition.actions (inline) sequentially"]
|
|
371
|
+
FailedCheck{"failed during actions?"}
|
|
372
|
+
UpdateStatus["updateEntityStatus(entity, nextStatus)"]
|
|
373
|
+
StatusActions["run actionsOnStatusChanged(from-to)"]
|
|
374
|
+
PostFailed{"failed after status-change actions?"}
|
|
375
|
+
IdleCheck{"isInIdleStatus(nextStatus)?"}
|
|
376
|
+
FinalCheck{"isInFailedStatus or final?"}
|
|
377
|
+
NextEvent["nextEvent(entity) -> event | null"]
|
|
378
|
+
LoopBack["repeat loop with new currentEvent/currentState"]
|
|
379
|
+
End["return updated entity"]
|
|
380
|
+
|
|
381
|
+
Start --> LoadEntity --> Found
|
|
382
|
+
Found -- no --> ReturnEntity
|
|
383
|
+
Found -- yes --> GetStatus --> FindTransition --> NoTransition
|
|
384
|
+
NoTransition -- no & fallback --> Fallback --> ReturnEntity
|
|
385
|
+
NoTransition -- no & no fallback --> ReturnEntity
|
|
386
|
+
NoTransition -- yes --> DetermineNext --> EventActions --> InlineActions --> FailedCheck
|
|
387
|
+
FailedCheck -- true --> UpdateStatus --> End
|
|
388
|
+
FailedCheck -- false --> UpdateStatus --> StatusActions --> PostFailed
|
|
389
|
+
PostFailed -- true --> UpdateStatus --> End
|
|
390
|
+
PostFailed -- false --> IdleCheck
|
|
391
|
+
IdleCheck -- true --> End
|
|
392
|
+
IdleCheck -- false --> FinalCheck
|
|
393
|
+
FinalCheck -- true --> End
|
|
394
|
+
FinalCheck -- false --> NextEvent --> LoopBack
|
|
395
|
+
```
|
|
396
|
+
````
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lambda.adapater.d.ts","sourceRoot":"","sources":["../../src/adapter/lambda.adapater.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,eAAO,MAAM,kBAAkB,GAC5B,KAAK,uBAAuB,KAAG,UA+D/B,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { OrchestratorService } from '@/workflow';
|
|
2
|
+
// NOTDE:
|
|
3
|
+
// - ReportBatchItemFailures must be enabled on SQS event source mapping
|
|
4
|
+
// - Lambda must have sufficient timeout to process messages
|
|
5
|
+
// - maxReceiveCount should be set as high as possible in main queue
|
|
6
|
+
export const LambdaEventHandler = (app) => async (event, context) => {
|
|
7
|
+
// Calculate safety window (5 seconds before timeout)
|
|
8
|
+
const safetyWindowMs = context.getRemainingTimeInMillis() - 5000;
|
|
9
|
+
const workflowRouter = app.get(OrchestratorService);
|
|
10
|
+
// For timeout retry only
|
|
11
|
+
const batchItemFailures = [];
|
|
12
|
+
// Track processed records
|
|
13
|
+
const processedRecords = new Set();
|
|
14
|
+
// Create a promise that will resolve when we need to shut down
|
|
15
|
+
let shutdownResolver;
|
|
16
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
17
|
+
shutdownResolver = resolve;
|
|
18
|
+
});
|
|
19
|
+
// Set timeout for graceful shutdown
|
|
20
|
+
const shutdownTimer = setTimeout(() => {
|
|
21
|
+
console.log(`Triggering graceful shutdown after ${safetyWindowMs}ms`);
|
|
22
|
+
shutdownResolver();
|
|
23
|
+
}, safetyWindowMs);
|
|
24
|
+
try {
|
|
25
|
+
const processingPromises = event.Records.map(async (record, i) => {
|
|
26
|
+
try {
|
|
27
|
+
const event = JSON.parse(record.body);
|
|
28
|
+
console.log('processing record ', i + 1);
|
|
29
|
+
console.log(event);
|
|
30
|
+
// Race between processing and shutdown
|
|
31
|
+
await Promise.race([
|
|
32
|
+
workflowRouter.transit(event),
|
|
33
|
+
shutdownPromise.then(() => {
|
|
34
|
+
console.log('Shutdown promise...');
|
|
35
|
+
// If we're shutting down and this promise hasn't completed,
|
|
36
|
+
// mark it as a failure so SQS can retry it
|
|
37
|
+
if (!processedRecords.has(record.messageId)) {
|
|
38
|
+
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
39
|
+
console.log(`Marked message ${record.messageId} for retry due to shutdown`);
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
// Mark record as successfully processed
|
|
44
|
+
processedRecords.add(record.messageId);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(`Error processing message ${record.messageId}:`, error);
|
|
48
|
+
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
// Wait for all processing to finish, or for shutdown
|
|
52
|
+
await Promise.race([Promise.all(processingPromises), shutdownPromise]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
// Clean up timeout
|
|
56
|
+
clearTimeout(shutdownTimer);
|
|
57
|
+
}
|
|
58
|
+
console.log(`Completed processing. Failed items: ${batchItemFailures.length}`);
|
|
59
|
+
return {
|
|
60
|
+
batchItemFailures,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=lambda.adapater.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lambda.adapater.js","sourceRoot":"","sources":["../../src/adapter/lambda.adapater.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/event-bus/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/event-bus/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type IBrokerPublisher } from '../types/broker-publisher.interface';
|
|
2
|
+
import { type IWorkflowEvent } from '../types/workflow-event.interface';
|
|
3
|
+
export declare class SqsEmitter implements IBrokerPublisher {
|
|
4
|
+
emit<T>(_payload: IWorkflowEvent<T>): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=sqs.emitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqs.emitter.d.ts","sourceRoot":"","sources":["../../../src/event-bus/sqs/sqs.emitter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,qBAAa,UAAW,YAAW,gBAAgB;IAC3C,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAC1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqs.emitter.js","sourceRoot":"","sources":["../../../src/event-bus/sqs/sqs.emitter.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,IAAI,CAAI,QAA2B,IAAkB,CAAC;CAC7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker-publisher.interface.d.ts","sourceRoot":"","sources":["../../../src/event-bus/types/broker-publisher.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker-publisher.interface.js","sourceRoot":"","sources":["../../../src/event-bus/types/broker-publisher.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/event-bus/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/event-bus/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-event.interface.d.ts","sourceRoot":"","sources":["../../../src/event-bus/types/workflow-event.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-event.interface.js","sourceRoot":"","sources":["../../../src/event-bus/types/workflow-event.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/exception/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/exception/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unretriable.exception.d.ts","sourceRoot":"","sources":["../../src/exception/unretriable.exception.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unretriable.exception.js","sourceRoot":"","sources":["../../src/exception/unretriable.exception.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF"}
|