@etohq/connector-engine 1.5.1-alpha.4

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.
Files changed (77) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +253 -0
  5. package/dist/engine/clean-connector-engine.d.ts +81 -0
  6. package/dist/engine/clean-connector-engine.d.ts.map +1 -0
  7. package/dist/engine/clean-connector-engine.js +350 -0
  8. package/dist/engine/clean-connector-engine.js.map +1 -0
  9. package/dist/engine/connector-engine-impl.d.ts +73 -0
  10. package/dist/engine/connector-engine-impl.d.ts.map +1 -0
  11. package/dist/engine/connector-engine-impl.js +332 -0
  12. package/dist/engine/connector-engine-impl.js.map +1 -0
  13. package/dist/engine/connector-engine.d.ts +54 -0
  14. package/dist/engine/connector-engine.d.ts.map +1 -0
  15. package/dist/engine/connector-engine.js +694 -0
  16. package/dist/engine/connector-engine.js.map +1 -0
  17. package/dist/engine/index.d.ts +7 -0
  18. package/dist/engine/index.d.ts.map +1 -0
  19. package/dist/engine/index.js +10 -0
  20. package/dist/engine/index.js.map +1 -0
  21. package/dist/engine/routing-engine.d.ts +26 -0
  22. package/dist/engine/routing-engine.d.ts.map +1 -0
  23. package/dist/engine/routing-engine.js +329 -0
  24. package/dist/engine/routing-engine.js.map +1 -0
  25. package/dist/examples/booking-connector-example.d.ts +7 -0
  26. package/dist/examples/booking-connector-example.d.ts.map +1 -0
  27. package/dist/examples/booking-connector-example.js +221 -0
  28. package/dist/examples/booking-connector-example.js.map +1 -0
  29. package/dist/examples/dynamic-methods-example.d.ts +7 -0
  30. package/dist/examples/dynamic-methods-example.d.ts.map +1 -0
  31. package/dist/examples/dynamic-methods-example.js +163 -0
  32. package/dist/examples/dynamic-methods-example.js.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +14 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/types/base-plugin.d.ts +170 -0
  38. package/dist/types/base-plugin.d.ts.map +1 -0
  39. package/dist/types/base-plugin.js +68 -0
  40. package/dist/types/base-plugin.js.map +1 -0
  41. package/dist/types/connector-plugin.d.ts +22 -0
  42. package/dist/types/connector-plugin.d.ts.map +1 -0
  43. package/dist/types/connector-plugin.js +11 -0
  44. package/dist/types/connector-plugin.js.map +1 -0
  45. package/dist/types/engine.d.ts +223 -0
  46. package/dist/types/engine.d.ts.map +1 -0
  47. package/dist/types/engine.js +7 -0
  48. package/dist/types/engine.js.map +1 -0
  49. package/dist/types/index.d.ts +5 -0
  50. package/dist/types/index.d.ts.map +1 -0
  51. package/dist/types/index.js +9 -0
  52. package/dist/types/index.js.map +1 -0
  53. package/dist/types/operation-groups.d.ts +78 -0
  54. package/dist/types/operation-groups.d.ts.map +1 -0
  55. package/dist/types/operation-groups.js +60 -0
  56. package/dist/types/operation-groups.js.map +1 -0
  57. package/dist/types/routing-config.d.ts +116 -0
  58. package/dist/types/routing-config.d.ts.map +1 -0
  59. package/dist/types/routing-config.js +6 -0
  60. package/dist/types/routing-config.js.map +1 -0
  61. package/dist/utils/create-connector-engine.d.ts +31 -0
  62. package/dist/utils/create-connector-engine.d.ts.map +1 -0
  63. package/dist/utils/create-connector-engine.js +30 -0
  64. package/dist/utils/create-connector-engine.js.map +1 -0
  65. package/examples/booking-example.ts +168 -0
  66. package/examples/booking-test.ts +231 -0
  67. package/hyperswitch-example.ts +263 -0
  68. package/jest.config.js +2 -0
  69. package/package.json +54 -0
  70. package/src/engine/clean-connector-engine.ts +726 -0
  71. package/src/engine/index.ts +13 -0
  72. package/src/engine/routing-engine.ts +394 -0
  73. package/src/index.ts +32 -0
  74. package/src/types/connector-plugin.ts +34 -0
  75. package/src/types/index.ts +5 -0
  76. package/src/types/routing-config.ts +196 -0
  77. package/tsconfig.json +3 -0
@@ -0,0 +1,726 @@
1
+ /**
2
+ * @fileoverview Clean Connector Engine - First Principles Implementation
3
+ */
4
+
5
+ import { ConnectorPlugin } from "../types/connector-plugin"
6
+ import { RoutingConfig, RoutingContext } from "../types/routing-config"
7
+ import { RoutingEngine } from "./routing-engine"
8
+ import {
9
+ createStep,
10
+ createWorkflow,
11
+ StepResponse,
12
+ WorkflowResponse,
13
+ ReturnWorkflow,
14
+ WorkflowData,
15
+ } from "@etohq/framework/workflows-sdk"
16
+
17
+ // ===== ENGINE TYPES =====
18
+
19
+ export interface CleanConnectorRegistry {
20
+ [connectorId: string]: ConnectorPlugin
21
+ }
22
+
23
+ export interface CleanOperationGroup {
24
+ connectors: string[]
25
+ operations: string[]
26
+ options: {
27
+ behavior: "execute_all" | "first_success" | "route_by_domain"
28
+ timeout?: number
29
+ retries?: number
30
+ }
31
+ }
32
+
33
+ export interface CleanOperationGroups {
34
+ [groupName: string]: CleanOperationGroup
35
+ }
36
+
37
+ export interface CleanEngineConfig {
38
+ configLoader: (connectorId: string) => Promise<any>
39
+ routingConfigLoader?: (groupId: string) => Promise<RoutingConfig> // Optional for gradual adoption
40
+ }
41
+
42
+ export interface CleanGroupResult<T> {
43
+ groupName: string
44
+ executedConnectors: string[]
45
+ results: Record<string, T>
46
+ metadata: {
47
+ behavior: string
48
+ executionTime: number
49
+ errors?: Record<string, Error>
50
+ routingDecision: any
51
+ algorithm: any
52
+ ruleApplied: string | undefined
53
+ }
54
+ }
55
+
56
+ // ===== TYPE GENERATION WITH AUTO-GROUPS =====
57
+
58
+ // Simplified auto-group methods (domain-based routing)
59
+ type AutoGroupMethods<TConnectors extends CleanConnectorRegistry> = {
60
+ [K in keyof TConnectors]: TConnectors[K] extends { getName(): infer Domain }
61
+ ? {
62
+ [Operation in keyof TConnectors[K]["operations"] as `execute${Capitalize<
63
+ Domain & string
64
+ >}${Capitalize<Operation & string>}`]: (
65
+ input: TConnectors[K]["operations"][Operation] extends (
66
+ input: infer I,
67
+ config: any
68
+ ) => any
69
+ ? I
70
+ : never,
71
+ context?: RoutingContext
72
+ ) => ReturnWorkflow<
73
+ TConnectors[K]["operations"][Operation] extends (
74
+ input: infer I,
75
+ config: any
76
+ ) => any
77
+ ? I
78
+ : never,
79
+ CleanGroupResult<
80
+ TConnectors[K]["operations"][Operation] extends (
81
+ input: any,
82
+ config: any
83
+ ) => Promise<infer O>
84
+ ? O
85
+ : never
86
+ >,
87
+ []
88
+ >
89
+ }
90
+ : {}
91
+ }[keyof TConnectors]
92
+
93
+ // Explicit group methods (existing)
94
+ type GroupMethods<TGroups extends CleanOperationGroups> = {
95
+ [GroupName in keyof TGroups]: {
96
+ [Operation in TGroups[GroupName]["operations"][number] as `execute${Capitalize<
97
+ GroupName & string
98
+ >}${Capitalize<Operation & string>}`]: (
99
+ input: any,
100
+ context?: RoutingContext
101
+ ) => ReturnWorkflow<any, CleanGroupResult<any>, []>
102
+ }
103
+ }[keyof TGroups]
104
+
105
+ type ConnectorMethods<TConnectors extends CleanConnectorRegistry> = {
106
+ [ConnectorId in keyof TConnectors]: {
107
+ [Operation in keyof TConnectors[ConnectorId]["operations"] as `execute${Capitalize<
108
+ ConnectorId & string
109
+ >}${Capitalize<Operation & string>}`]: (
110
+ input: TConnectors[ConnectorId]["operations"][Operation] extends (
111
+ input: infer I,
112
+ config: any
113
+ ) => any
114
+ ? I
115
+ : never
116
+ ) => ReturnWorkflow<
117
+ TConnectors[ConnectorId]["operations"][Operation] extends (
118
+ input: infer I,
119
+ config: any
120
+ ) => any
121
+ ? I
122
+ : never,
123
+ TConnectors[ConnectorId]["operations"][Operation] extends (
124
+ input: any,
125
+ config: any
126
+ ) => Promise<infer O>
127
+ ? O
128
+ : never,
129
+ []
130
+ >
131
+ }
132
+ }[keyof TConnectors]
133
+
134
+ // ===== ENGINE IMPLEMENTATION =====
135
+
136
+ class ConnectorEngineImpl<
137
+ TConnectors extends CleanConnectorRegistry,
138
+ TGroups extends CleanOperationGroups
139
+ > {
140
+ private connectors: TConnectors
141
+ private groups: TGroups
142
+ private configLoader: (connectorId: string) => Promise<any>
143
+ private routingConfigLoader?: (groupId: string) => Promise<RoutingConfig>
144
+ private routingEngine: RoutingEngine
145
+
146
+ constructor(
147
+ connectors: TConnectors,
148
+ groups: TGroups,
149
+ config: CleanEngineConfig
150
+ ) {
151
+ this.connectors = connectors
152
+ this.groups = groups
153
+ this.configLoader = config.configLoader
154
+ this.routingConfigLoader = config.routingConfigLoader
155
+ this.routingEngine = new RoutingEngine()
156
+
157
+ this.generateDynamicMethods()
158
+ }
159
+
160
+ // ===== DYNAMIC METHOD GENERATION =====
161
+
162
+ private generateDynamicMethods(): void {
163
+ this.generateConnectorMethods()
164
+ this.generateGroupMethods()
165
+ }
166
+
167
+ private generateConnectorMethods(): void {
168
+ for (const [connectorId, plugin] of Object.entries(this.connectors)) {
169
+ for (const operationName of Object.keys(plugin.operations)) {
170
+ const methodName = `execute${this.capitalize(
171
+ connectorId
172
+ )}${this.capitalize(operationName)}`
173
+
174
+ ;(this as any)[methodName] = (): ReturnWorkflow<any, any, []> => {
175
+ return this.createConnectorWorkflow(connectorId, operationName)
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ private generateGroupMethods(): void {
182
+ // Auto-generate group methods even without explicit groups
183
+ this.generateAutoGroups()
184
+
185
+ // Generate methods for explicit groups
186
+ for (const [groupName, groupConfig] of Object.entries(this.groups)) {
187
+ for (const operationName of groupConfig.operations) {
188
+ const methodName = `execute${this.capitalize(
189
+ groupName
190
+ )}${this.capitalize(operationName)}`
191
+
192
+ ;(this as any)[methodName] = (
193
+ input: any,
194
+ context?: RoutingContext
195
+ ): ReturnWorkflow<any, CleanGroupResult<any>, []> => {
196
+ return this.createSmartGroupWorkflow(
197
+ groupName,
198
+ operationName,
199
+ input,
200
+ context
201
+ )
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // Auto-generate groups for gradual adoption with type safety
208
+ private generateAutoGroups(): void {
209
+ const domainGroups: Record<
210
+ string,
211
+ { connectors: string[]; operations: Set<string> }
212
+ > = {}
213
+
214
+ // Group connectors by domain (plugin type)
215
+ for (const [connectorId, plugin] of Object.entries(this.connectors)) {
216
+ const domain = plugin.getName() // Use plugin name as domain
217
+
218
+ if (!domainGroups[domain]) {
219
+ domainGroups[domain] = { connectors: [], operations: new Set() }
220
+ }
221
+
222
+ domainGroups[domain].connectors.push(connectorId)
223
+
224
+ // Add all operations from this plugin
225
+ for (const op of Array.from(Object.keys(plugin.operations))) {
226
+ domainGroups[domain].operations.add(op)
227
+ }
228
+ }
229
+
230
+ // Generate group methods for each domain
231
+ for (const [domain, { connectors, operations }] of Object.entries(
232
+ domainGroups
233
+ )) {
234
+ for (const operationName of operations) {
235
+ const methodName = `execute${this.capitalize(domain)}${this.capitalize(
236
+ operationName
237
+ )}`
238
+
239
+ // Only generate if not already exists (explicit groups take precedence)
240
+ if (!(this as any)[methodName]) {
241
+ ;(this as any)[methodName] = (
242
+ input: any,
243
+ context?: RoutingContext
244
+ ): ReturnWorkflow<any, CleanGroupResult<any>, []> => {
245
+ return this.createAutoGroupWorkflow(
246
+ domain,
247
+ operationName,
248
+ input,
249
+ context,
250
+ connectors
251
+ )
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ // Create workflow for auto-generated groups
259
+ private createAutoGroupWorkflow<TInput, TOutput>(
260
+ domain: string,
261
+ operationName: string,
262
+ input: TInput,
263
+ context?: RoutingContext,
264
+ availableConnectors?: string[]
265
+ ): ReturnWorkflow<TInput, CleanGroupResult<TOutput>, []> {
266
+ return createWorkflow(
267
+ `${domain}-${operationName}-auto`,
268
+ (workflowInput: WorkflowData<TInput>) => {
269
+ const step = createStep(
270
+ `${domain}-${operationName}-auto-step`,
271
+ async (stepInput: TInput, { container }) => {
272
+ // Get connectors for this domain
273
+ const domainConnectors =
274
+ availableConnectors ||
275
+ Object.keys(this.connectors).filter(
276
+ (id) => this.connectors[id].getName() === domain
277
+ )
278
+
279
+ if (domainConnectors.length === 0) {
280
+ throw new Error(`No connectors available for domain: ${domain}`)
281
+ }
282
+
283
+ // Simple routing: try routing if available, otherwise use first connector
284
+ let selectedConnector: string
285
+
286
+ if (this.routingConfigLoader) {
287
+ try {
288
+ const routingConfig = await this.routingConfigLoader(domain)
289
+ const routingContext = this.extractRoutingContext(
290
+ stepInput,
291
+ context,
292
+ operationName,
293
+ domain
294
+ )
295
+ const routingDecision = await this.routingEngine.route(
296
+ routingContext,
297
+ routingConfig
298
+ )
299
+ selectedConnector = routingDecision.selected_connector
300
+ } catch {
301
+ // Fallback to simple selection if routing fails
302
+ selectedConnector = domainConnectors[0]
303
+ }
304
+ } else {
305
+ // No routing - use round robin or first available
306
+ selectedConnector = domainConnectors[0]
307
+ }
308
+
309
+ // Execute on selected connector
310
+ const config = await this.configLoader(selectedConnector)
311
+ const plugin = this.connectors[selectedConnector]
312
+ const operation =
313
+ plugin.operations[operationName as keyof typeof plugin.operations]
314
+
315
+ if (!operation) {
316
+ throw new Error(
317
+ `Operation ${operationName} not found on connector ${selectedConnector}`
318
+ )
319
+ }
320
+
321
+ const startTime = Date.now()
322
+ const result = await operation(stepInput, config)
323
+
324
+ return new StepResponse(
325
+ {
326
+ groupName: domain,
327
+ executedConnectors: [selectedConnector],
328
+ results: { [selectedConnector]: result },
329
+ metadata: {
330
+ behavior: "auto_group",
331
+ executionTime: Date.now() - startTime,
332
+ routingDecision: { selected_connector: selectedConnector, algorithm_used: 'auto', rule_applied: undefined },
333
+ algorithm: 'auto',
334
+ ruleApplied: undefined
335
+ },
336
+ },
337
+ result
338
+ )
339
+ }
340
+ )
341
+
342
+ const result = step(workflowInput)
343
+ return new WorkflowResponse(result)
344
+ }
345
+ )
346
+ }
347
+
348
+ private capitalize(str: string): string {
349
+ return str.charAt(0).toUpperCase() + str.slice(1)
350
+ }
351
+
352
+ // ===== WORKFLOW CREATION =====
353
+
354
+ private createConnectorWorkflow<TInput, TOutput>(
355
+ connectorId: string,
356
+ operationName: string
357
+ ): ReturnWorkflow<TInput, TOutput, []> {
358
+ return createWorkflow(
359
+ `${connectorId}-${operationName}`,
360
+ (workflowInput: WorkflowData<TInput>) => {
361
+ const step = createStep(
362
+ `${connectorId}-${operationName}-step`,
363
+ async (stepInput: TInput, { container }) => {
364
+ const config = await this.configLoader(connectorId)
365
+ const plugin = this.connectors[connectorId]
366
+ const operation =
367
+ plugin.operations[operationName as keyof typeof plugin.operations]
368
+
369
+ if (!operation) {
370
+ throw new Error(
371
+ `Operation ${operationName} not found on connector ${connectorId}`
372
+ )
373
+ }
374
+
375
+ const result = await operation(stepInput, config)
376
+ return new StepResponse(result, result)
377
+ }
378
+ )
379
+
380
+ const result = step(workflowInput)
381
+ return new WorkflowResponse(result)
382
+ }
383
+ )
384
+ }
385
+
386
+ private createSmartGroupWorkflow<TInput, TOutput>(
387
+ groupName: string,
388
+ operationName: string,
389
+ input: TInput,
390
+ context?: RoutingContext
391
+ ): ReturnWorkflow<TInput, CleanGroupResult<TOutput>, []> {
392
+ return createWorkflow(
393
+ `${groupName}-${operationName}-smart`,
394
+ (workflowInput: WorkflowData<TInput>) => {
395
+ const step = createStep(
396
+ `${groupName}-${operationName}-smart-step`,
397
+ async (stepInput: TInput, { container }) => {
398
+ // Load routing config from DB
399
+ const routingConfig = this.routingConfigLoader
400
+ ? await this.routingConfigLoader(groupName)
401
+ : {
402
+ profile_id: 'default',
403
+ name: 'default',
404
+ algorithm: 'priority' as any,
405
+ connectors: [],
406
+ rules: [],
407
+ created_at: new Date().toISOString(),
408
+ updated_at: new Date().toISOString(),
409
+ modified_at: new Date().toISOString(),
410
+ version: 1
411
+ }
412
+
413
+ // Create dynamic routing context from plugin input
414
+ const routingContext: RoutingContext = this.extractRoutingContext(
415
+ stepInput,
416
+ context,
417
+ operationName,
418
+ groupName
419
+ )
420
+
421
+ // Use smart routing to select connector
422
+ const routingDecision = await this.routingEngine.route(
423
+ routingContext,
424
+ routingConfig
425
+ )
426
+
427
+ // Execute on selected connector
428
+ const selectedConnector = routingDecision.selected_connector
429
+ const config = await this.configLoader(selectedConnector)
430
+ const plugin = this.connectors[selectedConnector]
431
+ const operation =
432
+ plugin.operations[operationName as keyof typeof plugin.operations]
433
+
434
+ if (!operation) {
435
+ throw new Error(
436
+ `Operation ${operationName} not found on connector ${selectedConnector}`
437
+ )
438
+ }
439
+
440
+ const startTime = Date.now()
441
+
442
+ try {
443
+ const result = await operation(stepInput, config)
444
+
445
+ // Update metrics for successful execution
446
+ await this.routingEngine.updateMetrics(
447
+ selectedConnector,
448
+ true,
449
+ Date.now() - startTime
450
+ )
451
+
452
+ return new StepResponse(
453
+ {
454
+ groupName,
455
+ executedConnectors: [selectedConnector],
456
+ results: { [selectedConnector]: result },
457
+ metadata: {
458
+ behavior: "smart_routing",
459
+ executionTime: Date.now() - startTime,
460
+ routingDecision,
461
+ algorithm: routingDecision.algorithm_used,
462
+ ruleApplied: routingDecision.rule_applied,
463
+ },
464
+ },
465
+ result
466
+ )
467
+ } catch (error) {
468
+ // Update metrics for failed execution
469
+ await this.routingEngine.updateMetrics(
470
+ selectedConnector,
471
+ false,
472
+ Date.now() - startTime
473
+ )
474
+
475
+ // Try fallback connectors if available
476
+ if (routingDecision.alternative_connectors.length > 0) {
477
+ return await this.tryFallbackConnectors(
478
+ routingDecision.alternative_connectors,
479
+ operationName,
480
+ stepInput,
481
+ groupName,
482
+ startTime,
483
+ routingContext
484
+ )
485
+ }
486
+
487
+ throw error
488
+ }
489
+ }
490
+ )
491
+
492
+ const result = step(workflowInput)
493
+ return new WorkflowResponse(result)
494
+ }
495
+ )
496
+ }
497
+
498
+ // ===== DYNAMIC CONTEXT EXTRACTION =====
499
+
500
+ private extractRoutingContext<TInput>(
501
+ input: TInput,
502
+ providedContext?: RoutingContext,
503
+ operationName?: string,
504
+ groupName?: string
505
+ ): RoutingContext {
506
+ const inputObj = input as any
507
+
508
+ // Start with provided context or defaults
509
+ const baseContext: RoutingContext = {
510
+ operation: operationName,
511
+ domain: groupName,
512
+ ...providedContext,
513
+ }
514
+
515
+ // Extract common fields from input (works for any domain)
516
+ if (inputObj.region !== undefined) baseContext.region = inputObj.region
517
+ if (inputObj.country !== undefined) baseContext.country = inputObj.country
518
+ if (inputObj.userId !== undefined) baseContext.userId = inputObj.userId
519
+ if (inputObj.user_id !== undefined) baseContext.userId = inputObj.user_id
520
+ if (inputObj.tenantId !== undefined)
521
+ baseContext.tenantId = inputObj.tenantId
522
+ if (inputObj.tenant_id !== undefined)
523
+ baseContext.tenantId = inputObj.tenant_id
524
+ if (inputObj.priority !== undefined)
525
+ baseContext.priority = inputObj.priority
526
+ if (inputObj.resourceType !== undefined)
527
+ baseContext.resourceType = inputObj.resourceType
528
+ if (inputObj.resource_type !== undefined)
529
+ baseContext.resourceType = inputObj.resource_type
530
+
531
+ // Extract size/amount fields (could be file size, payment amount, etc.)
532
+ if (inputObj.size !== undefined) baseContext.resourceSize = inputObj.size
533
+ if (inputObj.amount !== undefined)
534
+ baseContext.resourceSize = inputObj.amount
535
+ if (inputObj.fileSize !== undefined)
536
+ baseContext.resourceSize = inputObj.fileSize
537
+ if (inputObj.file_size !== undefined)
538
+ baseContext.resourceSize = inputObj.file_size
539
+
540
+ // Include all custom fields for rule evaluation
541
+ baseContext.metadata = {
542
+ ...baseContext.metadata,
543
+ operation: operationName,
544
+ domain: groupName,
545
+ inputFields: Object.keys(inputObj),
546
+ // Include any custom fields from input for rule evaluation
547
+ customFields: this.extractCustomFields(inputObj),
548
+ }
549
+
550
+ return baseContext
551
+ }
552
+
553
+ private extractCustomFields(input: any): Record<string, any> {
554
+ const customFields: Record<string, any> = {}
555
+ const standardFields = [
556
+ "region",
557
+ "country",
558
+ "userId",
559
+ "user_id",
560
+ "tenantId",
561
+ "tenant_id",
562
+ "priority",
563
+ "resourceType",
564
+ "resource_type",
565
+ "size",
566
+ "amount",
567
+ "fileSize",
568
+ "file_size",
569
+ ]
570
+
571
+ for (const [key, value] of Object.entries(input)) {
572
+ if (!standardFields.includes(key)) {
573
+ customFields[key] = value
574
+ }
575
+ }
576
+
577
+ return customFields
578
+ }
579
+
580
+ private async tryFallbackConnectors<TInput>(
581
+ fallbackConnectors: string[],
582
+ operationName: string,
583
+ input: TInput,
584
+ groupName: string,
585
+ originalStartTime: number,
586
+ context: RoutingContext
587
+ ): Promise<StepResponse<CleanGroupResult<any>, any>> {
588
+ const errors: Record<string, Error> = {}
589
+
590
+ for (const connectorId of fallbackConnectors) {
591
+ try {
592
+ const config = await this.configLoader(connectorId)
593
+ const plugin = this.connectors[connectorId]
594
+ const operation =
595
+ plugin.operations[operationName as keyof typeof plugin.operations]
596
+
597
+ if (!operation) continue
598
+
599
+ const startTime = Date.now()
600
+ const result = await operation(input, config)
601
+
602
+ // Update metrics for successful fallback
603
+ await this.routingEngine.updateMetrics(
604
+ connectorId,
605
+ true,
606
+ Date.now() - startTime
607
+ )
608
+
609
+ return new StepResponse(
610
+ {
611
+ groupName,
612
+ executedConnectors: [connectorId],
613
+ results: { [connectorId]: result },
614
+ metadata: {
615
+ behavior: "fallback_routing",
616
+ executionTime: Date.now() - originalStartTime,
617
+ routingDecision: { selected_connector: connectorId, algorithm_used: 'fallback', rule_applied: undefined },
618
+ algorithm: 'fallback',
619
+ ruleApplied: undefined
620
+ },
621
+ },
622
+ result
623
+ )
624
+ } catch (error) {
625
+ errors[connectorId] =
626
+ error instanceof Error ? error : new Error(String(error))
627
+
628
+ // Update metrics for failed fallback
629
+ await this.routingEngine.updateMetrics(
630
+ connectorId,
631
+ false,
632
+ Date.now() - originalStartTime
633
+ )
634
+ }
635
+ }
636
+
637
+ throw new Error(
638
+ `All connectors failed for ${groupName}.${operationName}. Errors: ${JSON.stringify(
639
+ errors
640
+ )}`
641
+ )
642
+ }
643
+
644
+ // ===== PUBLIC API =====
645
+
646
+ async execute<
647
+ ConnectorId extends keyof TConnectors,
648
+ Operation extends keyof TConnectors[ConnectorId]["operations"]
649
+ >(connectorId: ConnectorId, operation: Operation, input: any): Promise<any> {
650
+ const config = await this.configLoader(connectorId as string)
651
+ const plugin = this.connectors[connectorId]
652
+ const operationFn =
653
+ plugin.operations[operation as keyof typeof plugin.operations]
654
+
655
+ if (!operationFn) {
656
+ throw new Error(
657
+ `Operation ${String(operation)} not found on connector ${String(
658
+ connectorId
659
+ )}`
660
+ )
661
+ }
662
+
663
+ return await operationFn(input, config)
664
+ }
665
+
666
+ async executeGroup<GroupName extends keyof TGroups>(
667
+ groupName: GroupName,
668
+ operation: string,
669
+ input: any
670
+ ): Promise<CleanGroupResult<any>> {
671
+ // Use smart routing workflow and extract result
672
+ const workflow = this.createSmartGroupWorkflow(
673
+ groupName as string,
674
+ operation,
675
+ input
676
+ )
677
+ const result = await workflow.run({ input })
678
+
679
+ return result.result as CleanGroupResult<any>
680
+ }
681
+ }
682
+
683
+ // ===== ENGINE FACTORY =====
684
+
685
+ export function ConnectorEngine<
686
+ TConnectors extends CleanConnectorRegistry,
687
+ TGroups extends CleanOperationGroups = {}
688
+ >(
689
+ connectors: TConnectors,
690
+ groups?: TGroups,
691
+ config?: CleanEngineConfig
692
+ ): ConnectorEngineImpl<TConnectors, TGroups> &
693
+ ConnectorMethods<TConnectors> &
694
+ AutoGroupMethods<TConnectors> &
695
+ GroupMethods<TGroups> {
696
+ const engineConfig: CleanEngineConfig = config || {
697
+ configLoader: async () => ({}), // Default empty config
698
+ routingConfigLoader: async () => ({
699
+ profile_id: "default",
700
+ name: "default",
701
+ algorithm: { type: "priority", data: [] },
702
+ created_at: new Date().toISOString(),
703
+ modified_at: new Date().toISOString(),
704
+ version: 1,
705
+ }),
706
+ }
707
+
708
+ return new ConnectorEngineImpl(
709
+ connectors,
710
+ groups || ({} as TGroups),
711
+ engineConfig
712
+ ) as ConnectorEngineImpl<TConnectors, TGroups> &
713
+ ConnectorMethods<TConnectors> &
714
+ AutoGroupMethods<TConnectors> &
715
+ GroupMethods<TGroups>
716
+ }
717
+
718
+ // ===== EXPORTS =====
719
+
720
+ export type {
721
+ CleanConnectorRegistry as ConnectorRegistry,
722
+ CleanOperationGroup as OperationGroup,
723
+ CleanOperationGroups as OperationGroups,
724
+ CleanEngineConfig as EngineConfig,
725
+ CleanGroupResult as GroupResult,
726
+ }