@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/engine/clean-connector-engine.d.ts +81 -0
- package/dist/engine/clean-connector-engine.d.ts.map +1 -0
- package/dist/engine/clean-connector-engine.js +350 -0
- package/dist/engine/clean-connector-engine.js.map +1 -0
- package/dist/engine/connector-engine-impl.d.ts +73 -0
- package/dist/engine/connector-engine-impl.d.ts.map +1 -0
- package/dist/engine/connector-engine-impl.js +332 -0
- package/dist/engine/connector-engine-impl.js.map +1 -0
- package/dist/engine/connector-engine.d.ts +54 -0
- package/dist/engine/connector-engine.d.ts.map +1 -0
- package/dist/engine/connector-engine.js +694 -0
- package/dist/engine/connector-engine.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +10 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/routing-engine.d.ts +26 -0
- package/dist/engine/routing-engine.d.ts.map +1 -0
- package/dist/engine/routing-engine.js +329 -0
- package/dist/engine/routing-engine.js.map +1 -0
- package/dist/examples/booking-connector-example.d.ts +7 -0
- package/dist/examples/booking-connector-example.d.ts.map +1 -0
- package/dist/examples/booking-connector-example.js +221 -0
- package/dist/examples/booking-connector-example.js.map +1 -0
- package/dist/examples/dynamic-methods-example.d.ts +7 -0
- package/dist/examples/dynamic-methods-example.d.ts.map +1 -0
- package/dist/examples/dynamic-methods-example.js +163 -0
- package/dist/examples/dynamic-methods-example.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/types/base-plugin.d.ts +170 -0
- package/dist/types/base-plugin.d.ts.map +1 -0
- package/dist/types/base-plugin.js +68 -0
- package/dist/types/base-plugin.js.map +1 -0
- package/dist/types/connector-plugin.d.ts +22 -0
- package/dist/types/connector-plugin.d.ts.map +1 -0
- package/dist/types/connector-plugin.js +11 -0
- package/dist/types/connector-plugin.js.map +1 -0
- package/dist/types/engine.d.ts +223 -0
- package/dist/types/engine.d.ts.map +1 -0
- package/dist/types/engine.js +7 -0
- package/dist/types/engine.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/operation-groups.d.ts +78 -0
- package/dist/types/operation-groups.d.ts.map +1 -0
- package/dist/types/operation-groups.js +60 -0
- package/dist/types/operation-groups.js.map +1 -0
- package/dist/types/routing-config.d.ts +116 -0
- package/dist/types/routing-config.d.ts.map +1 -0
- package/dist/types/routing-config.js +6 -0
- package/dist/types/routing-config.js.map +1 -0
- package/dist/utils/create-connector-engine.d.ts +31 -0
- package/dist/utils/create-connector-engine.d.ts.map +1 -0
- package/dist/utils/create-connector-engine.js +30 -0
- package/dist/utils/create-connector-engine.js.map +1 -0
- package/examples/booking-example.ts +168 -0
- package/examples/booking-test.ts +231 -0
- package/hyperswitch-example.ts +263 -0
- package/jest.config.js +2 -0
- package/package.json +54 -0
- package/src/engine/clean-connector-engine.ts +726 -0
- package/src/engine/index.ts +13 -0
- package/src/engine/routing-engine.ts +394 -0
- package/src/index.ts +32 -0
- package/src/types/connector-plugin.ts +34 -0
- package/src/types/index.ts +5 -0
- package/src/types/routing-config.ts +196 -0
- 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
|
+
}
|