@entergreat/step-pipeline 1.1.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/.npmrc.example +5 -0
- package/README.md +496 -0
- package/package.json +26 -0
- package/src/core/StepDefinition.js +158 -0
- package/src/core/StepExecutor.js +237 -0
- package/src/core/StepPipeline.js +178 -0
- package/src/core/StepRegistry.js +124 -0
- package/src/index.js +31 -0
- package/src/utils/helpers.js +79 -0
package/.npmrc.example
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# @entergreat/step-pipeline
|
|
2
|
+
|
|
3
|
+
Reusable step-based pipeline framework for EnterGreat services. Build robust, resumable, and maintainable data processing pipelines with ease.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Step-based architecture** - Break complex workflows into manageable steps
|
|
8
|
+
- 🔄 **Resume capability** - Start from any step in the pipeline
|
|
9
|
+
- 🔁 **Automatic retries** - Configure retries per step
|
|
10
|
+
- ⏱️ **Timeout support** - Set execution timeouts for steps
|
|
11
|
+
- 📊 **Built-in metrics** - Track execution time and success rates
|
|
12
|
+
- 🎯 **Conditional execution** - Execute steps based on conditions
|
|
13
|
+
- 🔌 **Event system** - Hook into step lifecycle events
|
|
14
|
+
- 📝 **Comprehensive logging** - Configurable log levels
|
|
15
|
+
- 🛡️ **Error handling** - Graceful error handling with detailed reporting
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @entergreat/step-pipeline
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
|
|
27
|
+
|
|
28
|
+
// Create a pipeline
|
|
29
|
+
const pipeline = new StepPipeline();
|
|
30
|
+
|
|
31
|
+
// Define your steps
|
|
32
|
+
const steps = [
|
|
33
|
+
new StepDefinition(
|
|
34
|
+
0,
|
|
35
|
+
'Load Data',
|
|
36
|
+
'Load data from source',
|
|
37
|
+
async (context) => {
|
|
38
|
+
// Your logic here
|
|
39
|
+
return { data: 'loaded' };
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
new StepDefinition(
|
|
43
|
+
1,
|
|
44
|
+
'Process Data',
|
|
45
|
+
'Process the loaded data',
|
|
46
|
+
async (context) => {
|
|
47
|
+
// Your logic here
|
|
48
|
+
return { processed: true };
|
|
49
|
+
}
|
|
50
|
+
),
|
|
51
|
+
new StepDefinition(
|
|
52
|
+
2,
|
|
53
|
+
'Save Results',
|
|
54
|
+
'Save processed results',
|
|
55
|
+
async (context) => {
|
|
56
|
+
// Your logic here
|
|
57
|
+
return { saved: true };
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Register steps
|
|
63
|
+
pipeline.registerSteps(steps);
|
|
64
|
+
|
|
65
|
+
// Execute the pipeline
|
|
66
|
+
const context = { userId: '123', timestamp: Date.now() };
|
|
67
|
+
const results = await pipeline.executeAll(context);
|
|
68
|
+
|
|
69
|
+
// Print summary
|
|
70
|
+
pipeline.printSummary(results);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Core Concepts
|
|
74
|
+
|
|
75
|
+
### StepDefinition
|
|
76
|
+
|
|
77
|
+
Defines a single step in your pipeline:
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
new StepDefinition(
|
|
81
|
+
id, // Unique step ID (number)
|
|
82
|
+
name, // Display name (string)
|
|
83
|
+
description, // What this step does (string)
|
|
84
|
+
handler, // Async function to execute (async function)
|
|
85
|
+
options // Optional configuration (object)
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### StepPipeline
|
|
90
|
+
|
|
91
|
+
Main class for managing and executing pipelines:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const pipeline = new StepPipeline({
|
|
95
|
+
logLevel: 'info', // 'error', 'warn', 'info', 'debug'
|
|
96
|
+
stopOnError: true, // Stop pipeline on step failure
|
|
97
|
+
enableMetrics: true // Track execution metrics
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Context
|
|
102
|
+
|
|
103
|
+
Data object that flows through all steps:
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const context = {
|
|
107
|
+
// Your custom data
|
|
108
|
+
userId: '123',
|
|
109
|
+
input: 'some data',
|
|
110
|
+
// Context is mutable and can be updated by steps
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
### StepPipeline Methods
|
|
117
|
+
|
|
118
|
+
#### `registerSteps(stepDefinitions)`
|
|
119
|
+
Register multiple steps at once.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
pipeline.registerSteps([step1, step2, step3]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `executeAll(context, startStep?)`
|
|
126
|
+
Execute all registered steps from start to end.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
// Execute all steps
|
|
130
|
+
await pipeline.executeAll(context);
|
|
131
|
+
|
|
132
|
+
// Resume from step 5
|
|
133
|
+
await pipeline.executeAll(context, 5);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### `executeStep(stepId, context, skip?)`
|
|
137
|
+
Execute a single step by ID.
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
const result = await pipeline.executeStep(2, context);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `executeRange(startStep, endStep, context)`
|
|
144
|
+
Execute a specific range of steps.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// Execute steps 2 through 5
|
|
148
|
+
await pipeline.executeRange(2, 5, context);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### `executeSteps(stepIds, context)`
|
|
152
|
+
Execute only specific steps by IDs.
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
// Execute only steps 1, 3, and 5
|
|
156
|
+
await pipeline.executeSteps([1, 3, 5], context);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `listSteps()`
|
|
160
|
+
Get a list of all registered steps.
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const steps = pipeline.listSteps();
|
|
164
|
+
// Returns: [{ id, name, description, options }, ...]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### `getStepCount()`
|
|
168
|
+
Get the total number of registered steps.
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
const count = pipeline.getStepCount();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### `getSummary(results)`
|
|
175
|
+
Get execution summary with statistics.
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
const summary = pipeline.getSummary(results);
|
|
179
|
+
// Returns: { total, successful, failed, skipped, successRate, totalDuration }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `printSummary(results)`
|
|
183
|
+
Print formatted execution summary to console.
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
pipeline.printSummary(results);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Step Options
|
|
190
|
+
|
|
191
|
+
Configure individual steps with advanced options:
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
new StepDefinition(
|
|
195
|
+
5,
|
|
196
|
+
'Flaky API Call',
|
|
197
|
+
'Call external API with retries',
|
|
198
|
+
handler,
|
|
199
|
+
{
|
|
200
|
+
retries: 3, // Retry 3 times on failure
|
|
201
|
+
retryDelay: 1000, // Wait 1s between retries
|
|
202
|
+
timeout: 30000, // Timeout after 30s
|
|
203
|
+
skip: false, // Skip this step
|
|
204
|
+
condition: (context) => context.shouldRun // Conditional execution
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Event System
|
|
210
|
+
|
|
211
|
+
Hook into pipeline lifecycle events:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
pipeline
|
|
215
|
+
.on('stepStart', (stepId, stepName, context) => {
|
|
216
|
+
console.log(`Starting ${stepName}...`);
|
|
217
|
+
})
|
|
218
|
+
.on('stepComplete', (stepId, stepName, result, duration) => {
|
|
219
|
+
console.log(`Completed ${stepName} in ${duration}s`);
|
|
220
|
+
})
|
|
221
|
+
.on('stepError', (stepId, stepName, error, duration) => {
|
|
222
|
+
console.error(`Failed ${stepName}: ${error.message}`);
|
|
223
|
+
})
|
|
224
|
+
.on('stepSkipped', (stepId, stepName) => {
|
|
225
|
+
console.log(`Skipped ${stepName}`);
|
|
226
|
+
})
|
|
227
|
+
.on('sequenceStart', (startId, endId, context) => {
|
|
228
|
+
console.log(`Starting sequence ${startId} to ${endId}`);
|
|
229
|
+
})
|
|
230
|
+
.on('sequenceComplete', (results) => {
|
|
231
|
+
console.log(`Sequence completed with ${results.length} steps`);
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Usage Examples
|
|
236
|
+
|
|
237
|
+
### Example 1: Basic Pipeline
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
|
|
241
|
+
|
|
242
|
+
class DataProcessor {
|
|
243
|
+
constructor() {
|
|
244
|
+
this.pipeline = new StepPipeline();
|
|
245
|
+
this.registerSteps();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
registerSteps() {
|
|
249
|
+
const steps = [
|
|
250
|
+
new StepDefinition(0, 'Validate Input', 'Validate input data', this.validate.bind(this)),
|
|
251
|
+
new StepDefinition(1, 'Transform Data', 'Transform to required format', this.transform.bind(this)),
|
|
252
|
+
new StepDefinition(2, 'Save Results', 'Save to database', this.save.bind(this))
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
this.pipeline.registerSteps(steps);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async validate(context) {
|
|
259
|
+
if (!context.data) throw new Error('No data provided');
|
|
260
|
+
return { valid: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async transform(context) {
|
|
264
|
+
context.transformed = context.data.toUpperCase();
|
|
265
|
+
return { transformed: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async save(context) {
|
|
269
|
+
// Save to database
|
|
270
|
+
return { saved: true };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async process(data) {
|
|
274
|
+
const context = { data, timestamp: Date.now() };
|
|
275
|
+
const results = await this.pipeline.executeAll(context);
|
|
276
|
+
return this.pipeline.getSummary(results);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Usage
|
|
281
|
+
const processor = new DataProcessor();
|
|
282
|
+
const summary = await processor.process('hello world');
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Example 2: With Retries and Timeouts
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
const pipeline = new StepPipeline();
|
|
289
|
+
|
|
290
|
+
const steps = [
|
|
291
|
+
new StepDefinition(
|
|
292
|
+
0,
|
|
293
|
+
'Fetch Data',
|
|
294
|
+
'Fetch from external API',
|
|
295
|
+
async (context) => {
|
|
296
|
+
const response = await fetch(context.url);
|
|
297
|
+
return await response.json();
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
retries: 3,
|
|
301
|
+
retryDelay: 2000,
|
|
302
|
+
timeout: 10000
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
pipeline.registerSteps(steps);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Example 3: Conditional Execution
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
const steps = [
|
|
314
|
+
new StepDefinition(
|
|
315
|
+
0,
|
|
316
|
+
'Optional Enhancement',
|
|
317
|
+
'Run only if premium user',
|
|
318
|
+
async (context) => {
|
|
319
|
+
// Enhancement logic
|
|
320
|
+
return { enhanced: true };
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
condition: (context) => context.user.isPremium
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
];
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Example 4: Resume from Checkpoint
|
|
330
|
+
|
|
331
|
+
```javascript
|
|
332
|
+
// First run - fails at step 5
|
|
333
|
+
try {
|
|
334
|
+
await pipeline.executeAll(context);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Pipeline failed at step 5');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Resume from step 5 after fixing the issue
|
|
340
|
+
const results = await pipeline.executeAll(context, 5);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Example 5: With Event Tracking
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
const pipeline = new StepPipeline();
|
|
347
|
+
|
|
348
|
+
// Track metrics
|
|
349
|
+
const metrics = {
|
|
350
|
+
stepsExecuted: 0,
|
|
351
|
+
totalDuration: 0,
|
|
352
|
+
errors: []
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
pipeline
|
|
356
|
+
.on('stepComplete', (stepId, stepName, result, duration) => {
|
|
357
|
+
metrics.stepsExecuted++;
|
|
358
|
+
metrics.totalDuration += parseFloat(duration);
|
|
359
|
+
})
|
|
360
|
+
.on('stepError', (stepId, stepName, error) => {
|
|
361
|
+
metrics.errors.push({ stepId, stepName, error: error.message });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await pipeline.executeAll(context);
|
|
365
|
+
console.log('Metrics:', metrics);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Integration with EnterGreat Services
|
|
369
|
+
|
|
370
|
+
### Before (without package)
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
// Each service had duplicate code
|
|
374
|
+
import { StepRegistry, StepDefinition } from './core/StepRegistry.js';
|
|
375
|
+
import { StepExecutor } from './core/StepExecutor.js';
|
|
376
|
+
|
|
377
|
+
export class StepService {
|
|
378
|
+
constructor() {
|
|
379
|
+
this.registry = new StepRegistry();
|
|
380
|
+
this.executor = new StepExecutor(this.registry);
|
|
381
|
+
this.registerSteps();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async executeAll(context, startStep = 0) {
|
|
385
|
+
const endStep = 8; // Hardcoded
|
|
386
|
+
return await this.executor.executeSequence(startStep, endStep, context);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// More duplicate code...
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### After (with package)
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
|
|
397
|
+
|
|
398
|
+
export class StepService {
|
|
399
|
+
constructor() {
|
|
400
|
+
// One line instead of multiple!
|
|
401
|
+
this.pipeline = new StepPipeline();
|
|
402
|
+
this.registerSteps();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async executeAll(context, startStep = 0) {
|
|
406
|
+
// Uses the package's built-in method
|
|
407
|
+
return await this.pipeline.executeAll(context, startStep);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
listSteps() {
|
|
411
|
+
// Uses the package's built-in method
|
|
412
|
+
return this.pipeline.listSteps();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Advanced Features
|
|
418
|
+
|
|
419
|
+
### Custom Logging
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
const pipeline = new StepPipeline({
|
|
423
|
+
logLevel: 'debug' // Show all logs including debug messages
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Stop on Error
|
|
428
|
+
|
|
429
|
+
```javascript
|
|
430
|
+
const pipeline = new StepPipeline({
|
|
431
|
+
stopOnError: false // Continue even if steps fail
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Heartbeat Messages
|
|
436
|
+
|
|
437
|
+
Useful for long-running steps:
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
async longRunningStep(context) {
|
|
441
|
+
for (let i = 0; i < 100; i++) {
|
|
442
|
+
if (i % 10 === 0) {
|
|
443
|
+
this.pipeline.heartbeat(`Processing item ${i}/100`);
|
|
444
|
+
}
|
|
445
|
+
await processItem(i);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Testing
|
|
451
|
+
|
|
452
|
+
```javascript
|
|
453
|
+
import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
|
|
454
|
+
|
|
455
|
+
describe('My Pipeline', () => {
|
|
456
|
+
it('should execute all steps successfully', async () => {
|
|
457
|
+
const pipeline = new StepPipeline();
|
|
458
|
+
const steps = [
|
|
459
|
+
new StepDefinition(0, 'Test Step', 'Test', async () => ({ success: true }))
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
pipeline.registerSteps(steps);
|
|
463
|
+
const results = await pipeline.executeAll({});
|
|
464
|
+
|
|
465
|
+
expect(results[0].success).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Best Practices
|
|
471
|
+
|
|
472
|
+
1. **Use descriptive names** - Make step names and descriptions clear
|
|
473
|
+
2. **Keep steps focused** - Each step should do one thing well
|
|
474
|
+
3. **Handle errors gracefully** - Return meaningful error messages
|
|
475
|
+
4. **Use context wisely** - Don't mutate context unnecessarily
|
|
476
|
+
5. **Log important events** - Use heartbeat for long-running operations
|
|
477
|
+
6. **Test steps independently** - Unit test each step handler
|
|
478
|
+
7. **Use retries for flaky operations** - Network calls, external APIs
|
|
479
|
+
8. **Set appropriate timeouts** - Prevent hanging operations
|
|
480
|
+
|
|
481
|
+
## Migration Guide
|
|
482
|
+
|
|
483
|
+
See [MIGRATION.md](./MIGRATION.md) for detailed migration instructions from local implementations.
|
|
484
|
+
|
|
485
|
+
## Contributing
|
|
486
|
+
|
|
487
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
488
|
+
|
|
489
|
+
## License
|
|
490
|
+
|
|
491
|
+
MIT License - see [LICENSE](./LICENSE) file for details.
|
|
492
|
+
|
|
493
|
+
## Support
|
|
494
|
+
|
|
495
|
+
- GitHub Issues: [Create an issue](https://github.com/entergreat/step-pipeline/issues)
|
|
496
|
+
- Email: support@entergreat.com
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entergreat/step-pipeline",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Reusable step-based pipeline framework for EnterGreat services",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pipeline",
|
|
12
|
+
"steps",
|
|
13
|
+
"workflow",
|
|
14
|
+
"orchestration",
|
|
15
|
+
"entergreat"
|
|
16
|
+
],
|
|
17
|
+
"author": "EnterGreat",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/entergreat/step-pipeline.git"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a single step in the pipeline
|
|
3
|
+
* Supports both numeric IDs (legacy) and string IDs (recommended)
|
|
4
|
+
*/
|
|
5
|
+
export class StepDefinition {
|
|
6
|
+
constructor(id, name, description, handler, options = {}) {
|
|
7
|
+
this.id = id;
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.description = description;
|
|
10
|
+
this.handler = handler;
|
|
11
|
+
this.options = {
|
|
12
|
+
// Execution options
|
|
13
|
+
skip: options.skip || false,
|
|
14
|
+
retries: options.retries || 0,
|
|
15
|
+
retryDelay: options.retryDelay || 1000,
|
|
16
|
+
timeout: options.timeout || null,
|
|
17
|
+
condition: options.condition || null,
|
|
18
|
+
|
|
19
|
+
// Message options
|
|
20
|
+
startMessage: options.startMessage || `Starting ${name}...`,
|
|
21
|
+
successMessage: options.successMessage || `${name} completed successfully`,
|
|
22
|
+
errorMessage: options.errorMessage || `${name} failed`,
|
|
23
|
+
|
|
24
|
+
// Organization options
|
|
25
|
+
phase: options.phase || null,
|
|
26
|
+
tags: options.tags || [],
|
|
27
|
+
|
|
28
|
+
// Dependencies
|
|
29
|
+
dependsOn: options.dependsOn || [],
|
|
30
|
+
|
|
31
|
+
...options
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if this step should be executed based on condition
|
|
37
|
+
*/
|
|
38
|
+
shouldExecute(context) {
|
|
39
|
+
if (typeof this.options.condition === 'function') {
|
|
40
|
+
return this.options.condition(context);
|
|
41
|
+
}
|
|
42
|
+
return !this.options.skip;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get formatted start message with context interpolation
|
|
47
|
+
*/
|
|
48
|
+
getStartMessage(context = {}) {
|
|
49
|
+
return this._interpolate(this.options.startMessage, context);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get formatted success message with context interpolation
|
|
54
|
+
*/
|
|
55
|
+
getSuccessMessage(context = {}, result = {}) {
|
|
56
|
+
return this._interpolate(this.options.successMessage, { ...context, ...result });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get formatted error message with context interpolation
|
|
61
|
+
*/
|
|
62
|
+
getErrorMessage(context = {}, error = {}) {
|
|
63
|
+
return this._interpolate(this.options.errorMessage, { ...context, error: error.message });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Interpolate variables in message templates
|
|
68
|
+
* Example: "Processing {count} items" -> "Processing 5 items"
|
|
69
|
+
*/
|
|
70
|
+
_interpolate(template, data) {
|
|
71
|
+
if (!template) return '';
|
|
72
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
73
|
+
return data[key] !== undefined ? data[key] : match;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Builder class for creating step definitions with a fluent API
|
|
80
|
+
*/
|
|
81
|
+
export class StepBuilder {
|
|
82
|
+
constructor(id, name) {
|
|
83
|
+
this.id = id;
|
|
84
|
+
this.name = name;
|
|
85
|
+
this._description = '';
|
|
86
|
+
this._handler = null;
|
|
87
|
+
this._options = {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
description(desc) {
|
|
91
|
+
this._description = desc;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handler(fn) {
|
|
96
|
+
this._handler = fn;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
startMessage(msg) {
|
|
101
|
+
this._options.startMessage = msg;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
successMessage(msg) {
|
|
106
|
+
this._options.successMessage = msg;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
errorMessage(msg) {
|
|
111
|
+
this._options.errorMessage = msg;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
retries(count, delay = 1000) {
|
|
116
|
+
this._options.retries = count;
|
|
117
|
+
this._options.retryDelay = delay;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
timeout(ms) {
|
|
122
|
+
this._options.timeout = ms;
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
condition(fn) {
|
|
127
|
+
this._options.condition = fn;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
phase(phaseName) {
|
|
132
|
+
this._options.phase = phaseName;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
tags(...tags) {
|
|
137
|
+
this._options.tags = tags;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dependsOn(...stepIds) {
|
|
142
|
+
this._options.dependsOn = stepIds;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
build() {
|
|
147
|
+
if (!this._handler) {
|
|
148
|
+
throw new Error(`Step ${this.id}: handler is required`);
|
|
149
|
+
}
|
|
150
|
+
return new StepDefinition(
|
|
151
|
+
this.id,
|
|
152
|
+
this.name,
|
|
153
|
+
this._description,
|
|
154
|
+
this._handler,
|
|
155
|
+
this._options
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes pipeline steps with logging, error handling, and retries
|
|
3
|
+
*/
|
|
4
|
+
export class StepExecutor {
|
|
5
|
+
constructor(registry, options = {}) {
|
|
6
|
+
this.registry = registry;
|
|
7
|
+
this.options = {
|
|
8
|
+
logLevel: options.logLevel || 'info',
|
|
9
|
+
stopOnError: options.stopOnError !== false,
|
|
10
|
+
enableMetrics: options.enableMetrics || false,
|
|
11
|
+
...options
|
|
12
|
+
};
|
|
13
|
+
this.eventHandlers = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register an event handler
|
|
18
|
+
*/
|
|
19
|
+
on(event, handler) {
|
|
20
|
+
if (!this.eventHandlers.has(event)) {
|
|
21
|
+
this.eventHandlers.set(event, []);
|
|
22
|
+
}
|
|
23
|
+
this.eventHandlers.get(event).push(handler);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Emit an event
|
|
29
|
+
*/
|
|
30
|
+
emit(event, ...args) {
|
|
31
|
+
const handlers = this.eventHandlers.get(event) || [];
|
|
32
|
+
handlers.forEach(handler => {
|
|
33
|
+
try {
|
|
34
|
+
handler(...args);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Execute a single step
|
|
43
|
+
*/
|
|
44
|
+
async execute(stepId, context, skip = false) {
|
|
45
|
+
const step = this.registry.get(stepId);
|
|
46
|
+
|
|
47
|
+
this.log('info', `\n${'='.repeat(60)}`);
|
|
48
|
+
this.log('info', `Step ${stepId}: ${step.name}`);
|
|
49
|
+
this.log('info', `Description: ${step.description}`);
|
|
50
|
+
this.log('info', '='.repeat(60));
|
|
51
|
+
|
|
52
|
+
// Check if step should be skipped
|
|
53
|
+
if (skip || !step.shouldExecute(context)) {
|
|
54
|
+
this.log('info', `⏭️ Skipping step ${stepId}...`);
|
|
55
|
+
this.emit('stepSkipped', stepId, step.name);
|
|
56
|
+
return { skipped: true, stepId, stepName: step.name };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
let lastError = null;
|
|
61
|
+
const maxAttempts = step.options.retries + 1;
|
|
62
|
+
|
|
63
|
+
// Emit step start event
|
|
64
|
+
this.emit('stepStart', stepId, step.name, context);
|
|
65
|
+
|
|
66
|
+
// Retry logic
|
|
67
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
if (attempt > 1) {
|
|
70
|
+
this.log('warn', `🔄 Retry attempt ${attempt}/${maxAttempts} for step ${stepId}`);
|
|
71
|
+
await this.sleep(step.options.retryDelay);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Use custom start message from step definition
|
|
75
|
+
const startMessage = step.getStartMessage(context);
|
|
76
|
+
this.log('info', `🚀 ${startMessage}`);
|
|
77
|
+
|
|
78
|
+
// Execute with timeout if specified
|
|
79
|
+
const result = step.options.timeout
|
|
80
|
+
? await this.executeWithTimeout(step.handler, context, step.options.timeout)
|
|
81
|
+
: await step.handler(context);
|
|
82
|
+
|
|
83
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
84
|
+
|
|
85
|
+
// Use custom success message from step definition
|
|
86
|
+
const successMessage = step.getSuccessMessage(context, result);
|
|
87
|
+
this.log('info', `✅ ${successMessage} (${duration}s)`);
|
|
88
|
+
|
|
89
|
+
// Emit step complete event
|
|
90
|
+
this.emit('stepComplete', stepId, step.name, result, duration);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
stepId,
|
|
95
|
+
stepName: step.name,
|
|
96
|
+
duration,
|
|
97
|
+
result,
|
|
98
|
+
attempts: attempt
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
lastError = error;
|
|
102
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
103
|
+
|
|
104
|
+
if (attempt < maxAttempts) {
|
|
105
|
+
this.log('warn', `⚠️ Step ${stepId} failed (attempt ${attempt}/${maxAttempts})`);
|
|
106
|
+
this.log('warn', `Error: ${error.message}`);
|
|
107
|
+
} else {
|
|
108
|
+
// Use custom error message from step definition
|
|
109
|
+
const errorMessage = step.getErrorMessage(context, error);
|
|
110
|
+
this.log('error', `❌ ${errorMessage} after ${duration}s and ${attempt} attempt(s)`);
|
|
111
|
+
|
|
112
|
+
// Emit step error event
|
|
113
|
+
this.emit('stepError', stepId, step.name, error, duration);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
stepId,
|
|
118
|
+
stepName: step.name,
|
|
119
|
+
duration,
|
|
120
|
+
error: error.message,
|
|
121
|
+
stack: error.stack,
|
|
122
|
+
attempts: attempt
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// This should never be reached, but just in case
|
|
129
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
stepId,
|
|
133
|
+
stepName: step.name,
|
|
134
|
+
duration,
|
|
135
|
+
error: lastError?.message || 'Unknown error',
|
|
136
|
+
attempts: maxAttempts
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execute a sequence of steps
|
|
142
|
+
* Supports both numeric sequences (0, 1, 2) and string ID ranges
|
|
143
|
+
*/
|
|
144
|
+
async executeSequence(startStepId, endStepId, context) {
|
|
145
|
+
const results = [];
|
|
146
|
+
|
|
147
|
+
this.log('info', `\n${'*'.repeat(60)}`);
|
|
148
|
+
this.log('info', `Starting execution from step ${startStepId} to ${endStepId}`);
|
|
149
|
+
this.log('info', `${'*'.repeat(60)}\n`);
|
|
150
|
+
|
|
151
|
+
// Emit sequence start event
|
|
152
|
+
this.emit('sequenceStart', startStepId, endStepId, context);
|
|
153
|
+
|
|
154
|
+
// Get all step IDs in order
|
|
155
|
+
const allStepIds = this.registry.getStepIds();
|
|
156
|
+
|
|
157
|
+
// Find the start and end indices
|
|
158
|
+
const startIndex = allStepIds.indexOf(startStepId);
|
|
159
|
+
const endIndex = endStepId ? allStepIds.indexOf(endStepId) : allStepIds.length - 1;
|
|
160
|
+
|
|
161
|
+
if (startIndex === -1) {
|
|
162
|
+
throw new Error(`Start step ${startStepId} not found in registry`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (endIndex === -1 && endStepId) {
|
|
166
|
+
throw new Error(`End step ${endStepId} not found in registry`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Execute steps in range
|
|
170
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
171
|
+
const stepId = allStepIds[i];
|
|
172
|
+
|
|
173
|
+
const result = await this.execute(stepId, context);
|
|
174
|
+
results.push(result);
|
|
175
|
+
|
|
176
|
+
// Stop execution if step failed and stopOnError is true
|
|
177
|
+
if (!result.success && !result.skipped && this.options.stopOnError) {
|
|
178
|
+
this.log('error', `\n❌ Execution stopped at step ${stepId} due to error`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.log('info', `\n${'*'.repeat(60)}`);
|
|
184
|
+
this.log('info', `Execution completed`);
|
|
185
|
+
this.log('info', `${'*'.repeat(60)}\n`);
|
|
186
|
+
|
|
187
|
+
// Emit sequence complete event
|
|
188
|
+
this.emit('sequenceComplete', results);
|
|
189
|
+
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Execute handler with timeout
|
|
195
|
+
*/
|
|
196
|
+
async executeWithTimeout(handler, context, timeout) {
|
|
197
|
+
return Promise.race([
|
|
198
|
+
handler(context),
|
|
199
|
+
new Promise((_, reject) =>
|
|
200
|
+
setTimeout(() => reject(new Error(`Step timeout after ${timeout}ms`)), timeout)
|
|
201
|
+
)
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Sleep utility for retry delays
|
|
207
|
+
*/
|
|
208
|
+
sleep(ms) {
|
|
209
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Print a heartbeat message
|
|
214
|
+
*/
|
|
215
|
+
printHeartbeat(message) {
|
|
216
|
+
this.log('info', `💓 ${message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Log a message based on log level
|
|
221
|
+
*/
|
|
222
|
+
log(level, message) {
|
|
223
|
+
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
224
|
+
const currentLevel = levels[this.options.logLevel] || 2;
|
|
225
|
+
const messageLevel = levels[level] || 2;
|
|
226
|
+
|
|
227
|
+
if (messageLevel <= currentLevel) {
|
|
228
|
+
if (level === 'error') {
|
|
229
|
+
console.error(message);
|
|
230
|
+
} else if (level === 'warn') {
|
|
231
|
+
console.warn(message);
|
|
232
|
+
} else {
|
|
233
|
+
console.log(message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { StepRegistry } from './StepRegistry.js';
|
|
2
|
+
import { StepExecutor } from './StepExecutor.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* High-level API for managing and executing step-based pipelines
|
|
6
|
+
* This is the main class that services will use
|
|
7
|
+
*/
|
|
8
|
+
export class StepPipeline {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.registry = new StepRegistry();
|
|
11
|
+
this.executor = new StepExecutor(this.registry, options);
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register steps (convenience method)
|
|
17
|
+
*/
|
|
18
|
+
registerSteps(stepDefinitions) {
|
|
19
|
+
this.registry.registerMany(stepDefinitions);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute a single step by ID
|
|
25
|
+
*/
|
|
26
|
+
async executeStep(stepId, context, skip = false) {
|
|
27
|
+
return await this.executor.execute(stepId, context, skip);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute all registered steps
|
|
32
|
+
*/
|
|
33
|
+
async executeAll(context, startStep = null) {
|
|
34
|
+
const firstStep = startStep !== null ? startStep : this.registry.getFirstStepId();
|
|
35
|
+
const lastStep = this.registry.getLastStepId();
|
|
36
|
+
|
|
37
|
+
if (firstStep === -1 || lastStep === -1) {
|
|
38
|
+
throw new Error('No steps registered in pipeline');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await this.executor.executeSequence(firstStep, lastStep, context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute a range of steps
|
|
46
|
+
*/
|
|
47
|
+
async executeRange(startStep, endStep, context) {
|
|
48
|
+
return await this.executor.executeSequence(startStep, endStep, context);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute only specific steps by IDs
|
|
53
|
+
*/
|
|
54
|
+
async executeSteps(stepIds, context) {
|
|
55
|
+
const results = [];
|
|
56
|
+
|
|
57
|
+
for (const stepId of stepIds) {
|
|
58
|
+
if (!this.registry.has(stepId)) {
|
|
59
|
+
console.warn(`⚠️ Step ${stepId} not found, skipping...`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await this.executor.execute(stepId, context);
|
|
64
|
+
results.push(result);
|
|
65
|
+
|
|
66
|
+
// Stop if step failed and stopOnError is enabled
|
|
67
|
+
if (!result.success && !result.skipped && this.executor.options.stopOnError) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List all registered steps
|
|
77
|
+
*/
|
|
78
|
+
listSteps() {
|
|
79
|
+
return this.registry.getAll().map(step => ({
|
|
80
|
+
id: step.id,
|
|
81
|
+
name: step.name,
|
|
82
|
+
description: step.description,
|
|
83
|
+
options: step.options
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get a specific step definition
|
|
89
|
+
*/
|
|
90
|
+
getStep(stepId) {
|
|
91
|
+
return this.registry.get(stepId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a step exists
|
|
96
|
+
*/
|
|
97
|
+
hasStep(stepId) {
|
|
98
|
+
return this.registry.has(stepId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get total number of steps
|
|
103
|
+
*/
|
|
104
|
+
getStepCount() {
|
|
105
|
+
return this.registry.count();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the first step ID
|
|
110
|
+
*/
|
|
111
|
+
getFirstStepId() {
|
|
112
|
+
return this.registry.getFirstStepId();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the last step ID
|
|
117
|
+
*/
|
|
118
|
+
getLastStepId() {
|
|
119
|
+
return this.registry.getLastStepId();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register event handlers
|
|
124
|
+
*/
|
|
125
|
+
on(event, handler) {
|
|
126
|
+
this.executor.on(event, handler);
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Print a heartbeat message (useful for long-running steps)
|
|
132
|
+
*/
|
|
133
|
+
heartbeat(message) {
|
|
134
|
+
this.executor.printHeartbeat(message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get execution summary from results
|
|
139
|
+
*/
|
|
140
|
+
getSummary(results) {
|
|
141
|
+
const successful = results.filter(r => r.success).length;
|
|
142
|
+
const failed = results.filter(r => !r.success && !r.skipped).length;
|
|
143
|
+
const skipped = results.filter(r => r.skipped).length;
|
|
144
|
+
const totalDuration = results.reduce((sum, r) => {
|
|
145
|
+
return sum + (parseFloat(r.duration) || 0);
|
|
146
|
+
}, 0);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
total: results.length,
|
|
150
|
+
successful,
|
|
151
|
+
failed,
|
|
152
|
+
skipped,
|
|
153
|
+
successRate: results.length > 0 ? (successful / results.length * 100).toFixed(2) : 0,
|
|
154
|
+
totalDuration: totalDuration.toFixed(2),
|
|
155
|
+
results
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Print execution summary
|
|
161
|
+
*/
|
|
162
|
+
printSummary(results) {
|
|
163
|
+
const summary = this.getSummary(results);
|
|
164
|
+
|
|
165
|
+
console.log('\n' + '='.repeat(60));
|
|
166
|
+
console.log('📊 Execution Summary');
|
|
167
|
+
console.log('='.repeat(60));
|
|
168
|
+
console.log(`Total Steps: ${summary.total}`);
|
|
169
|
+
console.log(`✅ Successful: ${summary.successful}`);
|
|
170
|
+
console.log(`❌ Failed: ${summary.failed}`);
|
|
171
|
+
console.log(`⏭️ Skipped: ${summary.skipped}`);
|
|
172
|
+
console.log(`Success Rate: ${summary.successRate}%`);
|
|
173
|
+
console.log(`Total Duration: ${summary.totalDuration}s`);
|
|
174
|
+
console.log('='.repeat(60) + '\n');
|
|
175
|
+
|
|
176
|
+
return summary;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { StepDefinition } from './StepDefinition.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registry for managing step definitions
|
|
5
|
+
*/
|
|
6
|
+
export class StepRegistry {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.steps = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a new step
|
|
13
|
+
*/
|
|
14
|
+
register(stepDefinition) {
|
|
15
|
+
if (!(stepDefinition instanceof StepDefinition)) {
|
|
16
|
+
throw new Error('Invalid step definition');
|
|
17
|
+
}
|
|
18
|
+
this.steps.set(stepDefinition.id, stepDefinition);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register multiple steps at once
|
|
24
|
+
*/
|
|
25
|
+
registerMany(stepDefinitions) {
|
|
26
|
+
stepDefinitions.forEach(step => this.register(step));
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a step by ID
|
|
32
|
+
*/
|
|
33
|
+
get(stepId) {
|
|
34
|
+
const step = this.steps.get(stepId);
|
|
35
|
+
if (!step) {
|
|
36
|
+
throw new Error(`Step ${stepId} not found in registry`);
|
|
37
|
+
}
|
|
38
|
+
return step;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all registered steps
|
|
43
|
+
* Sorts by numeric ID if all IDs are numbers, otherwise maintains insertion order
|
|
44
|
+
*/
|
|
45
|
+
getAll() {
|
|
46
|
+
const steps = Array.from(this.steps.values());
|
|
47
|
+
|
|
48
|
+
// Check if all IDs are numeric
|
|
49
|
+
const allNumeric = steps.every(step => typeof step.id === 'number');
|
|
50
|
+
|
|
51
|
+
if (allNumeric) {
|
|
52
|
+
return steps.sort((a, b) => a.id - b.id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For string IDs, maintain insertion order (Map preserves insertion order)
|
|
56
|
+
return steps;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a step exists
|
|
61
|
+
*/
|
|
62
|
+
has(stepId) {
|
|
63
|
+
return this.steps.has(stepId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get total number of steps
|
|
68
|
+
*/
|
|
69
|
+
count() {
|
|
70
|
+
return this.steps.size;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the last step ID
|
|
75
|
+
*/
|
|
76
|
+
getLastStepId() {
|
|
77
|
+
const steps = this.getAll();
|
|
78
|
+
return steps.length > 0 ? steps[steps.length - 1].id : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the first step ID
|
|
83
|
+
*/
|
|
84
|
+
getFirstStepId() {
|
|
85
|
+
const steps = this.getAll();
|
|
86
|
+
return steps.length > 0 ? steps[0].id : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get steps by phase
|
|
91
|
+
*/
|
|
92
|
+
getByPhase(phase) {
|
|
93
|
+
return this.getAll().filter(step => step.options.phase === phase);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get steps by tag
|
|
98
|
+
*/
|
|
99
|
+
getByTag(tag) {
|
|
100
|
+
return this.getAll().filter(step => step.options.tags.includes(tag));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get step IDs in order
|
|
105
|
+
*/
|
|
106
|
+
getStepIds() {
|
|
107
|
+
return this.getAll().map(step => step.id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Clear all steps
|
|
112
|
+
*/
|
|
113
|
+
clear() {
|
|
114
|
+
this.steps.clear();
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Remove a specific step
|
|
120
|
+
*/
|
|
121
|
+
remove(stepId) {
|
|
122
|
+
return this.steps.delete(stepId);
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @entergreat/step-pipeline
|
|
3
|
+
*
|
|
4
|
+
* Reusable step-based pipeline framework for EnterGreat services
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StepDefinition, StepBuilder } from './core/StepDefinition.js';
|
|
8
|
+
import { StepRegistry } from './core/StepRegistry.js';
|
|
9
|
+
import { StepExecutor } from './core/StepExecutor.js';
|
|
10
|
+
import { StepPipeline } from './core/StepPipeline.js';
|
|
11
|
+
import { createStepsFromConfig } from './utils/helpers.js';
|
|
12
|
+
|
|
13
|
+
// Named exports
|
|
14
|
+
export {
|
|
15
|
+
StepDefinition,
|
|
16
|
+
StepBuilder,
|
|
17
|
+
StepRegistry,
|
|
18
|
+
StepExecutor,
|
|
19
|
+
StepPipeline,
|
|
20
|
+
createStepsFromConfig
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Default export
|
|
24
|
+
export default {
|
|
25
|
+
StepDefinition,
|
|
26
|
+
StepBuilder,
|
|
27
|
+
StepRegistry,
|
|
28
|
+
StepExecutor,
|
|
29
|
+
StepPipeline,
|
|
30
|
+
createStepsFromConfig
|
|
31
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { StepDefinition, StepBuilder } from '../core/StepDefinition.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create step definitions from a configuration array
|
|
5
|
+
*
|
|
6
|
+
* @param {Array} config - Array of step configurations
|
|
7
|
+
* @param {Object} handlers - Object containing handler functions
|
|
8
|
+
* @returns {Array<StepDefinition>} Array of step definitions
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const steps = createStepsFromConfig([
|
|
12
|
+
* {
|
|
13
|
+
* id: 'loadData',
|
|
14
|
+
* name: 'Load Data',
|
|
15
|
+
* startMessage: 'start loading data from source',
|
|
16
|
+
* successMessage: 'done loading data successfully'
|
|
17
|
+
* }
|
|
18
|
+
* ], handlers);
|
|
19
|
+
*/
|
|
20
|
+
export function createStepsFromConfig(config, handlers) {
|
|
21
|
+
if (!Array.isArray(config)) {
|
|
22
|
+
throw new Error('Config must be an array');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!handlers || typeof handlers !== 'object') {
|
|
26
|
+
throw new Error('Handlers must be an object');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return config.map((stepConfig, index) => {
|
|
30
|
+
const {
|
|
31
|
+
id,
|
|
32
|
+
name,
|
|
33
|
+
description,
|
|
34
|
+
startMessage,
|
|
35
|
+
successMessage,
|
|
36
|
+
errorMessage,
|
|
37
|
+
...options
|
|
38
|
+
} = stepConfig;
|
|
39
|
+
|
|
40
|
+
// Validate required fields
|
|
41
|
+
if (!id) {
|
|
42
|
+
throw new Error(`Step at index ${index}: 'id' is required`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!name) {
|
|
46
|
+
throw new Error(`Step ${id}: 'name' is required`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get handler function
|
|
50
|
+
const handler = handlers[id];
|
|
51
|
+
if (typeof handler !== 'function') {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Step ${id}: handler function not found in handlers object. ` +
|
|
54
|
+
`Expected handlers['${id}'] to be a function.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create step definition
|
|
59
|
+
return new StepDefinition(
|
|
60
|
+
id,
|
|
61
|
+
name,
|
|
62
|
+
description || startMessage || '',
|
|
63
|
+
handler,
|
|
64
|
+
{
|
|
65
|
+
startMessage,
|
|
66
|
+
successMessage,
|
|
67
|
+
errorMessage,
|
|
68
|
+
...options
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a fluent step builder
|
|
76
|
+
*/
|
|
77
|
+
export function step(id, name) {
|
|
78
|
+
return new StepBuilder(id, name);
|
|
79
|
+
}
|