@brandboostinggmbh/observable-workflows 0.5.0 → 0.7.0
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/README.md +386 -5
- package/dist/index.d.ts +189 -27
- package/dist/index.js +568 -301
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,15 +1,396 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Observable Workflows [](https://npmjs.com/package/@brandboostinggmbh/observable-workflows)
|
|
2
2
|
|
|
3
|
-
[](https://github.com/Brand-Boosting-GmbH/observable-workflows/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A powerful TypeScript library for creating observable, durable workflows with step-by-step tracking, comprehensive logging, and database persistence. Built specifically for Cloudflare Workers and D1 database environments.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔍 **Observable Execution** - Track every step of your workflow with detailed logging
|
|
10
|
+
- 💾 **Durable Persistence** - Automatic state persistence using Cloudflare D1 database
|
|
11
|
+
- 🔄 **Retry Mechanisms** - Built-in workflow retry with step result reuse
|
|
12
|
+
- 📊 **Multi-tenancy Support** - Isolate workflows by tenant for SaaS applications
|
|
13
|
+
- ⚡ **Queue Integration** - Support for queue-based workflow processing
|
|
14
|
+
- 🏷️ **Property Management** - Attach and query custom properties on workflows
|
|
15
|
+
- 🔎 **Advanced Filtering** - Filter workflows by status, properties, date ranges
|
|
16
|
+
- 📝 **Comprehensive Logging** - Structured logging with multiple levels (info, warn, error)
|
|
17
|
+
- 🔧 **TypeScript First** - Full type safety and excellent developer experience
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
8
20
|
|
|
9
21
|
```bash
|
|
10
|
-
npm
|
|
22
|
+
npm install @brandboostinggmbh/observable-workflows
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Define a Workflow
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { defineWorkflow } from '@brandboostinggmbh/observable-workflows'
|
|
31
|
+
|
|
32
|
+
interface EmailInput {
|
|
33
|
+
to: string
|
|
34
|
+
subject: string
|
|
35
|
+
body: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sendEmailWorkflow = defineWorkflow<EmailInput>(
|
|
39
|
+
'send-email',
|
|
40
|
+
async (input, { step, console }) => {
|
|
41
|
+
// Step 1: Validate email
|
|
42
|
+
await step('validate-email', async () => {
|
|
43
|
+
console.log('Validating email address:', input.to)
|
|
44
|
+
if (!input.to.includes('@')) {
|
|
45
|
+
throw new Error('Invalid email address')
|
|
46
|
+
}
|
|
47
|
+
return { valid: true }
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Step 2: Send email
|
|
51
|
+
const result = await step('send-email', async () => {
|
|
52
|
+
console.log('Sending email to:', input.to)
|
|
53
|
+
// Your email sending logic here - simulate async operation
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
55
|
+
return { messageId: 'msg_123', status: 'sent' }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Step 3: Log result
|
|
59
|
+
await step('log-result', async () => {
|
|
60
|
+
console.log('Email sent successfully:', result.messageId)
|
|
61
|
+
return { logged: true }
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Create Workflow Context and Execute
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createWorkflowContext } from '@brandboostinggmbh/observable-workflows'
|
|
73
|
+
|
|
74
|
+
// Create workflow context with D1 database
|
|
75
|
+
const workflowContext = createWorkflowContext({
|
|
76
|
+
D1: env.D1, // Cloudflare D1 database binding
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Execute the workflow
|
|
80
|
+
const result = await workflowContext.call({
|
|
81
|
+
workflow: sendEmailWorkflow,
|
|
82
|
+
input: {
|
|
83
|
+
to: 'user@example.com',
|
|
84
|
+
subject: 'Welcome!',
|
|
85
|
+
body: 'Welcome to our service!',
|
|
86
|
+
},
|
|
87
|
+
workflowName: 'Welcome Email',
|
|
88
|
+
tenantId: 'tenant-123',
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Access Workflow Logs and Data
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { createLogAccessor } from '@brandboostinggmbh/observable-workflows'
|
|
96
|
+
|
|
97
|
+
const logAccessor = createLogAccessor({
|
|
98
|
+
D1: env.D1,
|
|
99
|
+
tenantId: 'tenant-123',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// List recent workflows
|
|
103
|
+
const workflows = await logAccessor.listWorkflows(10, 0)
|
|
104
|
+
|
|
105
|
+
// Get specific workflow with steps and logs
|
|
106
|
+
const workflow = await logAccessor.getWorkflow(instanceId)
|
|
107
|
+
console.log('Workflow status:', workflow.workflowStatus)
|
|
108
|
+
console.log('Steps:', workflow.steps)
|
|
109
|
+
console.log('Logs:', workflow.logs)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Core Concepts
|
|
113
|
+
|
|
114
|
+
### Workflows
|
|
115
|
+
|
|
116
|
+
A workflow is a series of steps that execute in sequence. Each workflow has:
|
|
117
|
+
|
|
118
|
+
- **Type**: A unique identifier for the workflow type
|
|
119
|
+
- **Instance**: A specific execution of a workflow
|
|
120
|
+
- **Steps**: Individual units of work within the workflow
|
|
121
|
+
- **Properties**: Custom metadata that can be attached and queried
|
|
122
|
+
|
|
123
|
+
### Steps
|
|
124
|
+
|
|
125
|
+
Steps are the individual units of work within a workflow. They provide:
|
|
126
|
+
|
|
127
|
+
- **Automatic retry** - Failed steps can be retried
|
|
128
|
+
- **Result caching** - Successful step results are cached for retry scenarios
|
|
129
|
+
- **Isolated logging** - Each step has its own log context
|
|
130
|
+
- **Error handling** - Failed steps capture error details
|
|
131
|
+
|
|
132
|
+
### Tenancy
|
|
133
|
+
|
|
134
|
+
All workflows are isolated by tenant ID, enabling multi-tenant applications:
|
|
135
|
+
|
|
136
|
+
- Each workflow execution requires a `tenantId`
|
|
137
|
+
- Queries are automatically scoped to the tenant
|
|
138
|
+
- Complete data isolation between tenants
|
|
139
|
+
|
|
140
|
+
## Advanced Usage
|
|
141
|
+
|
|
142
|
+
### Workflow with Metadata and Properties
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const processOrderWorkflow = defineWorkflow(
|
|
146
|
+
{
|
|
147
|
+
workflowType: 'process-order',
|
|
148
|
+
metadata: { version: '1.0', category: 'ecommerce' },
|
|
149
|
+
},
|
|
150
|
+
async (input, { step, console, setWorkflowProperty, setWorkflowName }) => {
|
|
151
|
+
// Set dynamic workflow name
|
|
152
|
+
await setWorkflowName(`Order ${input.orderId}`)
|
|
153
|
+
|
|
154
|
+
// Set searchable properties
|
|
155
|
+
await setWorkflowProperty('orderId', input.orderId)
|
|
156
|
+
await setWorkflowProperty('customerId', input.customerId)
|
|
157
|
+
await setWorkflowProperty('amount', input.amount)
|
|
158
|
+
|
|
159
|
+
const result = await step('process-payment', async () => {
|
|
160
|
+
// Payment processing logic - simulate async operation
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
162
|
+
return { transactionId: 'txn_123' }
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
await step('update-inventory', async () => {
|
|
166
|
+
// Inventory update logic - simulate async operation
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
168
|
+
return { updated: true }
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Retry Failed Workflows
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Retry a failed workflow, reusing successful step results
|
|
180
|
+
await workflowContext.retry(processOrderWorkflow, failedWorkflowId, {
|
|
181
|
+
reuseSuccessfulSteps: true, // Default: true
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Queue-based Workflow Processing
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { createQueueWorkflowContext } from '@brandboostinggmbh/observable-workflows'
|
|
189
|
+
|
|
190
|
+
const queueContext = createQueueWorkflowContext({
|
|
191
|
+
workflowContext,
|
|
192
|
+
queue: env.WORKFLOW_QUEUE, // Cloudflare Queue binding
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Queue a workflow for processing
|
|
196
|
+
await queueContext.queueWorkflow({
|
|
197
|
+
workflow: sendEmailWorkflow,
|
|
198
|
+
input: emailData,
|
|
199
|
+
workflowName: 'Queued Email',
|
|
200
|
+
tenantId: 'tenant-123',
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Advanced Filtering and Querying
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Filter workflows by properties and status
|
|
208
|
+
const filteredWorkflows = await logAccessor.listWorkflows(50, 0, {
|
|
209
|
+
workflowStatus: ['completed', 'failed'],
|
|
210
|
+
properties: {
|
|
211
|
+
amount: { gt: 100 }, // Amount greater than 100
|
|
212
|
+
customerId: { equals: 'customer-456' },
|
|
213
|
+
},
|
|
214
|
+
startTime: {
|
|
215
|
+
gte: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
|
|
216
|
+
},
|
|
217
|
+
workflowType: { contains: 'order' },
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Get workflow properties
|
|
221
|
+
const properties = await logAccessor.getWorkflowProperties(instanceId)
|
|
11
222
|
```
|
|
12
223
|
|
|
224
|
+
## API Reference
|
|
225
|
+
|
|
226
|
+
### Core Functions
|
|
227
|
+
|
|
228
|
+
#### `defineWorkflow<T>(type, callback)`
|
|
229
|
+
|
|
230
|
+
Defines a new workflow type.
|
|
231
|
+
|
|
232
|
+
**Parameters:**
|
|
233
|
+
|
|
234
|
+
- `type` (string | object): Workflow type or configuration object
|
|
235
|
+
- `callback` (function): Workflow implementation function
|
|
236
|
+
|
|
237
|
+
**Returns:** `WorkflowFunction<T>`
|
|
238
|
+
|
|
239
|
+
#### `createWorkflowContext(options)`
|
|
240
|
+
|
|
241
|
+
Creates a workflow execution context.
|
|
242
|
+
|
|
243
|
+
**Parameters:**
|
|
244
|
+
|
|
245
|
+
- `options.D1` (D1Database): Cloudflare D1 database instance
|
|
246
|
+
- `options.serializer?` (Serializer): Custom serialization (optional)
|
|
247
|
+
- `options.idFactory?` (function): Custom ID generation (optional)
|
|
248
|
+
|
|
249
|
+
**Returns:** `WorkflowContextInstance`
|
|
250
|
+
|
|
251
|
+
#### `createLogAccessor(options)`
|
|
252
|
+
|
|
253
|
+
Creates an accessor for querying workflow data.
|
|
254
|
+
|
|
255
|
+
**Parameters:**
|
|
256
|
+
|
|
257
|
+
- `options.D1` (D1Database): Cloudflare D1 database instance
|
|
258
|
+
- `options.tenantId` (string): Tenant identifier
|
|
259
|
+
- `options.serializer?` (Serializer): Custom serialization (optional)
|
|
260
|
+
|
|
261
|
+
**Returns:** Log accessor with query methods
|
|
262
|
+
|
|
263
|
+
### Workflow Context Methods
|
|
264
|
+
|
|
265
|
+
#### `call(params)`
|
|
266
|
+
|
|
267
|
+
Executes a workflow.
|
|
268
|
+
|
|
269
|
+
**Parameters (object):**
|
|
270
|
+
|
|
271
|
+
- `workflow` (WorkflowFunction): The workflow to execute
|
|
272
|
+
- `input` (T): Input data for the workflow
|
|
273
|
+
- `workflowName` (string): Human-readable workflow name
|
|
274
|
+
- `tenantId` (string): Tenant identifier
|
|
275
|
+
- `parentInstanceId?` (string): Parent workflow ID (for retries)
|
|
276
|
+
- `reuseSuccessfulSteps?` (boolean): Whether to reuse successful steps in retries
|
|
277
|
+
|
|
278
|
+
#### `retry(workflow, retryInstanceId, retryOptions?)`
|
|
279
|
+
|
|
280
|
+
Retries a failed workflow.
|
|
281
|
+
|
|
282
|
+
**Parameters:**
|
|
283
|
+
|
|
284
|
+
- `workflow` (WorkflowFunction): The workflow definition
|
|
285
|
+
- `retryInstanceId` (string): ID of the workflow to retry
|
|
286
|
+
- `retryOptions?` (RetryWorkflowOptions): Retry configuration
|
|
287
|
+
|
|
288
|
+
### Step Context
|
|
289
|
+
|
|
290
|
+
Within a workflow, the step function provides:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
await step('step-name', async ({ console }) => {
|
|
294
|
+
console.log('Step is executing')
|
|
295
|
+
console.error('Something went wrong')
|
|
296
|
+
console.warn('Warning message')
|
|
297
|
+
// Simulate async operation
|
|
298
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
299
|
+
// Return step result
|
|
300
|
+
return { success: true }
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Workflow Context
|
|
305
|
+
|
|
306
|
+
The workflow callback receives a context with:
|
|
307
|
+
|
|
308
|
+
- `step` (function): Execute a workflow step
|
|
309
|
+
- `console` (ConsoleWrapper): Logging interface
|
|
310
|
+
- `setWorkflowName` (function): Update workflow name
|
|
311
|
+
- `setWorkflowProperty` (function): Set workflow property
|
|
312
|
+
|
|
313
|
+
## Configuration
|
|
314
|
+
|
|
315
|
+
### Custom Serialization
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { createWorkflowContext } from '@brandboostinggmbh/observable-workflows'
|
|
319
|
+
|
|
320
|
+
const customSerializer = {
|
|
321
|
+
serialize: (obj: any) => JSON.stringify(obj),
|
|
322
|
+
deserialize: (str: string) => JSON.parse(str),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const context = createWorkflowContext({
|
|
326
|
+
D1: env.D1,
|
|
327
|
+
serializer: customSerializer,
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Custom ID Generation
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const customIdFactory = () => `custom_${Date.now()}_${Math.random()}`
|
|
335
|
+
|
|
336
|
+
const context = createWorkflowContext({
|
|
337
|
+
D1: env.D1,
|
|
338
|
+
idFactory: customIdFactory,
|
|
339
|
+
})
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Database Schema
|
|
343
|
+
|
|
344
|
+
The library automatically creates the following tables in your D1 database:
|
|
345
|
+
|
|
346
|
+
- **WorkflowTable**: Stores workflow instances
|
|
347
|
+
- **StepTable**: Stores individual step executions
|
|
348
|
+
- **LogTable**: Stores all log entries
|
|
349
|
+
- **WorkflowProperties**: Stores custom workflow properties
|
|
350
|
+
|
|
351
|
+
Tables are created automatically when first accessing the database.
|
|
352
|
+
|
|
353
|
+
## Error Handling
|
|
354
|
+
|
|
355
|
+
The library provides comprehensive error handling:
|
|
356
|
+
|
|
357
|
+
- **Step Failures**: Captured with full error details and stack traces
|
|
358
|
+
- **Workflow Failures**: Marked with appropriate status and error information
|
|
359
|
+
- **Retry Logic**: Failed workflows can be retried with step result reuse
|
|
360
|
+
- **Validation**: Input validation and type checking
|
|
361
|
+
|
|
362
|
+
## Best Practices
|
|
363
|
+
|
|
364
|
+
1. **Use Descriptive Step Names**: Make step names descriptive for better observability
|
|
365
|
+
2. **Keep Steps Atomic**: Each step should represent a single unit of work
|
|
366
|
+
3. **Handle Errors Gracefully**: Use try-catch within steps for custom error handling
|
|
367
|
+
4. **Use Properties for Searching**: Add relevant properties to make workflows searchable
|
|
368
|
+
5. **Monitor Performance**: Use the logging data to monitor workflow performance
|
|
369
|
+
6. **Tenant Isolation**: Always use consistent tenant IDs for proper data isolation
|
|
370
|
+
|
|
371
|
+
## TypeScript Support
|
|
372
|
+
|
|
373
|
+
This library is built with TypeScript and provides full type safety:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
interface MyInput {
|
|
377
|
+
userId: string
|
|
378
|
+
data: Record<string, any>
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const typedWorkflow = defineWorkflow<MyInput>(
|
|
382
|
+
'my-workflow',
|
|
383
|
+
async (input, ctx) => {
|
|
384
|
+
// input is fully typed as MyInput
|
|
385
|
+
// ctx provides typed workflow context
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Contributing
|
|
391
|
+
|
|
392
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
|
|
393
|
+
|
|
13
394
|
## License
|
|
14
395
|
|
|
15
396
|
[MIT](./LICENSE) License © 2025 [Tim Stepanov](https://github.com/TimStepanovAtBrandBoosting)
|
package/dist/index.d.ts
CHANGED
|
@@ -49,7 +49,9 @@ declare function insertStepRecordFull(context: StepContextOptions, {
|
|
|
49
49
|
startTime,
|
|
50
50
|
endTime,
|
|
51
51
|
result,
|
|
52
|
-
error
|
|
52
|
+
error,
|
|
53
|
+
resultRef,
|
|
54
|
+
errorRef
|
|
53
55
|
}: {
|
|
54
56
|
instanceId: string;
|
|
55
57
|
name: string;
|
|
@@ -59,6 +61,8 @@ declare function insertStepRecordFull(context: StepContextOptions, {
|
|
|
59
61
|
endTime: number | null;
|
|
60
62
|
result: string | null;
|
|
61
63
|
error: string | null;
|
|
64
|
+
resultRef?: string | null;
|
|
65
|
+
errorRef?: string | null;
|
|
62
66
|
}): Promise<D1Result<Record<string, unknown>>>;
|
|
63
67
|
declare function pushLogToDB(options: InternalWorkflowContextOptions, {
|
|
64
68
|
instanceId,
|
|
@@ -78,6 +82,18 @@ declare function pushLogToDB(options: InternalWorkflowContextOptions, {
|
|
|
78
82
|
tenantId: string;
|
|
79
83
|
}): Promise<D1Result<Record<string, unknown>>>;
|
|
80
84
|
declare function tryDeserializeObj(obj: string, serializer: Serializer): any;
|
|
85
|
+
/**
|
|
86
|
+
* Serialize data and store externally if it exceeds the threshold
|
|
87
|
+
* Returns tuple of [data, externalRef] where one will be non-null
|
|
88
|
+
*/
|
|
89
|
+
declare function serializeWithExternalStorage(obj: any, serializer: Serializer, externalBlobStorage?: ExternalBlobStorage): Promise<{
|
|
90
|
+
data: string | null;
|
|
91
|
+
externalRef: string | null;
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* Deserialize data from direct storage or external storage
|
|
95
|
+
*/
|
|
96
|
+
declare function deserializeWithExternalStorage(data: string | null, externalRef: string | null, serializer: Serializer, externalBlobStorage?: ExternalBlobStorage): Promise<any>;
|
|
81
97
|
/**
|
|
82
98
|
* We want to save steps to a databse, we are using D1, so SQLite is the database.
|
|
83
99
|
* Step Table:
|
|
@@ -98,7 +114,8 @@ declare function tryDeserializeObj(obj: string, serializer: Serializer): any;
|
|
|
98
114
|
* - type: string (info, error, warning)
|
|
99
115
|
*/
|
|
100
116
|
/**
|
|
101
|
-
* Ensure that the required tables exist in D1 (SQLite).
|
|
117
|
+
* Ensure that the required tables exist in D1 (SQLite) and migrate schema if needed.
|
|
118
|
+
* This function is idempotent and ensures the schema matches the current requirements.
|
|
102
119
|
*/
|
|
103
120
|
declare function ensureTables(db: D1Database): Promise<void>;
|
|
104
121
|
declare function workflowTableRowToWorkflowRun(row: {
|
|
@@ -106,12 +123,13 @@ declare function workflowTableRowToWorkflowRun(row: {
|
|
|
106
123
|
workflowType: string;
|
|
107
124
|
workflowName: string;
|
|
108
125
|
input: string;
|
|
126
|
+
inputRef: string | null;
|
|
109
127
|
tenantId: string;
|
|
110
128
|
workflowStatus: StepWorkflowStatus;
|
|
111
129
|
startTime: number;
|
|
112
130
|
endTime: number | null;
|
|
113
131
|
parentInstanceId: string | null;
|
|
114
|
-
}, serializer: Serializer): WorkflowRun
|
|
132
|
+
}, serializer: Serializer, externalBlobStorage?: ExternalBlobStorage): Promise<WorkflowRun>;
|
|
115
133
|
declare function updateWorkflowName(context: {
|
|
116
134
|
D1: D1Database;
|
|
117
135
|
}, instanceId: string, newWorkflowName: string): Promise<D1Result<Record<string, unknown>>>;
|
|
@@ -149,6 +167,8 @@ type StepContextOptions = {
|
|
|
149
167
|
idFactory: () => string;
|
|
150
168
|
/** If true this step context will attempt to reuse results from parent instance steps where possible. Defaults to True */
|
|
151
169
|
reuseSuccessfulSteps?: boolean;
|
|
170
|
+
/** Optional external blob storage for large data that exceeds D1 size limits */
|
|
171
|
+
externalBlobStorage?: ExternalBlobStorage;
|
|
152
172
|
};
|
|
153
173
|
type ConsoleWrapper = {
|
|
154
174
|
log: (message?: any, ...optionalParams: any[]) => void;
|
|
@@ -205,15 +225,120 @@ type WorkflowPropertyDefinition = {
|
|
|
205
225
|
key: string;
|
|
206
226
|
valueType: 'string' | 'number' | 'boolean' | 'object';
|
|
207
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* Date range filter for filtering workflows by time periods
|
|
230
|
+
*/
|
|
231
|
+
type DateRangeFilter = {
|
|
232
|
+
/** Greater than or equal to (inclusive) */
|
|
233
|
+
gte?: number;
|
|
234
|
+
/** Less than or equal to (inclusive) */
|
|
235
|
+
lte?: number;
|
|
236
|
+
/** Greater than (exclusive) */
|
|
237
|
+
gt?: number;
|
|
238
|
+
/** Less than (exclusive) */
|
|
239
|
+
lt?: number;
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* String filter supporting exact match or substring matching
|
|
243
|
+
*/
|
|
244
|
+
type StringFilter = string | {
|
|
245
|
+
equals: string;
|
|
246
|
+
} | {
|
|
247
|
+
contains: string;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Property filter supporting various comparison operators for custom workflow properties
|
|
251
|
+
*/
|
|
252
|
+
type PropertyFilter = {
|
|
253
|
+
/** Exact match */
|
|
254
|
+
equals?: any;
|
|
255
|
+
/** Substring match (for string values) */
|
|
256
|
+
contains?: string;
|
|
257
|
+
/** Greater than (for numeric values) */
|
|
258
|
+
gt?: number;
|
|
259
|
+
/** Greater than or equal to (for numeric values) */
|
|
260
|
+
gte?: number;
|
|
261
|
+
/** Less than (for numeric values) */
|
|
262
|
+
lt?: number;
|
|
263
|
+
/** Less than or equal to (for numeric values) */
|
|
264
|
+
lte?: number;
|
|
265
|
+
/** Match any value in the array */
|
|
266
|
+
in?: any[];
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Comprehensive filter object for workflow listing with support for:
|
|
270
|
+
* - Text search across workflow names and types
|
|
271
|
+
* - Direct field filtering with various operators
|
|
272
|
+
* - Date range filtering
|
|
273
|
+
* - Custom property filtering with joins
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* // Text search
|
|
278
|
+
* const workflows = await listWorkflows(10, 0, { search: "payment" })
|
|
279
|
+
*
|
|
280
|
+
* // Status and type filtering
|
|
281
|
+
* const workflows = await listWorkflows(10, 0, {
|
|
282
|
+
* workflowStatus: ["completed", "failed"],
|
|
283
|
+
* workflowType: "PaymentWorkflow"
|
|
284
|
+
* })
|
|
285
|
+
*
|
|
286
|
+
* // Date range filtering
|
|
287
|
+
* const workflows = await listWorkflows(10, 0, {
|
|
288
|
+
* startTime: { gte: Date.now() - 86400000 } // Last 24 hours
|
|
289
|
+
* })
|
|
290
|
+
*
|
|
291
|
+
* // Property filtering
|
|
292
|
+
* const workflows = await listWorkflows(10, 0, {
|
|
293
|
+
* properties: {
|
|
294
|
+
* amount: { gt: 100, lte: 1000 },
|
|
295
|
+
* currency: { equals: "USD" },
|
|
296
|
+
* customer: { contains: "acme" }
|
|
297
|
+
* }
|
|
298
|
+
* })
|
|
299
|
+
*
|
|
300
|
+
* // Combined filtering
|
|
301
|
+
* const workflows = await listWorkflows(10, 0, {
|
|
302
|
+
* search: "payment",
|
|
303
|
+
* workflowStatus: "completed",
|
|
304
|
+
* startTime: { gte: Date.now() - 86400000 },
|
|
305
|
+
* properties: { amount: { gt: 100 } }
|
|
306
|
+
* })
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
type WorkflowFilter = {
|
|
310
|
+
/** Text search across workflow name and type */
|
|
311
|
+
search?: string;
|
|
312
|
+
/** Filter by workflow type (exact match) */
|
|
313
|
+
workflowType?: string | string[];
|
|
314
|
+
/** Filter by workflow status */
|
|
315
|
+
workflowStatus?: StepWorkflowStatus | StepWorkflowStatus[];
|
|
316
|
+
/** Filter by workflow name with string matching */
|
|
317
|
+
workflowName?: StringFilter;
|
|
318
|
+
/** Filter by workflow start time */
|
|
319
|
+
startTime?: DateRangeFilter;
|
|
320
|
+
/** Filter by workflow end time */
|
|
321
|
+
endTime?: DateRangeFilter;
|
|
322
|
+
/** Filter by custom workflow properties */
|
|
323
|
+
properties?: {
|
|
324
|
+
[key: string]: PropertyFilter;
|
|
325
|
+
};
|
|
326
|
+
};
|
|
208
327
|
type StepContext = Awaited<ReturnType<typeof createStepContext>>;
|
|
209
328
|
type Serializer = {
|
|
210
329
|
serialize: (data: any) => string;
|
|
211
330
|
deserialize: (data: string) => any;
|
|
212
331
|
};
|
|
332
|
+
type ExternalBlobStorage = {
|
|
333
|
+
threshold: number;
|
|
334
|
+
set: (data: string) => Promise<string>;
|
|
335
|
+
get: (id: string) => Promise<string>;
|
|
336
|
+
};
|
|
213
337
|
type WorkflowContextOptions = {
|
|
214
338
|
D1: D1Database;
|
|
215
339
|
idFactory?: () => string;
|
|
216
340
|
serializer?: Serializer;
|
|
341
|
+
externalBlobStorage?: ExternalBlobStorage;
|
|
217
342
|
};
|
|
218
343
|
type InternalWorkflowContextOptions = WorkflowContextOptions & Required<Pick<WorkflowContextOptions, 'serializer' | 'idFactory'>>;
|
|
219
344
|
type WorkflowContextInstance = {
|
|
@@ -263,43 +388,36 @@ type RetryWorkflowOptions = {
|
|
|
263
388
|
reuseSuccessfulSteps?: boolean;
|
|
264
389
|
};
|
|
265
390
|
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/observableWorkflows/defineWorkflow.d.ts
|
|
268
|
-
declare function defineWorkflow<I extends {} | null>(workflow: {
|
|
269
|
-
workflowType: string;
|
|
270
|
-
metadata?: Record<string, any>;
|
|
271
|
-
}, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
|
|
272
|
-
declare function defineWorkflow<I extends {} | null>(workflowType: string, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
|
|
273
|
-
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region src/observableWorkflows/createWorkflowContext.d.ts
|
|
276
|
-
declare function createWorkflowContext(options: WorkflowContextOptions): WorkflowContextInstance;
|
|
277
|
-
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/observableWorkflows/createQueueWorkflowContext.d.ts
|
|
280
|
-
declare function createQueueWorkflowContext(options: QueueWorkflowContextOptions): {
|
|
281
|
-
enqueueWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, input: I, initialName: string) => Promise<void>;
|
|
282
|
-
enqueueRetryWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, oldInstanceId: string) => Promise<void>;
|
|
283
|
-
handleWorkflowQueueMessage: (message: WorkflowQueueMessage, env: {
|
|
284
|
-
LOG_DB: D1Database;
|
|
285
|
-
}, workflowResolver: (workflowType: string) => WorkflowFunction<any> | undefined) => Promise<void>;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
391
|
//#endregion
|
|
289
392
|
//#region src/observableWorkflows/createLogAccessor.d.ts
|
|
290
393
|
declare const createLogAccessor: (context: {
|
|
291
394
|
D1: D1Database;
|
|
292
395
|
tenantId: string;
|
|
293
396
|
serializer?: Serializer;
|
|
397
|
+
externalBlobStorage?: ExternalBlobStorage;
|
|
294
398
|
}) => {
|
|
295
399
|
listSteps: (limit: number, offset: number, instanceId?: string | undefined) => Promise<Step[]>;
|
|
296
400
|
getStep: (instanceId: string, stepName: string) => Promise<Step | null>;
|
|
297
|
-
listWorkflows: (limit: number, offset: number) => Promise<WorkflowRun[]>;
|
|
401
|
+
listWorkflows: (limit: number, offset: number, filter?: WorkflowFilter) => Promise<WorkflowRun[]>;
|
|
298
402
|
getWorkflow: (instanceId: string) => Promise<WorkflowRun | null>;
|
|
299
403
|
getWorkflowTypesByTenantId: (tenantId: string) => Promise<string[]>;
|
|
300
404
|
getPropertiesKeys: (instanceId?: string) => Promise<WorkflowPropertyDefinition[]>;
|
|
301
405
|
};
|
|
302
406
|
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/observableWorkflows/createQueueWorkflowContext.d.ts
|
|
409
|
+
declare function createQueueWorkflowContext(options: QueueWorkflowContextOptions): {
|
|
410
|
+
enqueueWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, input: I, initialName: string) => Promise<void>;
|
|
411
|
+
enqueueRetryWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, oldInstanceId: string) => Promise<void>;
|
|
412
|
+
handleWorkflowQueueMessage: (message: WorkflowQueueMessage, env: {
|
|
413
|
+
LOG_DB: D1Database;
|
|
414
|
+
}, workflowResolver: (workflowType: string) => WorkflowFunction<any> | undefined) => Promise<void>;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/observableWorkflows/createWorkflowContext.d.ts
|
|
419
|
+
declare function createWorkflowContext(options: WorkflowContextOptions): WorkflowContextInstance;
|
|
420
|
+
|
|
303
421
|
//#endregion
|
|
304
422
|
//#region src/observableWorkflows/defaultImplementations.d.ts
|
|
305
423
|
declare const defaultSerializer: {
|
|
@@ -312,4 +430,48 @@ declare const defaultSerializer: {
|
|
|
312
430
|
declare const defaultIdFactory: () => string;
|
|
313
431
|
|
|
314
432
|
//#endregion
|
|
315
|
-
|
|
433
|
+
//#region src/observableWorkflows/defineWorkflow.d.ts
|
|
434
|
+
declare function defineWorkflow<I extends {} | null>(workflow: {
|
|
435
|
+
workflowType: string;
|
|
436
|
+
metadata?: Record<string, any>;
|
|
437
|
+
}, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
|
|
438
|
+
declare function defineWorkflow<I extends {} | null>(workflowType: string, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
|
|
439
|
+
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/observableWorkflows/r2ExternalBlobStorage.d.ts
|
|
442
|
+
/**
|
|
443
|
+
* Configuration options for Cloudflare R2 external blob storage
|
|
444
|
+
*/
|
|
445
|
+
type R2ExternalBlobStorageOptions = {
|
|
446
|
+
/** The R2 bucket instance to use for storage */
|
|
447
|
+
bucket: R2Bucket;
|
|
448
|
+
/** Size threshold in bytes for when to use external storage */
|
|
449
|
+
threshold: number;
|
|
450
|
+
/** Optional prefix for all keys stored in R2 */
|
|
451
|
+
keyPrefix?: string;
|
|
452
|
+
/** Optional custom ID factory for generating unique keys */
|
|
453
|
+
idFactory?: () => string;
|
|
454
|
+
};
|
|
455
|
+
/**
|
|
456
|
+
* Cloudflare R2 implementation of ExternalBlobStorage interface
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```typescript
|
|
460
|
+
* import { createWorkflowContext, createR2ExternalBlobStorage } from '@brandboostinggmbh/observable-workflows'
|
|
461
|
+
*
|
|
462
|
+
* const externalBlobStorage = createR2ExternalBlobStorage({
|
|
463
|
+
* bucket: env.MY_R2_BUCKET,
|
|
464
|
+
* threshold: 1024 * 1024, // 1MB threshold
|
|
465
|
+
* keyPrefix: 'workflows/', // Optional prefix
|
|
466
|
+
* })
|
|
467
|
+
*
|
|
468
|
+
* const workflowContext = createWorkflowContext({
|
|
469
|
+
* D1: env.D1,
|
|
470
|
+
* externalBlobStorage,
|
|
471
|
+
* })
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
declare function createR2ExternalBlobStorage(options: R2ExternalBlobStorageOptions): ExternalBlobStorage;
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
export { ConsoleWrapper, DateRangeFilter, ExternalBlobStorage, InternalWorkflowContextOptions, Log, PossibleValueTypeNames, PossibleValueTypes, PropertyFilter, QueueWorkflowContextOptions, R2ExternalBlobStorageOptions, Serializer, Step, StepContextOptions, StepCtx, StepWorkflowStatus, StringFilter, ValueTypeMap, WorkflowContext, WorkflowContextInstance, WorkflowContextOptions, WorkflowFilter, WorkflowFunction, WorkflowProperty, WorkflowPropertyDefinition, WorkflowQueueMessage, WorkflowRun, createLogAccessor, createQueueWorkflowContext, createR2ExternalBlobStorage, createStepContext, createWorkflowContext, defaultIdFactory, defaultSerializer, defineWorkflow, deserializeWithExternalStorage, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, serializeWithExternalStorage, tryDeserializeObj, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
|