@onlineapps/conn-orch-orchestrator 1.0.9 → 1.0.13
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/package.json +1 -1
- package/src/WorkflowOrchestrator.js +122 -38
package/package.json
CHANGED
|
@@ -37,10 +37,18 @@ class WorkflowOrchestrator {
|
|
|
37
37
|
this.logger = config.logger || console;
|
|
38
38
|
this.defaultTimeout = config.defaultTimeout || 30000;
|
|
39
39
|
|
|
40
|
-
// Create cookbook router
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// Create cookbook router - createRouter is an exported function, not a method
|
|
41
|
+
const { createRouter } = this.cookbook;
|
|
42
|
+
if (typeof createRouter === 'function') {
|
|
43
|
+
this.router = createRouter(this.mqClient, this.registryClient, {
|
|
44
|
+
logger: this.logger
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
// Fallback: try calling it as method (backward compatibility)
|
|
48
|
+
this.router = this.cookbook.createRouter?.(this.mqClient, this.registryClient, {
|
|
49
|
+
logger: this.logger
|
|
50
|
+
}) || null;
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
/**
|
|
@@ -87,36 +95,16 @@ class WorkflowOrchestrator {
|
|
|
87
95
|
// Check if this step is for this service
|
|
88
96
|
if (step.type === 'task' && step.service !== serviceName) {
|
|
89
97
|
this.logger.warn(`Step ${current_step} is for ${step.service}, not ${serviceName}`);
|
|
90
|
-
|
|
98
|
+
if (this.router?.routeToService) {
|
|
99
|
+
await this.router.routeToService(step.service, message);
|
|
100
|
+
} else {
|
|
101
|
+
// Fallback: publish to service's workflow queue directly
|
|
102
|
+
const serviceQueue = `${step.service}.workflow`;
|
|
103
|
+
await this.mqClient.publish(serviceQueue, message);
|
|
104
|
+
}
|
|
91
105
|
return { skipped: true, reason: 'wrong_service' };
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
// Publish workflow.progress to monitoring.workflow using unified helper
|
|
95
|
-
const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
|
|
96
|
-
try {
|
|
97
|
-
const stepIndex = cookbookDef.steps.findIndex(s => s.id === current_step);
|
|
98
|
-
console.log(`[WorkflowOrchestrator] [PROGRESS] Publishing progress for ${workflow_id}, step: ${current_step}, index: ${stepIndex}`);
|
|
99
|
-
await publishToMonitoringWorkflow(this.mqClient, {
|
|
100
|
-
event_type: 'progress',
|
|
101
|
-
workflow_id: workflow_id,
|
|
102
|
-
service_name: serviceName,
|
|
103
|
-
step_index: stepIndex >= 0 ? stepIndex : 0,
|
|
104
|
-
step_id: current_step,
|
|
105
|
-
cookbook: cookbookDef,
|
|
106
|
-
context: enrichedContext,
|
|
107
|
-
status: 'in_progress',
|
|
108
|
-
timestamp: new Date().toISOString()
|
|
109
|
-
}, this.logger, { workflow_id, step_id: current_step });
|
|
110
|
-
console.log(`[WorkflowOrchestrator] [PROGRESS] ✓ Progress published for ${workflow_id}`);
|
|
111
|
-
} catch (monitoringError) {
|
|
112
|
-
// Don't fail workflow if monitoring publish fails (helper already logs)
|
|
113
|
-
console.error(`[WorkflowOrchestrator] [PROGRESS] ✗ Failed to publish progress:`, monitoringError);
|
|
114
|
-
this.logger.warn('Failed to publish workflow.progress to monitoring', {
|
|
115
|
-
workflow_id,
|
|
116
|
-
error: monitoringError.message || String(monitoringError)
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
108
|
// Check cache if available
|
|
121
109
|
let result;
|
|
122
110
|
if (this.cache && step.type === 'task') {
|
|
@@ -158,6 +146,32 @@ class WorkflowOrchestrator {
|
|
|
158
146
|
steps: existingSteps
|
|
159
147
|
};
|
|
160
148
|
|
|
149
|
+
// Publish workflow.progress AFTER execution (so we have step output)
|
|
150
|
+
const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
|
|
151
|
+
try {
|
|
152
|
+
console.log(`[WorkflowOrchestrator] [PROGRESS] Publishing progress for ${workflow_id}, step: ${current_step}, index: ${currentIndex}`);
|
|
153
|
+
await publishToMonitoringWorkflow(this.mqClient, {
|
|
154
|
+
event_type: 'progress',
|
|
155
|
+
workflow_id: workflow_id,
|
|
156
|
+
service_name: serviceName,
|
|
157
|
+
step_index: currentIndex >= 0 ? currentIndex : 0,
|
|
158
|
+
step_id: current_step,
|
|
159
|
+
cookbook: cookbookDef,
|
|
160
|
+
context: updatedContext, // Context WITH step output
|
|
161
|
+
output: result, // Step output directly
|
|
162
|
+
status: 'completed', // Step completed (not in_progress)
|
|
163
|
+
timestamp: new Date().toISOString()
|
|
164
|
+
}, this.logger, { workflow_id, step_id: current_step });
|
|
165
|
+
console.log(`[WorkflowOrchestrator] [PROGRESS] ✓ Progress published for ${workflow_id}`);
|
|
166
|
+
} catch (monitoringError) {
|
|
167
|
+
// Don't fail workflow if monitoring publish fails
|
|
168
|
+
console.error(`[WorkflowOrchestrator] [PROGRESS] ✗ Failed to publish progress:`, monitoringError);
|
|
169
|
+
this.logger.warn('Failed to publish workflow.progress to monitoring', {
|
|
170
|
+
workflow_id,
|
|
171
|
+
error: monitoringError.message || String(monitoringError)
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
// Find next step (currentIndex already defined above)
|
|
162
176
|
const nextStep = cookbookDef.steps[currentIndex + 1];
|
|
163
177
|
|
|
@@ -251,21 +265,82 @@ class WorkflowOrchestrator {
|
|
|
251
265
|
* @returns {Promise<Object>} API call result
|
|
252
266
|
*/
|
|
253
267
|
async _executeTaskStep(step, context) {
|
|
254
|
-
//
|
|
268
|
+
// Resolve variable references in step.input (e.g. ${steps[0].output.message})
|
|
269
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.id} input BEFORE:`, JSON.stringify(step.input));
|
|
270
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Context.steps:`, JSON.stringify(context.steps?.map(s => ({ id: s.id, output: s.output }))));
|
|
271
|
+
|
|
272
|
+
const resolvedInput = this._resolveInputReferences(step.input, context);
|
|
273
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.id} input AFTER:`, JSON.stringify(resolvedInput));
|
|
274
|
+
|
|
275
|
+
// Use API mapper to call the service with resolved input
|
|
255
276
|
const result = await this.apiMapper.callOperation(
|
|
256
277
|
step.operation || step.id,
|
|
257
|
-
|
|
278
|
+
resolvedInput,
|
|
258
279
|
context
|
|
259
280
|
);
|
|
260
281
|
|
|
261
282
|
this.logger.info(`Task step executed`, {
|
|
262
283
|
step: step.id,
|
|
263
284
|
service: step.service,
|
|
264
|
-
operation: step.operation
|
|
285
|
+
operation: step.operation,
|
|
286
|
+
inputResolved: !!resolvedInput
|
|
265
287
|
});
|
|
266
288
|
|
|
267
289
|
return result;
|
|
268
290
|
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Resolve variable references in input object
|
|
294
|
+
* Supports ${steps[N].output.field}, ${api_input.field}, ${context.field}
|
|
295
|
+
* @private
|
|
296
|
+
* @param {Object} input - Input object with potential references
|
|
297
|
+
* @param {Object} context - Current context with steps, api_input, etc
|
|
298
|
+
* @returns {Object} Resolved input object
|
|
299
|
+
*/
|
|
300
|
+
_resolveInputReferences(input, context) {
|
|
301
|
+
if (!input) return input;
|
|
302
|
+
|
|
303
|
+
const resolveValue = (value) => {
|
|
304
|
+
if (typeof value === 'string' && value.includes('${')) {
|
|
305
|
+
// Replace ${...} references
|
|
306
|
+
return value.replace(/\$\{([^}]+)\}/g, (match, path) => {
|
|
307
|
+
const resolved = this._getValueFromPath(path.trim(), context);
|
|
308
|
+
return resolved !== undefined ? resolved : match;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
return value.map(resolveValue);
|
|
313
|
+
}
|
|
314
|
+
if (value && typeof value === 'object') {
|
|
315
|
+
return Object.fromEntries(
|
|
316
|
+
Object.entries(value).map(([k, v]) => [k, resolveValue(v)])
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return value;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return resolveValue(input);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get value from dot/bracket notation path
|
|
327
|
+
* @private
|
|
328
|
+
* @param {string} path - Path like "steps[0].output.message" or "api_input.name"
|
|
329
|
+
* @param {Object} context - Context object
|
|
330
|
+
* @returns {*} Resolved value or undefined
|
|
331
|
+
*/
|
|
332
|
+
_getValueFromPath(path, context) {
|
|
333
|
+
// Normalize bracket notation to dot notation: steps[0] -> steps.0
|
|
334
|
+
const normalized = path.replace(/\[(\d+)\]/g, '.$1');
|
|
335
|
+
const parts = normalized.split('.');
|
|
336
|
+
|
|
337
|
+
let current = context;
|
|
338
|
+
for (const part of parts) {
|
|
339
|
+
if (current === undefined || current === null) return undefined;
|
|
340
|
+
current = current[part];
|
|
341
|
+
}
|
|
342
|
+
return current;
|
|
343
|
+
}
|
|
269
344
|
|
|
270
345
|
/**
|
|
271
346
|
* Execute control flow step (foreach, switch, fork_join, etc)
|
|
@@ -322,10 +397,19 @@ class WorkflowOrchestrator {
|
|
|
322
397
|
|
|
323
398
|
if (nextStep.type === 'task') {
|
|
324
399
|
// Route to specific service
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
400
|
+
if (this.router?.routeToService) {
|
|
401
|
+
await this.router.routeToService(nextStep.service, message);
|
|
402
|
+
this.logger.info(`Routed to service: ${nextStep.service}`, {
|
|
403
|
+
step: nextStep.id
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
// Fallback: publish to service's workflow queue directly
|
|
407
|
+
const serviceQueue = `${nextStep.service}.workflow`;
|
|
408
|
+
await this.mqClient.publish(serviceQueue, message);
|
|
409
|
+
this.logger.info(`Published to queue: ${serviceQueue}`, {
|
|
410
|
+
step: nextStep.id
|
|
411
|
+
});
|
|
412
|
+
}
|
|
329
413
|
} else {
|
|
330
414
|
// Control flow steps handled by workflow init queue
|
|
331
415
|
await this.mqClient.publish('workflow.init', message);
|