@friggframework/core 2.0.0--canary.395.6ee402d.0 → 2.0.0--canary.395.704a466.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
//todo: this test can probably be removed later
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration test to verify that the auth flow works without requiring a database record
|
|
5
|
+
* This test simulates the actual problem we're solving.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
IntegrationEventDispatcher,
|
|
10
|
+
} = require('./integration-event-dispatcher');
|
|
11
|
+
const { IntegrationBase } = require('../integrations/integration-base');
|
|
12
|
+
|
|
13
|
+
// Simulate AsanaIntegration-like class
|
|
14
|
+
class SimulatedAsanaIntegration extends IntegrationBase {
|
|
15
|
+
static Definition = {
|
|
16
|
+
name: 'asana',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
modules: {
|
|
19
|
+
asana: { definition: { name: 'asana' } },
|
|
20
|
+
frontify: { definition: { name: 'frontify' } },
|
|
21
|
+
},
|
|
22
|
+
routes: [
|
|
23
|
+
{ path: '/auth', method: 'GET', event: 'AUTH_REQUEST' },
|
|
24
|
+
{
|
|
25
|
+
path: '/auth/redirect/:provider',
|
|
26
|
+
method: 'GET',
|
|
27
|
+
event: 'AUTH_REDIRECT',
|
|
28
|
+
},
|
|
29
|
+
{ path: '/fetch-form', method: 'GET', event: 'LOAD_FORM' },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
constructor(params) {
|
|
34
|
+
super(params);
|
|
35
|
+
|
|
36
|
+
// Define events like AsanaIntegration does
|
|
37
|
+
this.events = {
|
|
38
|
+
AUTH_REQUEST: {
|
|
39
|
+
handler: this.authRequest.bind(this),
|
|
40
|
+
},
|
|
41
|
+
AUTH_REDIRECT: {
|
|
42
|
+
handler: this.authRedirect.bind(this),
|
|
43
|
+
},
|
|
44
|
+
LOAD_FORM: {
|
|
45
|
+
handler: this.loadForm.bind(this),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async authRequest({ req, res }) {
|
|
51
|
+
// This should work WITHOUT any database record
|
|
52
|
+
// Simulating OAuth flow initiation
|
|
53
|
+
const provider = req.params?.provider || 'asana';
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
action: 'redirect',
|
|
58
|
+
url: `https://app.asana.com/oauth/authorize?client_id=test&redirect_uri=test`,
|
|
59
|
+
provider: provider,
|
|
60
|
+
message: 'Auth initiated without database record!',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async authRedirect({ req, res }) {
|
|
65
|
+
// Handle OAuth callback
|
|
66
|
+
const code = req.query?.code;
|
|
67
|
+
|
|
68
|
+
if (!code) {
|
|
69
|
+
throw new Error('No authorization code provided');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// In real implementation, would exchange code for tokens
|
|
73
|
+
// and create database record here
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
action: 'tokens_received',
|
|
77
|
+
message: 'Would create integration record here',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async loadForm({ req, res }) {
|
|
82
|
+
// This SHOULD require database record
|
|
83
|
+
if (!this.id) {
|
|
84
|
+
throw new Error('Integration not found - must authenticate first');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
form: {
|
|
90
|
+
fields: ['field1', 'field2'],
|
|
91
|
+
},
|
|
92
|
+
integrationId: this.id,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe('Auth Flow Integration Test', () => {
|
|
98
|
+
let dispatcher;
|
|
99
|
+
let mockIntegrationRepository;
|
|
100
|
+
let mockModuleRepository;
|
|
101
|
+
let mockModuleFactory;
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
// Create mocks
|
|
105
|
+
mockIntegrationRepository = {
|
|
106
|
+
findIntegrationByName: jest.fn(),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
mockModuleRepository = {
|
|
110
|
+
findEntitiesByIds: jest.fn(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
mockModuleFactory = {
|
|
114
|
+
getModuleInstance: jest.fn(),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Create dispatcher
|
|
118
|
+
dispatcher = new IntegrationEventDispatcher({
|
|
119
|
+
integrationRepository: mockIntegrationRepository,
|
|
120
|
+
moduleFactory: mockModuleFactory,
|
|
121
|
+
moduleRepository: mockModuleRepository,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('First-time authentication (no database record)', () => {
|
|
126
|
+
it('should handle AUTH_REQUEST without database record', async () => {
|
|
127
|
+
// Simulate: No integration exists in database yet
|
|
128
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(
|
|
129
|
+
null
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const req = { params: { provider: 'asana' }, query: {} };
|
|
133
|
+
const res = { json: jest.fn(), redirect: jest.fn() };
|
|
134
|
+
const next = jest.fn();
|
|
135
|
+
|
|
136
|
+
// This is the key test - auth should work WITHOUT database record
|
|
137
|
+
const result = await dispatcher.dispatchHttp({
|
|
138
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
139
|
+
event: 'AUTH_REQUEST',
|
|
140
|
+
req,
|
|
141
|
+
res,
|
|
142
|
+
next,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result).toMatchObject({
|
|
146
|
+
success: true,
|
|
147
|
+
action: 'redirect',
|
|
148
|
+
message: 'Auth initiated without database record!',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Verify we tried to look up the integration
|
|
152
|
+
expect(
|
|
153
|
+
mockIntegrationRepository.findIntegrationByName
|
|
154
|
+
).toHaveBeenCalledWith('asana');
|
|
155
|
+
|
|
156
|
+
// Verify we didn't try to load modules (stateless)
|
|
157
|
+
expect(mockModuleFactory.getModuleInstance).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle AUTH_REDIRECT without database record', async () => {
|
|
161
|
+
// Still no database record during OAuth callback
|
|
162
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(
|
|
163
|
+
null
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const req = {
|
|
167
|
+
params: { provider: 'asana' },
|
|
168
|
+
query: { code: 'auth_code_123' },
|
|
169
|
+
};
|
|
170
|
+
const res = { json: jest.fn() };
|
|
171
|
+
const next = jest.fn();
|
|
172
|
+
|
|
173
|
+
const result = await dispatcher.dispatchHttp({
|
|
174
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
175
|
+
event: 'AUTH_REDIRECT',
|
|
176
|
+
req,
|
|
177
|
+
res,
|
|
178
|
+
next,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result).toMatchObject({
|
|
182
|
+
success: true,
|
|
183
|
+
action: 'tokens_received',
|
|
184
|
+
message: 'Would create integration record here',
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should fail LOAD_FORM without database record', async () => {
|
|
189
|
+
// No database record means not authenticated
|
|
190
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(
|
|
191
|
+
null
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const req = { query: {} };
|
|
195
|
+
const res = { json: jest.fn() };
|
|
196
|
+
const next = jest.fn();
|
|
197
|
+
|
|
198
|
+
// This should fail because loadForm requires database state
|
|
199
|
+
await expect(
|
|
200
|
+
dispatcher.dispatchHttp({
|
|
201
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
202
|
+
event: 'LOAD_FORM',
|
|
203
|
+
req,
|
|
204
|
+
res,
|
|
205
|
+
next,
|
|
206
|
+
})
|
|
207
|
+
).rejects.toThrow(
|
|
208
|
+
'Integration not found - must authenticate first'
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('Authenticated user (with database record)', () => {
|
|
214
|
+
it('should handle LOAD_FORM with database record', async () => {
|
|
215
|
+
// Simulate existing integration in database
|
|
216
|
+
const mockRecord = {
|
|
217
|
+
id: 'integration-abc-123',
|
|
218
|
+
userId: 'user-456',
|
|
219
|
+
config: {
|
|
220
|
+
type: 'asana',
|
|
221
|
+
asanaWorkspaceId: 'workspace-789',
|
|
222
|
+
},
|
|
223
|
+
status: 'ENABLED',
|
|
224
|
+
version: '1.0.0',
|
|
225
|
+
messages: { errors: [], warnings: [] },
|
|
226
|
+
entitiesIds: ['entity-asana', 'entity-frontify'],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const mockEntities = [
|
|
230
|
+
{ _id: 'entity-asana', name: 'asana' },
|
|
231
|
+
{ _id: 'entity-frontify', name: 'frontify' },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const mockAsanaModule = {
|
|
235
|
+
getName: () => 'asana',
|
|
236
|
+
api: { getUser: jest.fn() },
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const mockFrontifyModule = {
|
|
240
|
+
getName: () => 'frontify',
|
|
241
|
+
api: { getProjects: jest.fn() },
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(
|
|
245
|
+
mockRecord
|
|
246
|
+
);
|
|
247
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue(
|
|
248
|
+
mockEntities
|
|
249
|
+
);
|
|
250
|
+
mockModuleFactory.getModuleInstance
|
|
251
|
+
.mockResolvedValueOnce(mockAsanaModule)
|
|
252
|
+
.mockResolvedValueOnce(mockFrontifyModule);
|
|
253
|
+
|
|
254
|
+
const req = { query: {} };
|
|
255
|
+
const res = { json: jest.fn() };
|
|
256
|
+
const next = jest.fn();
|
|
257
|
+
|
|
258
|
+
const result = await dispatcher.dispatchHttp({
|
|
259
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
260
|
+
event: 'LOAD_FORM',
|
|
261
|
+
req,
|
|
262
|
+
res,
|
|
263
|
+
next,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(result).toMatchObject({
|
|
267
|
+
success: true,
|
|
268
|
+
form: {
|
|
269
|
+
fields: expect.any(Array),
|
|
270
|
+
},
|
|
271
|
+
integrationId: 'integration-abc-123',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Verify modules were loaded
|
|
275
|
+
expect(mockModuleFactory.getModuleInstance).toHaveBeenCalledTimes(
|
|
276
|
+
2
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('Error scenarios', () => {
|
|
282
|
+
it('should gracefully handle database connection errors during auth', async () => {
|
|
283
|
+
// Database is down but auth should still work
|
|
284
|
+
mockIntegrationRepository.findIntegrationByName.mockRejectedValue(
|
|
285
|
+
new Error('MongoDB connection failed')
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const req = { params: { provider: 'asana' }, query: {} };
|
|
289
|
+
const res = { json: jest.fn() };
|
|
290
|
+
const next = jest.fn();
|
|
291
|
+
|
|
292
|
+
// Should still work with stateless instance
|
|
293
|
+
const result = await dispatcher.dispatchHttp({
|
|
294
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
295
|
+
event: 'AUTH_REQUEST',
|
|
296
|
+
req,
|
|
297
|
+
res,
|
|
298
|
+
next,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(result).toMatchObject({
|
|
302
|
+
success: true,
|
|
303
|
+
action: 'redirect',
|
|
304
|
+
message: 'Auth initiated without database record!',
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should handle missing event gracefully', async () => {
|
|
309
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(
|
|
310
|
+
null
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const req = {};
|
|
314
|
+
const res = { json: jest.fn() };
|
|
315
|
+
const next = jest.fn();
|
|
316
|
+
|
|
317
|
+
await expect(
|
|
318
|
+
dispatcher.dispatchHttp({
|
|
319
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
320
|
+
event: 'UNKNOWN_EVENT',
|
|
321
|
+
req,
|
|
322
|
+
res,
|
|
323
|
+
next,
|
|
324
|
+
})
|
|
325
|
+
).rejects.toThrow('Event UNKNOWN_EVENT not registered for asana');
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('Real-world scenario simulation', () => {
|
|
331
|
+
it('should handle complete auth flow from start to finish', async () => {
|
|
332
|
+
const mockIntegrationRepository = {
|
|
333
|
+
findIntegrationByName: jest.fn(),
|
|
334
|
+
createIntegration: jest.fn(),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const mockModuleRepository = {
|
|
338
|
+
findEntitiesByIds: jest.fn(),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const mockModuleFactory = {
|
|
342
|
+
getModuleInstance: jest.fn(),
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const dispatcher = new IntegrationEventDispatcher({
|
|
346
|
+
integrationRepository: mockIntegrationRepository,
|
|
347
|
+
moduleFactory: mockModuleFactory,
|
|
348
|
+
moduleRepository: mockModuleRepository,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Step 1: User initiates auth - no DB record exists
|
|
352
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
353
|
+
|
|
354
|
+
const authReq = { params: { provider: 'asana' } };
|
|
355
|
+
const authRes = { json: jest.fn() };
|
|
356
|
+
const authNext = jest.fn();
|
|
357
|
+
|
|
358
|
+
const step1 = await dispatcher.dispatchHttp({
|
|
359
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
360
|
+
event: 'AUTH_REQUEST',
|
|
361
|
+
req: authReq,
|
|
362
|
+
res: authRes,
|
|
363
|
+
next: authNext,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(step1.success).toBe(true);
|
|
367
|
+
expect(step1.action).toBe('redirect');
|
|
368
|
+
|
|
369
|
+
// Step 2: OAuth callback - still no DB record
|
|
370
|
+
const callbackReq = {
|
|
371
|
+
params: { provider: 'asana' },
|
|
372
|
+
query: { code: 'oauth_code_xyz' },
|
|
373
|
+
};
|
|
374
|
+
const callbackRes = { json: jest.fn() };
|
|
375
|
+
const callbackNext = jest.fn();
|
|
376
|
+
|
|
377
|
+
const step2 = await dispatcher.dispatchHttp({
|
|
378
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
379
|
+
event: 'AUTH_REDIRECT',
|
|
380
|
+
req: callbackReq,
|
|
381
|
+
res: callbackRes,
|
|
382
|
+
next: callbackNext,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(step2.success).toBe(true);
|
|
386
|
+
expect(step2.message).toBe('Would create integration record here');
|
|
387
|
+
|
|
388
|
+
// In real implementation, the AUTH_REDIRECT handler would create the DB record
|
|
389
|
+
// Let's simulate that it was created
|
|
390
|
+
|
|
391
|
+
// Step 3: Now user can access protected routes with DB record
|
|
392
|
+
const mockRecord = {
|
|
393
|
+
id: 'new-integration-123',
|
|
394
|
+
userId: 'user-456',
|
|
395
|
+
config: { type: 'asana' },
|
|
396
|
+
status: 'ENABLED',
|
|
397
|
+
version: '1.0.0',
|
|
398
|
+
messages: { errors: [], warnings: [] },
|
|
399
|
+
entitiesIds: [],
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(
|
|
403
|
+
mockRecord
|
|
404
|
+
);
|
|
405
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue([]);
|
|
406
|
+
|
|
407
|
+
const formReq = { query: {} };
|
|
408
|
+
const formRes = { json: jest.fn() };
|
|
409
|
+
const formNext = jest.fn();
|
|
410
|
+
|
|
411
|
+
const step3 = await dispatcher.dispatchHttp({
|
|
412
|
+
integrationClass: SimulatedAsanaIntegration,
|
|
413
|
+
event: 'LOAD_FORM',
|
|
414
|
+
req: formReq,
|
|
415
|
+
res: formRes,
|
|
416
|
+
next: formNext,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(step3.success).toBe(true);
|
|
420
|
+
expect(step3.integrationId).toBe('new-integration-123');
|
|
421
|
+
|
|
422
|
+
// Verify the progression from stateless to stateful
|
|
423
|
+
expect(
|
|
424
|
+
mockIntegrationRepository.findIntegrationByName
|
|
425
|
+
).toHaveBeenCalledTimes(3);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
@@ -4,7 +4,7 @@ const { IntegrationRepository } = require('../integrations/integration-repositor
|
|
|
4
4
|
const { ModuleFactory } = require('../modules/module-factory');
|
|
5
5
|
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
6
6
|
const { ModuleRepository } = require('../modules/module-repository');
|
|
7
|
-
const {
|
|
7
|
+
const { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
|
|
8
8
|
|
|
9
9
|
const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
10
10
|
|
|
@@ -14,6 +14,14 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
|
14
14
|
moduleRepository,
|
|
15
15
|
moduleDefinitions: getModulesDefinitionFromIntegrationClasses([IntegrationClass]),
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
// Create the event dispatcher
|
|
19
|
+
const dispatcher = new IntegrationEventDispatcher({
|
|
20
|
+
integrationRepository,
|
|
21
|
+
moduleFactory,
|
|
22
|
+
moduleRepository,
|
|
23
|
+
});
|
|
24
|
+
|
|
17
25
|
const router = Router();
|
|
18
26
|
const { path, method, event } = routerObject;
|
|
19
27
|
|
|
@@ -23,13 +31,13 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
|
23
31
|
|
|
24
32
|
router[method.toLowerCase()](path, async (req, res, next) => {
|
|
25
33
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const result = await dispatcher.dispatchHttp({
|
|
35
|
+
integrationClass: IntegrationClass,
|
|
36
|
+
event,
|
|
37
|
+
req,
|
|
38
|
+
res,
|
|
39
|
+
next
|
|
30
40
|
});
|
|
31
|
-
const integration = await getIntegrationInstanceByDefinition.execute(IntegrationClass);
|
|
32
|
-
const result = await integration.send(event, { req, res, next });
|
|
33
41
|
res.json(result);
|
|
34
42
|
} catch (error) {
|
|
35
43
|
next(error);
|
|
@@ -50,19 +58,19 @@ const createQueueWorker = (integrationClass) => {
|
|
|
50
58
|
moduleDefinitions: getModulesDefinitionFromIntegrationClasses([integrationClass]),
|
|
51
59
|
});
|
|
52
60
|
|
|
61
|
+
dispatcher = new IntegrationEventDispatcher({
|
|
62
|
+
integrationRepository: this.integrationRepository,
|
|
63
|
+
moduleFactory: this.moduleFactory,
|
|
64
|
+
moduleRepository: this.moduleRepository,
|
|
65
|
+
});
|
|
66
|
+
|
|
53
67
|
async _run(params, context) {
|
|
54
68
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
moduleRepository: this.moduleRepository,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const integration = await getIntegrationInstanceByDefinition.execute(integrationClass);
|
|
62
|
-
|
|
63
|
-
const res = await integration.send(params.event, {
|
|
69
|
+
const res = await this.dispatcher.dispatchJob({
|
|
70
|
+
integrationClass: integrationClass,
|
|
71
|
+
event: params.event,
|
|
64
72
|
data: params.data,
|
|
65
|
-
context,
|
|
73
|
+
context: context,
|
|
66
74
|
});
|
|
67
75
|
return res;
|
|
68
76
|
} catch (error) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
|
|
2
|
+
class IntegrationEventDispatcher {
|
|
3
|
+
constructor({ integrationRepository, moduleFactory, moduleRepository }) {
|
|
4
|
+
this.integrationRepository = integrationRepository;
|
|
5
|
+
this.moduleFactory = moduleFactory;
|
|
6
|
+
this.moduleRepository = moduleRepository;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async dispatchHttp({ integrationClass, event, req, res, next }) {
|
|
10
|
+
// Create or get integration instance
|
|
11
|
+
const integration = await this.resolveIntegration(integrationClass);
|
|
12
|
+
|
|
13
|
+
// Get the handler method from the integration's events object
|
|
14
|
+
// The integration defines this.events = { EVENT_NAME: { handler: this.method.bind(this) } }
|
|
15
|
+
const handler = this.findEventHandler(integration, event);
|
|
16
|
+
|
|
17
|
+
if (!handler) {
|
|
18
|
+
throw new Error(`Event ${event} not registered for ${integrationClass.Definition.name}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Call the handler directly
|
|
22
|
+
return await handler.call(integration, { req, res, next });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async dispatchJob({ integrationClass, event, data, context }) {
|
|
26
|
+
const integration = await this.resolveIntegration(integrationClass);
|
|
27
|
+
|
|
28
|
+
const handler = this.findEventHandler(integration, event);
|
|
29
|
+
|
|
30
|
+
if (!handler) {
|
|
31
|
+
throw new Error(`Event ${event} not registered for ${integrationClass.Definition.name}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return await handler.call(integration, { data, context });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
findEventHandler(integration, event) {
|
|
38
|
+
// First check if integration has events object
|
|
39
|
+
if (integration.events && integration.events[event]) {
|
|
40
|
+
return integration.events[event].handler;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check default events (ON_CREATE, ON_UPDATE, etc.)
|
|
44
|
+
if (integration.defaultEvents && integration.defaultEvents[event]) {
|
|
45
|
+
return integration.defaultEvents[event].handler;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async resolveIntegration(integrationClass) {
|
|
52
|
+
let integrationInstance;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Try to get persisted integration from database
|
|
56
|
+
const integrationRecord = await this.integrationRepository.findIntegrationByName(
|
|
57
|
+
integrationClass.Definition.name
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (integrationRecord) {
|
|
61
|
+
// Load with full database state and modules
|
|
62
|
+
const entities = await this.moduleRepository.findEntitiesByIds(integrationRecord.entitiesIds);
|
|
63
|
+
const modules = [];
|
|
64
|
+
|
|
65
|
+
for (const entity of entities) {
|
|
66
|
+
const moduleInstance = await this.moduleFactory.getModuleInstance(
|
|
67
|
+
entity.id,
|
|
68
|
+
integrationRecord.userId
|
|
69
|
+
);
|
|
70
|
+
modules.push(moduleInstance);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
integrationInstance = new integrationClass({
|
|
74
|
+
id: integrationRecord.id,
|
|
75
|
+
userId: integrationRecord.userId,
|
|
76
|
+
entities: entities,
|
|
77
|
+
config: integrationRecord.config,
|
|
78
|
+
status: integrationRecord.status,
|
|
79
|
+
version: integrationRecord.version,
|
|
80
|
+
messages: integrationRecord.messages,
|
|
81
|
+
modules: modules
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
// No DB record - create stateless instance for auth routes
|
|
85
|
+
integrationInstance = new integrationClass({});
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// If DB lookup fails, create stateless instance
|
|
89
|
+
integrationInstance = new integrationClass({});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initialize the integration (loads dynamic events if any)
|
|
93
|
+
if (typeof integrationInstance.initialize === 'function') {
|
|
94
|
+
await integrationInstance.initialize();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return integrationInstance;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { IntegrationEventDispatcher };
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
const { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
|
|
2
|
+
const { IntegrationBase } = require('../integrations/integration-base');
|
|
3
|
+
|
|
4
|
+
// Mock dependencies
|
|
5
|
+
const mockIntegrationRepository = {
|
|
6
|
+
findIntegrationByName: jest.fn()
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const mockModuleRepository = {
|
|
10
|
+
findEntitiesByIds: jest.fn()
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const mockModuleFactory = {
|
|
14
|
+
getModuleInstance: jest.fn()
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Create a test integration class
|
|
18
|
+
class TestIntegration extends IntegrationBase {
|
|
19
|
+
static Definition = {
|
|
20
|
+
name: 'test-integration',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
modules: {},
|
|
23
|
+
routes: [
|
|
24
|
+
{
|
|
25
|
+
path: '/auth',
|
|
26
|
+
method: 'GET',
|
|
27
|
+
event: 'AUTH_REQUEST'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: '/data',
|
|
31
|
+
method: 'GET',
|
|
32
|
+
event: 'LOAD_DATA'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
path: '/test',
|
|
36
|
+
method: 'POST',
|
|
37
|
+
event: 'TEST_EVENT'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
path: '/dynamic',
|
|
41
|
+
method: 'GET',
|
|
42
|
+
event: 'DYNAMIC_EVENT'
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
constructor(params) {
|
|
48
|
+
super(params);
|
|
49
|
+
// Define events in constructor like AsanaIntegration does
|
|
50
|
+
this.events = {
|
|
51
|
+
'AUTH_REQUEST': {
|
|
52
|
+
handler: this.authRequest.bind(this)
|
|
53
|
+
},
|
|
54
|
+
'LOAD_DATA': {
|
|
55
|
+
handler: this.loadData.bind(this)
|
|
56
|
+
},
|
|
57
|
+
'TEST_EVENT': {
|
|
58
|
+
handler: this.testHandler.bind(this)
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async authRequest({ req, res }) {
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
message: 'Auth request handled',
|
|
67
|
+
hasDatabase: !!this.id
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async loadData({ req, res }) {
|
|
72
|
+
if (!this.id) {
|
|
73
|
+
throw new Error('Database record required');
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
data: this.config,
|
|
78
|
+
integrationId: this.id
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async testHandler({ data }) {
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
received: data
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async initialize() {
|
|
90
|
+
// Simulate loading dynamic events
|
|
91
|
+
const additionalEvents = {
|
|
92
|
+
'DYNAMIC_EVENT': {
|
|
93
|
+
handler: this.dynamicHandler.bind(this)
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.events = { ...this.events, ...additionalEvents };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async dynamicHandler({ req }) {
|
|
100
|
+
return { dynamic: true };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('IntegrationEventDispatcher', () => {
|
|
105
|
+
let dispatcher;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
// Reset all mocks
|
|
109
|
+
jest.clearAllMocks();
|
|
110
|
+
|
|
111
|
+
// Create dispatcher instance
|
|
112
|
+
dispatcher = new IntegrationEventDispatcher({
|
|
113
|
+
integrationRepository: mockIntegrationRepository,
|
|
114
|
+
moduleFactory: mockModuleFactory,
|
|
115
|
+
moduleRepository: mockModuleRepository
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('dispatchHttp', () => {
|
|
120
|
+
it('should handle auth request without database record (stateless)', async () => {
|
|
121
|
+
// Mock no database record found
|
|
122
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
123
|
+
|
|
124
|
+
const req = { headers: {} };
|
|
125
|
+
const res = { json: jest.fn() };
|
|
126
|
+
const next = jest.fn();
|
|
127
|
+
|
|
128
|
+
const result = await dispatcher.dispatchHttp({
|
|
129
|
+
integrationClass: TestIntegration,
|
|
130
|
+
event: 'AUTH_REQUEST',
|
|
131
|
+
req,
|
|
132
|
+
res,
|
|
133
|
+
next
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual({
|
|
137
|
+
success: true,
|
|
138
|
+
message: 'Auth request handled',
|
|
139
|
+
hasDatabase: false
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Verify repository was called
|
|
143
|
+
expect(mockIntegrationRepository.findIntegrationByName).toHaveBeenCalledWith('test-integration');
|
|
144
|
+
|
|
145
|
+
// Verify modules were not loaded (stateless)
|
|
146
|
+
expect(mockModuleRepository.findEntitiesByIds).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockModuleFactory.getModuleInstance).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle request with database record (stateful)', async () => {
|
|
151
|
+
// Mock database record
|
|
152
|
+
const mockRecord = {
|
|
153
|
+
id: 'integration-123',
|
|
154
|
+
userId: 'user-456',
|
|
155
|
+
config: { type: 'test', apiKey: 'secret' },
|
|
156
|
+
status: 'ENABLED',
|
|
157
|
+
version: '1.0.0',
|
|
158
|
+
messages: { errors: [], warnings: [] },
|
|
159
|
+
entitiesIds: ['entity-1', 'entity-2']
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const mockEntities = [
|
|
163
|
+
{ _id: 'entity-1', name: 'module1' },
|
|
164
|
+
{ _id: 'entity-2', name: 'module2' }
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const mockModule1 = { getName: () => 'module1', api: {} };
|
|
168
|
+
const mockModule2 = { getName: () => 'module2', api: {} };
|
|
169
|
+
|
|
170
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(mockRecord);
|
|
171
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue(mockEntities);
|
|
172
|
+
mockModuleFactory.getModuleInstance
|
|
173
|
+
.mockResolvedValueOnce(mockModule1)
|
|
174
|
+
.mockResolvedValueOnce(mockModule2);
|
|
175
|
+
|
|
176
|
+
const req = { headers: {} };
|
|
177
|
+
const res = { json: jest.fn() };
|
|
178
|
+
const next = jest.fn();
|
|
179
|
+
|
|
180
|
+
const result = await dispatcher.dispatchHttp({
|
|
181
|
+
integrationClass: TestIntegration,
|
|
182
|
+
event: 'LOAD_DATA',
|
|
183
|
+
req,
|
|
184
|
+
res,
|
|
185
|
+
next
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result).toEqual({
|
|
189
|
+
success: true,
|
|
190
|
+
data: mockRecord.config,
|
|
191
|
+
integrationId: 'integration-123'
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Verify all dependencies were called
|
|
195
|
+
expect(mockIntegrationRepository.findIntegrationByName).toHaveBeenCalledWith('test-integration');
|
|
196
|
+
expect(mockModuleRepository.findEntitiesByIds).toHaveBeenCalledWith(['entity-1', 'entity-2']);
|
|
197
|
+
expect(mockModuleFactory.getModuleInstance).toHaveBeenCalledTimes(2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should call initialize and load dynamic events', async () => {
|
|
201
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
202
|
+
|
|
203
|
+
const req = { headers: {} };
|
|
204
|
+
const res = { json: jest.fn() };
|
|
205
|
+
const next = jest.fn();
|
|
206
|
+
|
|
207
|
+
const result = await dispatcher.dispatchHttp({
|
|
208
|
+
integrationClass: TestIntegration,
|
|
209
|
+
event: 'DYNAMIC_EVENT',
|
|
210
|
+
req,
|
|
211
|
+
res,
|
|
212
|
+
next
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result).toEqual({ dynamic: true });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should throw error if event is not registered', async () => {
|
|
219
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
220
|
+
|
|
221
|
+
const req = { headers: {} };
|
|
222
|
+
const res = { json: jest.fn() };
|
|
223
|
+
const next = jest.fn();
|
|
224
|
+
|
|
225
|
+
await expect(dispatcher.dispatchHttp({
|
|
226
|
+
integrationClass: TestIntegration,
|
|
227
|
+
event: 'UNKNOWN_EVENT',
|
|
228
|
+
req,
|
|
229
|
+
res,
|
|
230
|
+
next
|
|
231
|
+
})).rejects.toThrow('Event UNKNOWN_EVENT not registered for test-integration');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should handle database lookup errors gracefully', async () => {
|
|
235
|
+
// Mock database error
|
|
236
|
+
mockIntegrationRepository.findIntegrationByName.mockRejectedValue(
|
|
237
|
+
new Error('Database connection failed')
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const req = { headers: {} };
|
|
241
|
+
const res = { json: jest.fn() };
|
|
242
|
+
const next = jest.fn();
|
|
243
|
+
|
|
244
|
+
// Should still work with stateless instance
|
|
245
|
+
const result = await dispatcher.dispatchHttp({
|
|
246
|
+
integrationClass: TestIntegration,
|
|
247
|
+
event: 'AUTH_REQUEST',
|
|
248
|
+
req,
|
|
249
|
+
res,
|
|
250
|
+
next
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result).toEqual({
|
|
254
|
+
success: true,
|
|
255
|
+
message: 'Auth request handled',
|
|
256
|
+
hasDatabase: false
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('dispatchJob', () => {
|
|
262
|
+
it('should handle job with integration context', async () => {
|
|
263
|
+
const mockRecord = {
|
|
264
|
+
id: 'integration-123',
|
|
265
|
+
userId: 'user-456',
|
|
266
|
+
config: { type: 'test' },
|
|
267
|
+
status: 'ENABLED',
|
|
268
|
+
version: '1.0.0',
|
|
269
|
+
messages: { errors: [], warnings: [] },
|
|
270
|
+
entitiesIds: []
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(mockRecord);
|
|
274
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue([]);
|
|
275
|
+
|
|
276
|
+
const data = { testData: 'value' };
|
|
277
|
+
const context = { integrationId: 'integration-123' };
|
|
278
|
+
|
|
279
|
+
const result = await dispatcher.dispatchJob({
|
|
280
|
+
integrationClass: TestIntegration,
|
|
281
|
+
event: 'TEST_EVENT',
|
|
282
|
+
data,
|
|
283
|
+
context
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result).toEqual({
|
|
287
|
+
success: true,
|
|
288
|
+
received: data
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should handle job without integration context', async () => {
|
|
293
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
294
|
+
|
|
295
|
+
const data = { testData: 'value' };
|
|
296
|
+
const context = {};
|
|
297
|
+
|
|
298
|
+
const result = await dispatcher.dispatchJob({
|
|
299
|
+
integrationClass: TestIntegration,
|
|
300
|
+
event: 'TEST_EVENT',
|
|
301
|
+
data,
|
|
302
|
+
context
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(result).toEqual({
|
|
306
|
+
success: true,
|
|
307
|
+
received: data
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should throw error for unknown job event', async () => {
|
|
312
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
313
|
+
|
|
314
|
+
const data = {};
|
|
315
|
+
const context = {};
|
|
316
|
+
|
|
317
|
+
await expect(dispatcher.dispatchJob({
|
|
318
|
+
integrationClass: TestIntegration,
|
|
319
|
+
event: 'UNKNOWN_JOB',
|
|
320
|
+
data,
|
|
321
|
+
context
|
|
322
|
+
})).rejects.toThrow('Event UNKNOWN_JOB not registered for test-integration');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('findEventHandler', () => {
|
|
327
|
+
it('should find handler in events object', async () => {
|
|
328
|
+
const integration = new TestIntegration({});
|
|
329
|
+
await integration.initialize();
|
|
330
|
+
|
|
331
|
+
const handler = dispatcher.findEventHandler(integration, 'AUTH_REQUEST');
|
|
332
|
+
expect(handler).toBeDefined();
|
|
333
|
+
expect(typeof handler).toBe('function');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should find handler in defaultEvents', async () => {
|
|
337
|
+
const integration = new TestIntegration({});
|
|
338
|
+
|
|
339
|
+
const handler = dispatcher.findEventHandler(integration, 'ON_CREATE');
|
|
340
|
+
expect(handler).toBeDefined();
|
|
341
|
+
expect(typeof handler).toBe('function');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should return null for non-existent event', async () => {
|
|
345
|
+
const integration = new TestIntegration({});
|
|
346
|
+
|
|
347
|
+
const handler = dispatcher.findEventHandler(integration, 'NON_EXISTENT');
|
|
348
|
+
expect(handler).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('resolveIntegration', () => {
|
|
353
|
+
it('should create stateless instance when no database record exists', async () => {
|
|
354
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(null);
|
|
355
|
+
|
|
356
|
+
const integration = await dispatcher.resolveIntegration(TestIntegration);
|
|
357
|
+
|
|
358
|
+
expect(integration).toBeInstanceOf(TestIntegration);
|
|
359
|
+
expect(integration.id).toBeUndefined();
|
|
360
|
+
expect(integration.config).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should create stateful instance with all properties', async () => {
|
|
364
|
+
const mockRecord = {
|
|
365
|
+
id: 'integration-123',
|
|
366
|
+
userId: 'user-456',
|
|
367
|
+
config: { apiKey: 'secret' },
|
|
368
|
+
status: 'ENABLED',
|
|
369
|
+
version: '1.0.0',
|
|
370
|
+
messages: { errors: [], warnings: [] },
|
|
371
|
+
entitiesIds: ['entity-1']
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const mockEntities = [{ _id: 'entity-1', name: 'module1' }];
|
|
375
|
+
const mockModule = {
|
|
376
|
+
getName: () => 'testModule',
|
|
377
|
+
name: 'testModule',
|
|
378
|
+
api: {}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(mockRecord);
|
|
382
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue(mockEntities);
|
|
383
|
+
mockModuleFactory.getModuleInstance.mockResolvedValue(mockModule);
|
|
384
|
+
|
|
385
|
+
const integration = await dispatcher.resolveIntegration(TestIntegration);
|
|
386
|
+
|
|
387
|
+
expect(integration).toBeInstanceOf(TestIntegration);
|
|
388
|
+
expect(integration.id).toBe('integration-123');
|
|
389
|
+
expect(integration.userId).toBe('user-456');
|
|
390
|
+
expect(integration.config).toEqual({ apiKey: 'secret' });
|
|
391
|
+
expect(integration.status).toBe('ENABLED');
|
|
392
|
+
expect(integration.testModule).toBe(mockModule);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should fall back to stateless when module loading fails', async () => {
|
|
396
|
+
const mockRecord = {
|
|
397
|
+
id: 'integration-123',
|
|
398
|
+
userId: 'user-456',
|
|
399
|
+
config: {},
|
|
400
|
+
status: 'ENABLED',
|
|
401
|
+
version: '1.0.0',
|
|
402
|
+
messages: { errors: [], warnings: [] },
|
|
403
|
+
entitiesIds: ['entity-1']
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(mockRecord);
|
|
407
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue([{ _id: 'entity-1' }]);
|
|
408
|
+
mockModuleFactory.getModuleInstance.mockRejectedValue(new Error('Module load failed'));
|
|
409
|
+
|
|
410
|
+
// Should fall back to stateless instance
|
|
411
|
+
const integration = await dispatcher.resolveIntegration(TestIntegration);
|
|
412
|
+
|
|
413
|
+
expect(integration).toBeInstanceOf(TestIntegration);
|
|
414
|
+
// Should be stateless (no id, config, etc.)
|
|
415
|
+
expect(integration.id).toBeUndefined();
|
|
416
|
+
expect(integration.config).toBeUndefined();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Test for default events from IntegrationBase
|
|
422
|
+
describe('IntegrationEventDispatcher with default events', () => {
|
|
423
|
+
let dispatcher;
|
|
424
|
+
|
|
425
|
+
beforeEach(() => {
|
|
426
|
+
jest.clearAllMocks();
|
|
427
|
+
dispatcher = new IntegrationEventDispatcher({
|
|
428
|
+
integrationRepository: mockIntegrationRepository,
|
|
429
|
+
moduleFactory: mockModuleFactory,
|
|
430
|
+
moduleRepository: mockModuleRepository
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should handle default ON_CREATE event', async () => {
|
|
435
|
+
const mockRecord = {
|
|
436
|
+
id: 'integration-123',
|
|
437
|
+
userId: 'user-456',
|
|
438
|
+
config: {},
|
|
439
|
+
status: 'ENABLED',
|
|
440
|
+
version: '1.0.0',
|
|
441
|
+
messages: { errors: [], warnings: [] },
|
|
442
|
+
entitiesIds: []
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
mockIntegrationRepository.findIntegrationByName.mockResolvedValue(mockRecord);
|
|
446
|
+
mockModuleRepository.findEntitiesByIds.mockResolvedValue([]);
|
|
447
|
+
|
|
448
|
+
// Mock the updateIntegrationStatus to avoid actual DB calls
|
|
449
|
+
const integration = new TestIntegration({
|
|
450
|
+
id: mockRecord.id,
|
|
451
|
+
userId: mockRecord.userId,
|
|
452
|
+
config: mockRecord.config,
|
|
453
|
+
status: mockRecord.status
|
|
454
|
+
});
|
|
455
|
+
integration.updateIntegrationStatus = {
|
|
456
|
+
execute: jest.fn().mockResolvedValue(true)
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const req = {};
|
|
460
|
+
const res = {};
|
|
461
|
+
const next = jest.fn();
|
|
462
|
+
|
|
463
|
+
// The dispatcher should be able to find and call ON_CREATE
|
|
464
|
+
const handler = dispatcher.findEventHandler(integration, 'ON_CREATE');
|
|
465
|
+
expect(handler).toBeDefined();
|
|
466
|
+
|
|
467
|
+
// Call the handler
|
|
468
|
+
const result = await handler.call(integration, { integrationId: 'integration-123' });
|
|
469
|
+
|
|
470
|
+
// ON_CREATE should update status to ENABLED
|
|
471
|
+
expect(integration.updateIntegrationStatus.execute).toHaveBeenCalledWith('integration-123', 'ENABLED');
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -62,12 +62,7 @@ class IntegrationBase {
|
|
|
62
62
|
return this.Definition.version;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
registerEventHandlers()
|
|
66
|
-
this.on = {
|
|
67
|
-
...this.defaultEvents,
|
|
68
|
-
...this.events,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
65
|
+
// REMOVED: registerEventHandlers() - Event handling is now done by IntegrationEventDispatcher
|
|
71
66
|
|
|
72
67
|
constructor(params = {}) {
|
|
73
68
|
// Data from database record (when instantiated by use cases)
|
|
@@ -130,14 +125,7 @@ class IntegrationBase {
|
|
|
130
125
|
};
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
|
|
134
|
-
if (!this.on[event]) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Event ${event} is not defined in the Integration event object`
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
return this.on[event].handler.call(this, object);
|
|
140
|
-
}
|
|
128
|
+
// REMOVED: send() - Event dispatching is now done by IntegrationEventDispatcher
|
|
141
129
|
|
|
142
130
|
async validateConfig() {
|
|
143
131
|
const configOptions = await this.getConfigOptions();
|
|
@@ -336,8 +324,7 @@ class IntegrationBase {
|
|
|
336
324
|
this.addError(e);
|
|
337
325
|
}
|
|
338
326
|
|
|
339
|
-
//
|
|
340
|
-
await this.registerEventHandlers();
|
|
327
|
+
// Event handlers are no longer registered here - handled by IntegrationEventDispatcher
|
|
341
328
|
}
|
|
342
329
|
|
|
343
330
|
getOptionDetails() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.395.
|
|
4
|
+
"version": "2.0.0--canary.395.704a466.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"aws-sdk": "^2.1200.0",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"uuid": "^9.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@friggframework/eslint-config": "2.0.0--canary.395.
|
|
26
|
-
"@friggframework/prettier-config": "2.0.0--canary.395.
|
|
27
|
-
"@friggframework/test": "2.0.0--canary.395.
|
|
25
|
+
"@friggframework/eslint-config": "2.0.0--canary.395.704a466.0",
|
|
26
|
+
"@friggframework/prettier-config": "2.0.0--canary.395.704a466.0",
|
|
27
|
+
"@friggframework/test": "2.0.0--canary.395.704a466.0",
|
|
28
28
|
"@types/lodash": "4.17.15",
|
|
29
29
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
30
30
|
"chai": "^4.3.6",
|
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "704a466eb0e5b9fd4db245f95071f85842c698de"
|
|
60
60
|
}
|