@onlineapps/conn-orch-orchestrator 1.0.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/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@onlineapps/conn-orch-orchestrator",
3
+ "version": "1.0.0",
4
+ "description": "Workflow orchestration connector for OA Drive - handles message routing and workflow execution",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:watch": "jest --watch",
9
+ "test:coverage": "jest --coverage",
10
+ "lint": "eslint src/",
11
+ "docs": "jsdoc2md --files src/**/*.js > API.md"
12
+ },
13
+ "keywords": [
14
+ "workflow",
15
+ "orchestration",
16
+ "message-routing",
17
+ "cookbook",
18
+ "connector"
19
+ ],
20
+ "author": "OA Drive Team",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@onlineapps/conn-base-logger": "^1.0.0",
24
+ "@onlineapps/conn-infra-mq": "^1.0.0",
25
+ "@onlineapps/conn-orch-registry": "^1.0.0",
26
+ "@onlineapps/conn-orch-cookbook": "^1.0.0",
27
+ "@onlineapps/conn-orch-api-mapper": "^1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "jest": "^29.5.0",
31
+ "eslint": "^8.30.0",
32
+ "jsdoc-to-markdown": "^8.0.0"
33
+ },
34
+ "jest": {
35
+ "testEnvironment": "node",
36
+ "coverageDirectory": "coverage",
37
+ "collectCoverageFrom": [
38
+ "src/**/*.js"
39
+ ]
40
+ }
41
+ }
@@ -0,0 +1,341 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @class WorkflowOrchestrator
5
+ * @description Main orchestration class that coordinates all workflow execution.
6
+ * Handles message routing, step execution, and workflow state management.
7
+ *
8
+ * This is the core logic moved from service-wrapper's WorkflowProcessor to make
9
+ * service-wrapper a true thin orchestration layer.
10
+ */
11
+ class WorkflowOrchestrator {
12
+ /**
13
+ * Create a new WorkflowOrchestrator instance
14
+ * @constructor
15
+ * @param {Object} config - Configuration object
16
+ * @param {Object} config.mqClient - MQ client for message operations
17
+ * @param {Object} config.registryClient - Registry client for service discovery
18
+ * @param {Object} config.apiMapper - API mapper for cookbook to HTTP mapping
19
+ * @param {Object} config.cookbook - Cookbook connector for validation and execution
20
+ * @param {Object} [config.cache] - Cache connector for caching
21
+ * @param {Object} [config.errorHandler] - Error handler connector
22
+ * @param {Object} [config.logger] - Logger instance
23
+ * @param {number} [config.defaultTimeout=30000] - Default timeout for operations
24
+ */
25
+ constructor(config) {
26
+ if (!config.mqClient) throw new Error('mqClient is required');
27
+ if (!config.registryClient) throw new Error('registryClient is required');
28
+ if (!config.apiMapper) throw new Error('apiMapper is required');
29
+ if (!config.cookbook) throw new Error('cookbook is required');
30
+
31
+ this.mqClient = config.mqClient;
32
+ this.registryClient = config.registryClient;
33
+ this.apiMapper = config.apiMapper;
34
+ this.cookbook = config.cookbook;
35
+ this.cache = config.cache;
36
+ this.errorHandler = config.errorHandler;
37
+ this.logger = config.logger || console;
38
+ this.defaultTimeout = config.defaultTimeout || 30000;
39
+
40
+ // Create cookbook router
41
+ this.router = this.cookbook.createRouter(this.mqClient, this.registryClient, {
42
+ logger: this.logger
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Process a workflow message
48
+ * @async
49
+ * @method processWorkflowMessage
50
+ * @param {Object} message - Workflow message from queue
51
+ * @param {string} message.workflow_id - Workflow ID
52
+ * @param {Object} message.cookbook - Cookbook definition
53
+ * @param {string} message.current_step - Current step ID
54
+ * @param {Object} message.context - Workflow context
55
+ * @param {string} serviceName - Name of the service processing this
56
+ * @returns {Promise<Object>} Processing result
57
+ *
58
+ * @example
59
+ * const result = await orchestrator.processWorkflowMessage({
60
+ * workflow_id: 'wf-123',
61
+ * cookbook: cookbookDef,
62
+ * current_step: 'step1',
63
+ * context: {}
64
+ * }, 'hello-service');
65
+ */
66
+ async processWorkflowMessage(message, serviceName) {
67
+ const { workflow_id, cookbook: cookbookDef, current_step, context } = message;
68
+
69
+ try {
70
+ // Validate cookbook structure
71
+ this.cookbook.validateCookbook(cookbookDef);
72
+
73
+ // Find current step
74
+ const step = cookbookDef.steps.find(s => s.id === current_step);
75
+ if (!step) {
76
+ throw new Error(`Step not found: ${current_step}`);
77
+ }
78
+
79
+ // Check if this step is for this service
80
+ if (step.type === 'task' && step.service !== serviceName) {
81
+ this.logger.warn(`Step ${current_step} is for ${step.service}, not ${serviceName}`);
82
+ await this.router.routeToService(step.service, message);
83
+ return { skipped: true, reason: 'wrong_service' };
84
+ }
85
+
86
+ // Check cache if available
87
+ let result;
88
+ if (this.cache && step.type === 'task') {
89
+ const cacheKey = this._getCacheKey(step, context);
90
+ result = await this.cache.get(cacheKey);
91
+
92
+ if (result) {
93
+ this.logger.info('Cache hit', { step: step.id, cacheKey });
94
+ } else {
95
+ // Execute the step
96
+ result = await this._executeStep(step, context, cookbookDef);
97
+ await this.cache.set(cacheKey, result, { ttl: 300 });
98
+ }
99
+ } else {
100
+ // Execute without cache
101
+ result = await this._executeStep(step, context, cookbookDef);
102
+ }
103
+
104
+ // Update context with result
105
+ const updatedContext = {
106
+ ...context,
107
+ steps: {
108
+ ...context.steps,
109
+ [current_step]: result
110
+ }
111
+ };
112
+
113
+ // Find next step
114
+ const currentIndex = cookbookDef.steps.findIndex(s => s.id === current_step);
115
+ const nextStep = cookbookDef.steps[currentIndex + 1];
116
+
117
+ if (nextStep) {
118
+ // Route to next step
119
+ await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id);
120
+ } else {
121
+ // Workflow completed
122
+ await this._completeWorkflow(workflow_id, updatedContext);
123
+ }
124
+
125
+ return {
126
+ success: true,
127
+ workflow_id,
128
+ step_id: current_step,
129
+ result
130
+ };
131
+
132
+ } catch (error) {
133
+ this.logger.error(`Workflow processing failed: ${error.message}`, {
134
+ workflow_id,
135
+ current_step,
136
+ error: error.stack
137
+ });
138
+
139
+ // Handle error
140
+ if (this.errorHandler) {
141
+ await this.errorHandler.handleError(error, {
142
+ workflow_id,
143
+ current_step,
144
+ service: serviceName,
145
+ context
146
+ });
147
+ } else {
148
+ // Fallback to router DLQ
149
+ await this.router.routeToDLQ(cookbookDef, context, error, serviceName);
150
+ }
151
+
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Execute a single step
158
+ * @private
159
+ * @async
160
+ * @param {Object} step - Step to execute
161
+ * @param {Object} context - Execution context
162
+ * @param {Object} cookbookDef - Full cookbook definition
163
+ * @returns {Promise<Object>} Step result
164
+ */
165
+ async _executeStep(step, context, cookbookDef) {
166
+ if (step.type === 'task') {
167
+ // Task step - call service API
168
+ return await this._executeTaskStep(step, context);
169
+ } else {
170
+ // Control flow step - use cookbook executor
171
+ return await this._executeControlFlowStep(step, context, cookbookDef);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Execute a task step by calling service API
177
+ * @private
178
+ * @async
179
+ * @param {Object} step - Task step
180
+ * @param {Object} context - Execution context
181
+ * @returns {Promise<Object>} API call result
182
+ */
183
+ async _executeTaskStep(step, context) {
184
+ // Use API mapper to call the service
185
+ const result = await this.apiMapper.callOperation(
186
+ step.operation || step.id,
187
+ step.input,
188
+ context
189
+ );
190
+
191
+ this.logger.info(`Task step executed`, {
192
+ step: step.id,
193
+ service: step.service,
194
+ operation: step.operation
195
+ });
196
+
197
+ return result;
198
+ }
199
+
200
+ /**
201
+ * Execute control flow step (foreach, switch, fork_join, etc)
202
+ * @private
203
+ * @async
204
+ * @param {Object} step - Control flow step
205
+ * @param {Object} context - Execution context
206
+ * @param {Object} cookbookDef - Full cookbook definition
207
+ * @returns {Promise<Object>} Control flow result
208
+ */
209
+ async _executeControlFlowStep(step, context, cookbookDef) {
210
+ const { CookbookExecutor } = this.cookbook;
211
+
212
+ // Create executor for control flow
213
+ const executor = new CookbookExecutor(
214
+ { steps: [step] },
215
+ {
216
+ logger: this.logger,
217
+ defaultTimeout: this.defaultTimeout
218
+ }
219
+ );
220
+
221
+ // Set context
222
+ executor.context.setInput(context.api_input || {});
223
+ executor.context.data.steps = context.steps || {};
224
+
225
+ // Execute the control flow step
226
+ const result = await executor.executeStep(step);
227
+
228
+ this.logger.info(`Control flow step executed`, {
229
+ step: step.id,
230
+ type: step.type
231
+ });
232
+
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Route to next step in workflow
238
+ * @private
239
+ * @async
240
+ * @param {Object} nextStep - Next step to execute
241
+ * @param {Object} cookbookDef - Full cookbook definition
242
+ * @param {Object} context - Updated context
243
+ * @param {string} workflow_id - Workflow ID
244
+ */
245
+ async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
246
+ const message = {
247
+ workflow_id,
248
+ cookbook: cookbookDef,
249
+ current_step: nextStep.id,
250
+ context
251
+ };
252
+
253
+ if (nextStep.type === 'task') {
254
+ // Route to specific service
255
+ await this.router.routeToService(nextStep.service, message);
256
+ this.logger.info(`Routed to service: ${nextStep.service}`, {
257
+ step: nextStep.id
258
+ });
259
+ } else {
260
+ // Control flow steps handled by workflow init queue
261
+ await this.mqClient.publish('workflow.init', message);
262
+ this.logger.info(`Routed control flow to workflow.init`, {
263
+ step: nextStep.id
264
+ });
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Complete workflow
270
+ * @private
271
+ * @async
272
+ * @param {string} workflow_id - Workflow ID
273
+ * @param {Object} finalContext - Final workflow context
274
+ */
275
+ async _completeWorkflow(workflow_id, finalContext) {
276
+ await this.mqClient.publish('workflow.completed', {
277
+ workflow_id,
278
+ result: finalContext,
279
+ completed_at: new Date().toISOString()
280
+ });
281
+
282
+ this.logger.info(`Workflow completed: ${workflow_id}`);
283
+ }
284
+
285
+ /**
286
+ * Generate cache key for a step
287
+ * @private
288
+ * @param {Object} step - Step object
289
+ * @param {Object} context - Execution context
290
+ * @returns {string} Cache key
291
+ */
292
+ _getCacheKey(step, context) {
293
+ const crypto = require('crypto');
294
+ const data = JSON.stringify({
295
+ service: step.service,
296
+ operation: step.operation || step.id,
297
+ input: step.input,
298
+ contextInput: context.api_input
299
+ });
300
+ const hash = crypto.createHash('sha256').update(data).digest('hex');
301
+ return `workflow:step:${step.service}:${step.operation || step.id}:${hash}`;
302
+ }
303
+
304
+ /**
305
+ * Generate cookbook from OpenAPI spec (utility method)
306
+ * @method generateCookbook
307
+ * @param {Object} openApiSpec - OpenAPI specification
308
+ * @returns {Object} Generated cookbook
309
+ *
310
+ * @example
311
+ * const cookbook = orchestrator.generateCookbook(openApiSpec);
312
+ */
313
+ generateCookbook(openApiSpec) {
314
+ const { CookbookGenerator } = this.cookbook;
315
+
316
+ const generator = new CookbookGenerator({
317
+ defaultTimeout: 10000,
318
+ defaultRetry: {
319
+ maxAttempts: 3,
320
+ delayMs: 2000
321
+ }
322
+ });
323
+
324
+ return generator.generate(openApiSpec);
325
+ }
326
+
327
+ /**
328
+ * Transform API response using cookbook mapping
329
+ * @method transformResponse
330
+ * @param {Object} response - API response
331
+ * @param {Object} mapping - Output mapping
332
+ * @returns {Object} Transformed response
333
+ */
334
+ transformResponse(response, mapping) {
335
+ const { ResponseMapper } = this.cookbook;
336
+ const mapper = new ResponseMapper();
337
+ return mapper.mapResponse(response, mapping);
338
+ }
339
+ }
340
+
341
+ module.exports = WorkflowOrchestrator;
package/src/index.js ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module @onlineapps/conn-orch-orchestrator
5
+ * @description Workflow orchestration connector that handles message routing and workflow execution.
6
+ * Coordinates all other connectors to process workflow messages.
7
+ *
8
+ * @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-orch-orchestrator|GitHub Repository}
9
+ * @author OA Drive Team
10
+ * @license MIT
11
+ * @since 1.0.0
12
+ */
13
+
14
+ const WorkflowOrchestrator = require('./WorkflowOrchestrator');
15
+
16
+ /**
17
+ * Create orchestrator instance
18
+ * @function create
19
+ * @param {Object} config - Configuration options
20
+ * @param {Object} config.mqClient - MQ client instance
21
+ * @param {Object} config.registryClient - Registry client instance
22
+ * @param {Object} config.apiMapper - API mapper instance
23
+ * @param {Object} config.cookbook - Cookbook connector instance
24
+ * @param {Object} [config.logger] - Logger instance
25
+ * @returns {WorkflowOrchestrator} New orchestrator instance
26
+ *
27
+ * @example
28
+ * const orchestrator = create({
29
+ * mqClient: new MQClient(),
30
+ * registryClient: new RegistryClient(),
31
+ * apiMapper: new ApiMapper(),
32
+ * cookbook: new CookbookConnector()
33
+ * });
34
+ */
35
+ function create(config) {
36
+ return new WorkflowOrchestrator(config);
37
+ }
38
+
39
+ module.exports = {
40
+ WorkflowOrchestrator,
41
+ create,
42
+ VERSION: '1.0.0'
43
+ };