@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,694 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Universal Connector Engine
|
|
4
|
+
* @description Plugin-first connector engine with operation groups and full type safety
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.UniversalConnectorEngine = void 0;
|
|
8
|
+
exports.ConnectorEngine = ConnectorEngine;
|
|
9
|
+
const utils_1 = require("@etohq/framework/utils");
|
|
10
|
+
class PluginRegistryImpl {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.plugins = new Map();
|
|
13
|
+
}
|
|
14
|
+
register(name, plugin) {
|
|
15
|
+
if (!this.validate(plugin)) {
|
|
16
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Plugin ${name} failed validation`);
|
|
17
|
+
}
|
|
18
|
+
this.plugins.set(name, plugin);
|
|
19
|
+
}
|
|
20
|
+
get(name) {
|
|
21
|
+
return this.plugins.get(name);
|
|
22
|
+
}
|
|
23
|
+
list() {
|
|
24
|
+
return Array.from(this.plugins.keys());
|
|
25
|
+
}
|
|
26
|
+
validate(plugin) {
|
|
27
|
+
return (typeof plugin.getName === 'function' &&
|
|
28
|
+
typeof plugin.getVersion === 'function' &&
|
|
29
|
+
typeof plugin.getCapabilities === 'function' &&
|
|
30
|
+
typeof plugin.initialize === 'function' &&
|
|
31
|
+
typeof plugin.destroy === 'function' &&
|
|
32
|
+
typeof plugin.healthCheck === 'function');
|
|
33
|
+
}
|
|
34
|
+
getOperations(pluginName) {
|
|
35
|
+
const plugin = this.get(pluginName);
|
|
36
|
+
if (!plugin) {
|
|
37
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Plugin ${pluginName} not found`);
|
|
38
|
+
}
|
|
39
|
+
return Object.getOwnPropertyNames(plugin)
|
|
40
|
+
.filter(name => typeof plugin[name] === 'function' &&
|
|
41
|
+
!['getName', 'getVersion', 'getCapabilities', 'initialize', 'destroy', 'healthCheck', 'parseWebhook', 'validateWebhook'].includes(name));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ===== PLUGIN-FIRST CONNECTOR ENGINE FACTORY =====
|
|
45
|
+
function ConnectorEngine(plugins, operationGroups) {
|
|
46
|
+
as `execute${Capitalize}${Capitalize}`;
|
|
47
|
+
;
|
|
48
|
+
}
|
|
49
|
+
[keyof, TPlugins];
|
|
50
|
+
`execute${Capitalize}${Capitalize}`;
|
|
51
|
+
;
|
|
52
|
+
[(operation_groups_1.ExtractGroupNames)];
|
|
53
|
+
{ }
|
|
54
|
+
;
|
|
55
|
+
class ConnectorEngineImpl {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.plugins = plugins;
|
|
58
|
+
this.operationGroups = operationGroups || {};
|
|
59
|
+
this.pluginRegistry = new PluginRegistryImpl();
|
|
60
|
+
// ✅ Validate plugins against operation groups
|
|
61
|
+
this.validatePluginConfiguration();
|
|
62
|
+
// ✅ Register all plugins
|
|
63
|
+
this.registerPlugins();
|
|
64
|
+
// ✅ Generate dynamic methods
|
|
65
|
+
this.generateDynamicMethods();
|
|
66
|
+
}
|
|
67
|
+
// ✅ Generate dynamic methods: execute<Domain><Operation> and execute<GroupName><Operation>
|
|
68
|
+
generateDynamicMethods() {
|
|
69
|
+
// Generate domain methods: executeCalendarCreateEvent, executeNotificationSendEmail
|
|
70
|
+
this.generateDomainMethods();
|
|
71
|
+
// Generate group methods: executeBookingCreateEvent, executeBookingSendEmail
|
|
72
|
+
this.generateGroupMethods();
|
|
73
|
+
}
|
|
74
|
+
// ✅ Generate execute<Domain><Operation> methods
|
|
75
|
+
generateDomainMethods() {
|
|
76
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
77
|
+
const operations = this.pluginRegistry.getOperations(domain);
|
|
78
|
+
for (const operation of operations) {
|
|
79
|
+
// Pattern: execute<Domain><Operation>
|
|
80
|
+
const methodName = `execute${capitalize(domain)}${capitalize(operation)}`;
|
|
81
|
+
this[methodName] = async (input, sharedContext) => {
|
|
82
|
+
return await this.executeDomainOperation(domain, operation, input, sharedContext);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ✅ Generate execute<GroupName><Operation> methods
|
|
88
|
+
generateGroupMethods() {
|
|
89
|
+
if (!this.operationGroups)
|
|
90
|
+
return;
|
|
91
|
+
for (const [groupName, groupConfig] of Object.entries(this.operationGroups)) {
|
|
92
|
+
for (const operation of groupConfig.operations) {
|
|
93
|
+
// Pattern: execute<GroupName><Operation>
|
|
94
|
+
const methodName = `execute${capitalize(groupName)}${capitalize(operation)}`;
|
|
95
|
+
this[methodName] = async (input, sharedContext) => {
|
|
96
|
+
return await this.executeGroupOperation(groupName, operation, input, sharedContext);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ✅ Execute single domain operation (like EtoInternalService method)
|
|
102
|
+
async executeDomainOperation(domain, operation, input, sharedContext) {
|
|
103
|
+
const plugin = this.plugins[domain];
|
|
104
|
+
if (!plugin) {
|
|
105
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Plugin not found for domain: ${String(domain)}`);
|
|
106
|
+
}
|
|
107
|
+
const pluginMethod = plugin[operation];
|
|
108
|
+
if (!pluginMethod || typeof pluginMethod !== 'function') {
|
|
109
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Operation ${operation} not supported by plugin for domain ${String(domain)}`);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
// ✅ Call plugin method - can return value or workflow step
|
|
113
|
+
const result = await pluginMethod.call(plugin, {}, // config
|
|
114
|
+
input, { container: this.getContainerFromContext(sharedContext) });
|
|
115
|
+
// ✅ Handle both normal methods and workflow steps
|
|
116
|
+
if (this.isStepDefinition(result)) {
|
|
117
|
+
return await result.run(input, { container: this.getContainerFromContext(sharedContext) });
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.UNKNOWN_ERROR, `Plugin operation ${String(domain)}.${operation} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ✅ Execute operation group by group ID
|
|
126
|
+
async executeGroupOperation(groupId, operation, input, sharedContext) {
|
|
127
|
+
const groupConfig = this.operationGroups[groupId];
|
|
128
|
+
if (!groupConfig) {
|
|
129
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Operation group '${String(groupId)}' not found`);
|
|
130
|
+
}
|
|
131
|
+
if (!groupConfig.operations.includes(operation)) {
|
|
132
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Operation '${operation}' not supported by group '${String(groupId)}'`);
|
|
133
|
+
}
|
|
134
|
+
const { behavior } = groupConfig.options;
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
switch (behavior) {
|
|
137
|
+
case 'execute_all':
|
|
138
|
+
return await this.executeAllDomains(groupConfig.domains, operation, input, sharedContext, String(groupId), startTime);
|
|
139
|
+
case 'route_by_domain':
|
|
140
|
+
return await this.routeByDomain(groupConfig.domains, operation, input, groupConfig.options, sharedContext, String(groupId), startTime);
|
|
141
|
+
case 'first_success':
|
|
142
|
+
return await this.executeFirstSuccess(groupConfig.domains, operation, input, sharedContext, String(groupId), startTime);
|
|
143
|
+
default:
|
|
144
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Unsupported behavior: ${behavior}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ===== PRIVATE HELPER METHODS =====
|
|
148
|
+
// ✅ Register all plugins in the registry
|
|
149
|
+
registerPlugins() {
|
|
150
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
151
|
+
this.pluginRegistry.register(domain, plugin);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ✅ Extract container from ETO shared context
|
|
155
|
+
getContainerFromContext(sharedContext) {
|
|
156
|
+
// In ETO, container is available in shared context
|
|
157
|
+
return sharedContext?.__container__ || {};
|
|
158
|
+
}
|
|
159
|
+
// ✅ Execute all domains in parallel
|
|
160
|
+
async executeAllDomains(domains, operation, input, sharedContext, groupName, startTime) {
|
|
161
|
+
const results = {};
|
|
162
|
+
const errors = {};
|
|
163
|
+
// Execute all domains in parallel
|
|
164
|
+
const domainPromises = domains.map(async (domain) => {
|
|
165
|
+
try {
|
|
166
|
+
const result = await this.executeDomainOperation(domain, operation, input, sharedContext);
|
|
167
|
+
results[domain] = result;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
errors[domain] = error instanceof Error ? error : new Error(String(error));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
await Promise.all(domainPromises);
|
|
174
|
+
return {
|
|
175
|
+
groupName,
|
|
176
|
+
executedDomains: domains,
|
|
177
|
+
results,
|
|
178
|
+
metadata: {
|
|
179
|
+
behavior: 'execute_all',
|
|
180
|
+
executionTime: Date.now() - startTime,
|
|
181
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// ✅ Route to specific domain
|
|
186
|
+
async routeByDomain(groupDomains, operation, input, options, sharedContext, groupName, startTime) {
|
|
187
|
+
// ✅ Domain precedence: options.connectorId > first group domain
|
|
188
|
+
const targetDomain = options.connectorId || groupDomains[0];
|
|
189
|
+
// ✅ Validate domain is supported by this group
|
|
190
|
+
if (!groupDomains.includes(targetDomain)) {
|
|
191
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Domain '${targetDomain}' not supported by group '${groupName}'. Supported domains: ${groupDomains.join(', ')}`);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const result = await this.executeDomainOperation(targetDomain, operation, input, sharedContext);
|
|
195
|
+
return {
|
|
196
|
+
groupName,
|
|
197
|
+
executedDomains: [targetDomain],
|
|
198
|
+
results: { [targetDomain]: result },
|
|
199
|
+
metadata: {
|
|
200
|
+
behavior: 'route_by_domain',
|
|
201
|
+
connectorId: options.connectorId,
|
|
202
|
+
executionTime: Date.now() - startTime,
|
|
203
|
+
domainSource: options.connectorId ? 'options' : 'group_default'
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
groupName,
|
|
210
|
+
executedDomains: [targetDomain],
|
|
211
|
+
results: {},
|
|
212
|
+
metadata: {
|
|
213
|
+
behavior: 'route_by_domain',
|
|
214
|
+
connectorId: options.connectorId,
|
|
215
|
+
executionTime: Date.now() - startTime,
|
|
216
|
+
domainSource: options.connectorId ? 'options' : 'group_default',
|
|
217
|
+
errors: { [targetDomain]: error instanceof Error ? error : new Error(String(error)) }
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ✅ Execute domains until first success
|
|
223
|
+
async executeFirstSuccess(domains, operation, input, sharedContext, groupName, startTime) {
|
|
224
|
+
const errors = {};
|
|
225
|
+
for (const domain of domains) {
|
|
226
|
+
try {
|
|
227
|
+
const result = await this.executeDomainOperation(domain, operation, input, sharedContext);
|
|
228
|
+
return {
|
|
229
|
+
groupName,
|
|
230
|
+
executedDomains: [domain],
|
|
231
|
+
results: { [domain]: result },
|
|
232
|
+
metadata: {
|
|
233
|
+
behavior: 'first_success',
|
|
234
|
+
executionTime: Date.now() - startTime
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
errors[domain] = error instanceof Error ? error : new Error(String(error));
|
|
240
|
+
continue; // Try next domain
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// All domains failed
|
|
244
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.UNKNOWN_ERROR, `All domains failed for operation ${operation} in group ${groupName}. Errors: ${JSON.stringify(errors)}`);
|
|
245
|
+
}
|
|
246
|
+
// ===== PRIVATE HELPER METHODS =====
|
|
247
|
+
// ✅ Register all plugins in the registry
|
|
248
|
+
registerPlugins() {
|
|
249
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
250
|
+
this.pluginRegistry.register(domain, plugin);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ✅ Extract container from ETO shared context
|
|
254
|
+
getContainerFromContext(sharedContext) {
|
|
255
|
+
// In ETO, container is available in shared context
|
|
256
|
+
return sharedContext?.__container__ || {};
|
|
257
|
+
}
|
|
258
|
+
// ✅ Execute all domains in parallel
|
|
259
|
+
async executeAllDomains(domains, operation, input, sharedContext, groupName, startTime) {
|
|
260
|
+
const results = {};
|
|
261
|
+
const errors = {};
|
|
262
|
+
// Execute all domains in parallel
|
|
263
|
+
const domainPromises = domains.map(async (domain) => {
|
|
264
|
+
try {
|
|
265
|
+
const result = await this.executeDomainOperation(domain, operation, input, sharedContext);
|
|
266
|
+
results[domain] = result;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
errors[domain] = error instanceof Error ? error : new Error(String(error));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
await Promise.all(domainPromises);
|
|
273
|
+
return {
|
|
274
|
+
groupName,
|
|
275
|
+
executedDomains: domains,
|
|
276
|
+
results,
|
|
277
|
+
metadata: {
|
|
278
|
+
behavior: 'execute_all',
|
|
279
|
+
executionTime: Date.now() - startTime,
|
|
280
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// ✅ Route to specific domain
|
|
285
|
+
async routeByDomain(groupDomains, operation, input, options, sharedContext, groupName, startTime) {
|
|
286
|
+
// ✅ Domain precedence: options.connectorId > first group domain
|
|
287
|
+
const targetDomain = options.connectorId || groupDomains[0];
|
|
288
|
+
// ✅ Validate domain is supported by this group
|
|
289
|
+
if (!groupDomains.includes(targetDomain)) {
|
|
290
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Domain '${targetDomain}' not supported by group '${groupName}'. Supported domains: ${groupDomains.join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const result = await this.executeDomainOperation(targetDomain, operation, input, sharedContext);
|
|
294
|
+
return {
|
|
295
|
+
groupName,
|
|
296
|
+
executedDomains: [targetDomain],
|
|
297
|
+
results: { [targetDomain]: result },
|
|
298
|
+
metadata: {
|
|
299
|
+
behavior: 'route_by_domain',
|
|
300
|
+
connectorId: options.connectorId,
|
|
301
|
+
executionTime: Date.now() - startTime,
|
|
302
|
+
domainSource: options.connectorId ? 'options' : 'group_default'
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return {
|
|
308
|
+
groupName,
|
|
309
|
+
executedDomains: [targetDomain],
|
|
310
|
+
results: {},
|
|
311
|
+
metadata: {
|
|
312
|
+
behavior: 'route_by_domain',
|
|
313
|
+
connectorId: options.connectorId,
|
|
314
|
+
executionTime: Date.now() - startTime,
|
|
315
|
+
domainSource: options.connectorId ? 'options' : 'group_default',
|
|
316
|
+
errors: { [targetDomain]: error instanceof Error ? error : new Error(String(error)) }
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// ✅ Execute domains until first success
|
|
322
|
+
async executeFirstSuccess(domains, operation, input, sharedContext, groupName, startTime) {
|
|
323
|
+
const errors = {};
|
|
324
|
+
for (const domain of domains) {
|
|
325
|
+
try {
|
|
326
|
+
const result = await this.executeDomainOperation(domain, operation, input, sharedContext);
|
|
327
|
+
return {
|
|
328
|
+
groupName,
|
|
329
|
+
executedDomains: [domain],
|
|
330
|
+
results: { [domain]: result },
|
|
331
|
+
metadata: {
|
|
332
|
+
behavior: 'first_success',
|
|
333
|
+
executionTime: Date.now() - startTime
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
errors[domain] = error instanceof Error ? error : new Error(String(error));
|
|
339
|
+
continue; // Try next domain
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// All domains failed
|
|
343
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.UNKNOWN_ERROR, `All domains failed for operation ${operation} in group ${groupName}. Errors: ${JSON.stringify(errors)}`);
|
|
344
|
+
}
|
|
345
|
+
// ✅ Validate that operation groups reference valid plugins and operations
|
|
346
|
+
validatePluginConfiguration() {
|
|
347
|
+
if (!this.operationGroups)
|
|
348
|
+
return;
|
|
349
|
+
for (const [groupName, groupConfig] of Object.entries(this.operationGroups)) {
|
|
350
|
+
// Validate domains exist in plugins
|
|
351
|
+
for (const domain of groupConfig.domains) {
|
|
352
|
+
if (!this.plugins[domain]) {
|
|
353
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Operation group '${groupName}' references unknown domain '${domain}'. Available domains: ${Object.keys(this.plugins).join(', ')}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Validate operations exist in domain plugins
|
|
357
|
+
for (const domain of groupConfig.domains) {
|
|
358
|
+
const plugin = this.plugins[domain];
|
|
359
|
+
for (const operation of groupConfig.operations) {
|
|
360
|
+
const pluginMethod = plugin[operation];
|
|
361
|
+
if (!pluginMethod || typeof pluginMethod !== 'function') {
|
|
362
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Operation group '${groupName}' references unknown operation '${operation}' for domain '${domain}'`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ✅ Generate ETO-style methods dynamically
|
|
369
|
+
generateEtoStyleMethods() {
|
|
370
|
+
// Generate individual plugin methods (like EtoInternalService)
|
|
371
|
+
this.generatePluginMethods();
|
|
372
|
+
// Generate operation group methods based on group configuration
|
|
373
|
+
this.generateOperationGroupMethods();
|
|
374
|
+
}
|
|
375
|
+
// ✅ Generate individual plugin methods (domain.operation -> operationDomain)
|
|
376
|
+
generatePluginMethods() {
|
|
377
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
378
|
+
const operations = this.pluginRegistry.getOperations(domain);
|
|
379
|
+
for (const operation of operations) {
|
|
380
|
+
// ETO-style method naming: createCalendar, updateCalendar, etc.
|
|
381
|
+
const methodName = `${operation}${capitalize(domain)}`;
|
|
382
|
+
this[methodName] = async (input, sharedContext) => {
|
|
383
|
+
return await this.executeSingleOperation(domain, operation, input, sharedContext);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// ✅ Generate operation group methods based on group behavior
|
|
389
|
+
generateOperationGroupMethods() {
|
|
390
|
+
if (!this.operationGroups)
|
|
391
|
+
return;
|
|
392
|
+
for (const [groupName, groupConfig] of Object.entries(this.operationGroups)) {
|
|
393
|
+
for (const operation of groupConfig.operations) {
|
|
394
|
+
// ETO-style method naming based on group behavior
|
|
395
|
+
const methodName = this.getGroupMethodName(operation, groupName, groupConfig.options.behavior);
|
|
396
|
+
this[methodName] = async (input, sharedContext) => {
|
|
397
|
+
return await this.executeOperationGroup(groupName, operation, input, groupConfig, sharedContext);
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// ✅ Determine method name based on group behavior
|
|
403
|
+
getGroupMethodName(operation, groupName, behavior) {
|
|
404
|
+
switch (behavior) {
|
|
405
|
+
case 'execute_all':
|
|
406
|
+
// createBookingAll, updateBookingAll
|
|
407
|
+
return `${operation}${capitalize(groupName)}All`;
|
|
408
|
+
case 'route_by_domain':
|
|
409
|
+
// createBooking, updateBooking (routes to appropriate domain)
|
|
410
|
+
return `${operation}${capitalize(groupName)}`;
|
|
411
|
+
case 'first_success':
|
|
412
|
+
// createBookingFirstSuccess, updateBookingFirstSuccess
|
|
413
|
+
return `${operation}${capitalize(groupName)}FirstSuccess`;
|
|
414
|
+
default:
|
|
415
|
+
return `${operation}${capitalize(groupName)}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// ✅ Execute single plugin operation (like EtoInternalService method)
|
|
419
|
+
async executeSingleOperation(domain, operation, input, sharedContext) {
|
|
420
|
+
const plugin = this.plugins[domain];
|
|
421
|
+
if (!plugin) {
|
|
422
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Plugin not found for domain: ${domain}`);
|
|
423
|
+
}
|
|
424
|
+
const pluginMethod = plugin[operation];
|
|
425
|
+
if (!pluginMethod || typeof pluginMethod !== 'function') {
|
|
426
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.NOT_FOUND, `Operation ${operation} not supported by plugin for domain ${domain}`);
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
// ✅ Call plugin method directly (like EtoInternalService)
|
|
430
|
+
const result = await pluginMethod.call(plugin, {}, // config
|
|
431
|
+
input, { container: this.getContainerFromContext(sharedContext) });
|
|
432
|
+
if (this.isStepDefinition(result)) {
|
|
433
|
+
return await result.run(input, { container: this.getContainerFromContext(sharedContext) });
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.UNKNOWN_ERROR, `Plugin operation ${domain}.${operation} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// ✅ Execute operation group based on behavior
|
|
442
|
+
async executeOperationGroup(groupName, operation, input, groupConfig, sharedContext) {
|
|
443
|
+
const { behavior } = groupConfig.options;
|
|
444
|
+
switch (behavior) {
|
|
445
|
+
case 'execute_all':
|
|
446
|
+
return await this.executeAllDomains(groupConfig.domains, operation, input, sharedContext);
|
|
447
|
+
case 'route_by_domain':
|
|
448
|
+
return await this.routeByDomain(groupConfig.domains, operation, input, groupConfig.options, sharedContext);
|
|
449
|
+
case 'first_success':
|
|
450
|
+
return await this.executeFirstSuccess(groupConfig.domains, operation, input, sharedContext);
|
|
451
|
+
default:
|
|
452
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Unsupported behavior: ${behavior}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// ✅ Extract container from ETO shared context
|
|
456
|
+
getContainerFromContext(sharedContext) {
|
|
457
|
+
// In ETO, container is available in shared context
|
|
458
|
+
return sharedContext?.__container__ || {};
|
|
459
|
+
}
|
|
460
|
+
// ✅ Execute all domains in parallel with type safety
|
|
461
|
+
async executeAllDomains(domains, operation, input) {
|
|
462
|
+
const results = {};
|
|
463
|
+
const errors = {};
|
|
464
|
+
const startTime = Date.now();
|
|
465
|
+
// Execute all domains in parallel
|
|
466
|
+
const domainPromises = domains.map(async (domain) => {
|
|
467
|
+
try {
|
|
468
|
+
const result = await this.executeDomainOperation(domain, operation, input);
|
|
469
|
+
results[domain] = result;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
errors[domain] = error instanceof Error ? error : new Error(String(error));
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
await Promise.all(domainPromises);
|
|
476
|
+
return {
|
|
477
|
+
groupName: '', // Will be set by caller
|
|
478
|
+
executedDomains: domains,
|
|
479
|
+
results,
|
|
480
|
+
metadata: {
|
|
481
|
+
behavior: 'execute_all',
|
|
482
|
+
executionTime: Date.now() - startTime,
|
|
483
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// ✅ Route to specific domain based on input or group configuration
|
|
488
|
+
async routeByDomain(groupDomains, operation, input, options) {
|
|
489
|
+
const startTime = Date.now();
|
|
490
|
+
// ✅ Domain precedence: options.connectorId > first group domain
|
|
491
|
+
const targetDomain = options.connectorId || groupDomains[0];
|
|
492
|
+
// ✅ Validate domain is supported by this group
|
|
493
|
+
if (!groupDomains.includes(targetDomain)) {
|
|
494
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Domain '${targetDomain}' not supported by this operation group. ` +
|
|
495
|
+
`Supported domains: ${groupDomains.join(', ')}`);
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const result = await this.executeDomainOperation(targetDomain, operation, input);
|
|
499
|
+
return {
|
|
500
|
+
groupName: '', // Will be set by caller
|
|
501
|
+
executedDomains: [targetDomain],
|
|
502
|
+
results: { [targetDomain]: result },
|
|
503
|
+
metadata: {
|
|
504
|
+
behavior: 'route_by_domain',
|
|
505
|
+
connectorId: options.connectorId,
|
|
506
|
+
executionTime: Date.now() - startTime,
|
|
507
|
+
domainSource: options.connectorId ? 'options' : 'group_default'
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
return {
|
|
513
|
+
groupName: '',
|
|
514
|
+
executedDomains: [targetDomain],
|
|
515
|
+
results: {},
|
|
516
|
+
metadata: {
|
|
517
|
+
behavior: 'route_by_domain',
|
|
518
|
+
connectorId: options.connectorId,
|
|
519
|
+
executionTime: Date.now() - startTime,
|
|
520
|
+
domainSource: options.connectorId ? 'options' : 'group_default',
|
|
521
|
+
errors: { [targetDomain]: error instanceof Error ? error : new Error(String(error)) }
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// ✅ Execute domains until first success
|
|
527
|
+
async executeFirstSuccess(domains, operation, input) {
|
|
528
|
+
const startTime = Date.now();
|
|
529
|
+
const errors = {};
|
|
530
|
+
for (const domain of domains) {
|
|
531
|
+
try {
|
|
532
|
+
const result = await this.executeDomainOperation(domain, operation, input);
|
|
533
|
+
return {
|
|
534
|
+
groupName: '',
|
|
535
|
+
executedDomains: [domain],
|
|
536
|
+
results: { [domain]: result },
|
|
537
|
+
metadata: {
|
|
538
|
+
behavior: 'first_success',
|
|
539
|
+
executionTime: Date.now() - startTime
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
errors[domain] = error instanceof Error ? error : new Error(String(error));
|
|
545
|
+
continue; // Try next domain
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// All domains failed
|
|
549
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.UNKNOWN_ERROR, `All domains failed for operation ${operation}. Errors: ${JSON.stringify(errors)}`);
|
|
550
|
+
}
|
|
551
|
+
// ===== TYPE GUARDS =====
|
|
552
|
+
isStepDefinition(value) {
|
|
553
|
+
return value !== null &&
|
|
554
|
+
typeof value === 'object' &&
|
|
555
|
+
'run' in value &&
|
|
556
|
+
typeof value.run === 'function' &&
|
|
557
|
+
'stepId' in value &&
|
|
558
|
+
typeof value.stepId === 'string';
|
|
559
|
+
}
|
|
560
|
+
// ===== HEALTH MONITORING =====
|
|
561
|
+
async getSystemHealth() {
|
|
562
|
+
const pluginHealths = {};
|
|
563
|
+
let healthyCount = 0;
|
|
564
|
+
let totalCount = 0;
|
|
565
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
566
|
+
totalCount++;
|
|
567
|
+
try {
|
|
568
|
+
pluginHealths[domain] = await plugin.healthCheck();
|
|
569
|
+
if (pluginHealths[domain].status === 'healthy') {
|
|
570
|
+
healthyCount++;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
pluginHealths[domain] = {
|
|
575
|
+
status: 'unhealthy',
|
|
576
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
577
|
+
lastChecked: new Date()
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const healthRatio = totalCount > 0 ? healthyCount / totalCount : 1;
|
|
582
|
+
const overall = healthRatio === 1 ? 'healthy' : healthRatio >= 0.5 ? 'degraded' : 'unhealthy';
|
|
583
|
+
return { overall, plugins: pluginHealths };
|
|
584
|
+
}
|
|
585
|
+
// ===== PUBLIC API =====
|
|
586
|
+
getAvailableDomains() {
|
|
587
|
+
return Object.keys(this.plugins);
|
|
588
|
+
}
|
|
589
|
+
getAvailableOperations(domain) {
|
|
590
|
+
return this.pluginRegistry.getOperations(domain);
|
|
591
|
+
}
|
|
592
|
+
getOperationGroups() {
|
|
593
|
+
return this.operationGroups;
|
|
594
|
+
}
|
|
595
|
+
// ✅ Validate that operation groups reference valid plugins and operations
|
|
596
|
+
validatePluginConfiguration() {
|
|
597
|
+
if (!this.operationGroups)
|
|
598
|
+
return;
|
|
599
|
+
for (const [groupName, groupConfig] of Object.entries(this.operationGroups)) {
|
|
600
|
+
// Validate domains exist in plugins
|
|
601
|
+
for (const domain of groupConfig.domains) {
|
|
602
|
+
if (!this.plugins[domain]) {
|
|
603
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Operation group '${groupName}' references unknown domain '${domain}'. Available domains: ${Object.keys(this.plugins).join(', ')}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Validate operations exist in domain plugins
|
|
607
|
+
for (const domain of groupConfig.domains) {
|
|
608
|
+
const plugin = this.plugins[domain];
|
|
609
|
+
for (const operation of groupConfig.operations) {
|
|
610
|
+
const pluginMethod = plugin[operation];
|
|
611
|
+
if (!pluginMethod || typeof pluginMethod !== 'function') {
|
|
612
|
+
throw new utils_1.EtoError(utils_1.EtoError.Types.INVALID_DATA, `Operation group '${groupName}' references unknown operation '${operation}' for domain '${domain}'`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// ===== TYPE GUARDS =====
|
|
619
|
+
isStepDefinition(value) {
|
|
620
|
+
return value !== null &&
|
|
621
|
+
typeof value === 'object' &&
|
|
622
|
+
'run' in value &&
|
|
623
|
+
typeof value.run === 'function' &&
|
|
624
|
+
'stepId' in value &&
|
|
625
|
+
typeof value.stepId === 'string';
|
|
626
|
+
}
|
|
627
|
+
// ===== HEALTH MONITORING =====
|
|
628
|
+
async getSystemHealth() {
|
|
629
|
+
const pluginHealths = {};
|
|
630
|
+
let healthyCount = 0;
|
|
631
|
+
let totalCount = 0;
|
|
632
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
633
|
+
totalCount++;
|
|
634
|
+
try {
|
|
635
|
+
pluginHealths[domain] = await plugin.healthCheck();
|
|
636
|
+
if (pluginHealths[domain].status === 'healthy') {
|
|
637
|
+
healthyCount++;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
pluginHealths[domain] = {
|
|
642
|
+
status: 'unhealthy',
|
|
643
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
644
|
+
lastChecked: new Date()
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const healthRatio = totalCount > 0 ? healthyCount / totalCount : 1;
|
|
649
|
+
const overall = healthRatio === 1 ? 'healthy' : healthRatio >= 0.5 ? 'degraded' : 'unhealthy';
|
|
650
|
+
return { overall, plugins: pluginHealths };
|
|
651
|
+
}
|
|
652
|
+
// ===== PUBLIC API =====
|
|
653
|
+
getAvailableDomains() {
|
|
654
|
+
return Object.keys(this.plugins);
|
|
655
|
+
}
|
|
656
|
+
getAvailableOperations(domain) {
|
|
657
|
+
return this.pluginRegistry.getOperations(domain);
|
|
658
|
+
}
|
|
659
|
+
getOperationGroups() {
|
|
660
|
+
return this.operationGroups;
|
|
661
|
+
}
|
|
662
|
+
getPluginRegistry() {
|
|
663
|
+
return this.pluginRegistry;
|
|
664
|
+
}
|
|
665
|
+
// ✅ Get list of generated dynamic methods
|
|
666
|
+
getGeneratedMethods() {
|
|
667
|
+
const domainMethods = [];
|
|
668
|
+
const groupMethods = [];
|
|
669
|
+
// Domain methods
|
|
670
|
+
for (const [domain, plugin] of Object.entries(this.plugins)) {
|
|
671
|
+
const operations = this.pluginRegistry.getOperations(domain);
|
|
672
|
+
for (const operation of operations) {
|
|
673
|
+
domainMethods.push(`execute${capitalize(domain)}${capitalize(operation)}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Group methods
|
|
677
|
+
if (this.operationGroups) {
|
|
678
|
+
for (const [groupName, groupConfig] of Object.entries(this.operationGroups)) {
|
|
679
|
+
for (const operation of groupConfig.operations) {
|
|
680
|
+
groupMethods.push(`execute${capitalize(groupName)}${capitalize(operation)}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { domainMethods, groupMethods };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
exports.UniversalConnectorEngine = ConnectorEngineImpl;
|
|
688
|
+
return ConnectorEngineImpl;
|
|
689
|
+
return ConnectorEngineImpl;
|
|
690
|
+
// ===== UTILITY FUNCTIONS =====
|
|
691
|
+
function capitalize(str) {
|
|
692
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=connector-engine.js.map
|