@friggframework/core 2.0.0--canary.395.a6dcc5a.0 → 2.0.0--canary.395.de6bcdf.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/auth-flow.integration.test.js +140 -0
- package/handlers/backend-utils.js +19 -37
- package/handlers/integration-event-dispatcher.js +55 -0
- package/handlers/integration-event-dispatcher.test.js +134 -0
- package/index.js +10 -0
- package/integrations/index.js +6 -0
- package/integrations/integration-base.js +94 -35
- package/integrations/use-cases/load-integration-context-full.test.js +314 -0
- package/integrations/use-cases/load-integration-context.js +77 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/modules/index.js +4 -0
- package/modules/module-factory.js +1 -1
- package/modules/module-hydration.test.js +188 -0
- package/modules/module-repository.js +19 -0
- package/package.json +5 -5
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
|
|
2
|
+
const { IntegrationBase } = require('../integrations/integration-base');
|
|
3
|
+
|
|
4
|
+
class SimulatedAsanaIntegration extends IntegrationBase {
|
|
5
|
+
static Definition = {
|
|
6
|
+
name: 'asana',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
modules: {},
|
|
9
|
+
routes: [
|
|
10
|
+
{ path: '/auth', method: 'GET', event: 'AUTH_REQUEST' },
|
|
11
|
+
{ path: '/auth/redirect/:provider', method: 'GET', event: 'AUTH_REDIRECT' },
|
|
12
|
+
{ path: '/form', method: 'GET', event: 'LOAD_FORM' },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
constructor(params = {}) {
|
|
17
|
+
super(params);
|
|
18
|
+
this.events = {
|
|
19
|
+
AUTH_REQUEST: { handler: this.authRequest.bind(this) },
|
|
20
|
+
AUTH_REDIRECT: { handler: this.authRedirect.bind(this) },
|
|
21
|
+
LOAD_FORM: { handler: this.loadForm.bind(this) },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async authRequest() {
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
action: 'redirect',
|
|
29
|
+
hydrated: this.isHydrated,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async authRedirect({ req }) {
|
|
34
|
+
const { code } = req.query || {};
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
action: 'tokens_received',
|
|
38
|
+
receivedCode: code,
|
|
39
|
+
hydrated: this.isHydrated,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async loadForm() {
|
|
44
|
+
if (!this.isHydrated && SimulatedAsanaIntegration.testRecord) {
|
|
45
|
+
this.setIntegrationRecord({
|
|
46
|
+
record: SimulatedAsanaIntegration.testRecord.record,
|
|
47
|
+
modules: SimulatedAsanaIntegration.testRecord.modules,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.assertHydrated('Integration not found - must authenticate first');
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
form: {
|
|
56
|
+
fields: ['field1', 'field2'],
|
|
57
|
+
},
|
|
58
|
+
integrationId: this.id,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('IntegrationEventDispatcher auth flow', () => {
|
|
64
|
+
const createDispatcher = () =>
|
|
65
|
+
new IntegrationEventDispatcher(new SimulatedAsanaIntegration());
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
SimulatedAsanaIntegration.testRecord = null;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles auth request without hydration', async () => {
|
|
72
|
+
const dispatcher = createDispatcher();
|
|
73
|
+
const result = await dispatcher.dispatchHttp({
|
|
74
|
+
event: 'AUTH_REQUEST',
|
|
75
|
+
req: { params: { provider: 'asana' }, query: {} },
|
|
76
|
+
res: {},
|
|
77
|
+
next: jest.fn(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result).toEqual({ success: true, action: 'redirect', hydrated: false });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('handles auth redirect without hydration', async () => {
|
|
84
|
+
const dispatcher = createDispatcher();
|
|
85
|
+
const result = await dispatcher.dispatchHttp({
|
|
86
|
+
event: 'AUTH_REDIRECT',
|
|
87
|
+
req: { params: { provider: 'asana' }, query: { code: 'abc123' } },
|
|
88
|
+
res: {},
|
|
89
|
+
next: jest.fn(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
success: true,
|
|
94
|
+
action: 'tokens_received',
|
|
95
|
+
receivedCode: 'abc123',
|
|
96
|
+
hydrated: false,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('throws for protected routes when no record is loaded', async () => {
|
|
101
|
+
const dispatcher = createDispatcher();
|
|
102
|
+
await expect(
|
|
103
|
+
dispatcher.dispatchHttp({
|
|
104
|
+
event: 'LOAD_FORM',
|
|
105
|
+
req: { query: {} },
|
|
106
|
+
res: {},
|
|
107
|
+
next: jest.fn(),
|
|
108
|
+
})
|
|
109
|
+
).rejects.toThrow('Integration not found - must authenticate first');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('allows handlers to hydrate explicitly before continuing', async () => {
|
|
113
|
+
SimulatedAsanaIntegration.testRecord = {
|
|
114
|
+
record: {
|
|
115
|
+
id: 'integration-123',
|
|
116
|
+
userId: 'user-456',
|
|
117
|
+
config: { type: 'asana' },
|
|
118
|
+
status: 'ENABLED',
|
|
119
|
+
version: '1.0.0',
|
|
120
|
+
messages: { errors: [], warnings: [] },
|
|
121
|
+
entities: [],
|
|
122
|
+
},
|
|
123
|
+
modules: [],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const dispatcher = createDispatcher();
|
|
127
|
+
const result = await dispatcher.dispatchHttp({
|
|
128
|
+
event: 'LOAD_FORM',
|
|
129
|
+
req: { query: {} },
|
|
130
|
+
res: {},
|
|
131
|
+
next: jest.fn(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual({
|
|
135
|
+
success: true,
|
|
136
|
+
form: { fields: ['field1', 'field2'] },
|
|
137
|
+
integrationId: 'integration-123',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
const { Router } = require('express');
|
|
2
2
|
const { Worker } = require('@friggframework/core');
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const { ModuleRepository } = require('../modules/module-repository');
|
|
7
|
-
const { GetIntegrationInstanceByDefinition } = require('../integrations/use-cases/get-integration-instance-by-definition');
|
|
3
|
+
const {
|
|
4
|
+
IntegrationEventDispatcher,
|
|
5
|
+
} = require('./integration-event-dispatcher');
|
|
8
6
|
|
|
9
7
|
const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
10
|
-
|
|
11
|
-
const integrationRepository = new IntegrationRepository();
|
|
12
|
-
const moduleRepository = new ModuleRepository();
|
|
13
|
-
const moduleFactory = new ModuleFactory({
|
|
14
|
-
moduleRepository,
|
|
15
|
-
moduleDefinitions: getModulesDefinitionFromIntegrationClasses([IntegrationClass]),
|
|
16
|
-
});
|
|
17
8
|
const router = Router();
|
|
18
9
|
const { path, method, event } = routerObject;
|
|
19
10
|
|
|
@@ -23,13 +14,16 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
|
23
14
|
|
|
24
15
|
router[method.toLowerCase()](path, async (req, res, next) => {
|
|
25
16
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
const integrationInstance = new IntegrationClass();
|
|
18
|
+
const dispatcher = new IntegrationEventDispatcher(
|
|
19
|
+
integrationInstance
|
|
20
|
+
);
|
|
21
|
+
const result = await dispatcher.dispatchHttp({
|
|
22
|
+
event,
|
|
23
|
+
req,
|
|
24
|
+
res,
|
|
25
|
+
next,
|
|
30
26
|
});
|
|
31
|
-
const integration = await getIntegrationInstanceByDefinition.execute(IntegrationClass);
|
|
32
|
-
const result = await integration.send(event, { req, res, next });
|
|
33
27
|
res.json(result);
|
|
34
28
|
} catch (error) {
|
|
35
29
|
next(error);
|
|
@@ -39,30 +33,18 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
|
39
33
|
return router;
|
|
40
34
|
};
|
|
41
35
|
|
|
42
|
-
//todo: this should be in a use case class
|
|
43
36
|
const createQueueWorker = (integrationClass) => {
|
|
44
37
|
class QueueWorker extends Worker {
|
|
45
|
-
|
|
46
|
-
integrationRepository = new IntegrationRepository();
|
|
47
|
-
moduleRepository = new ModuleRepository();
|
|
48
|
-
moduleFactory = new ModuleFactory({
|
|
49
|
-
moduleRepository: this.moduleRepository,
|
|
50
|
-
moduleDefinitions: getModulesDefinitionFromIntegrationClasses([integrationClass]),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
38
|
async _run(params, context) {
|
|
54
39
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const integration = await getIntegrationInstanceByDefinition.execute(integrationClass);
|
|
62
|
-
|
|
63
|
-
const res = await integration.send(params.event, {
|
|
40
|
+
const integrationInstance = new integrationClass();
|
|
41
|
+
const dispatcher = new IntegrationEventDispatcher(
|
|
42
|
+
integrationInstance
|
|
43
|
+
);
|
|
44
|
+
const res = await dispatcher.dispatchJob({
|
|
45
|
+
event: params.event,
|
|
64
46
|
data: params.data,
|
|
65
|
-
context,
|
|
47
|
+
context: context,
|
|
66
48
|
});
|
|
67
49
|
return res;
|
|
68
50
|
} catch (error) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight dispatcher that executes integration event handlers.
|
|
4
|
+
* Signature: `new IntegrationEventDispatcher(integrationInstance)`
|
|
5
|
+
* @param {import('../integrations/integration-base')} integrationInstance Pre-instantiated integration.
|
|
6
|
+
*/
|
|
7
|
+
class IntegrationEventDispatcher {
|
|
8
|
+
constructor(integrationInstance) {
|
|
9
|
+
if (!integrationInstance) {
|
|
10
|
+
throw new Error('Integration instance is required');
|
|
11
|
+
}
|
|
12
|
+
this.integrationInstance = integrationInstance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async dispatchHttp({ event, req, res, next }) {
|
|
16
|
+
const instance = this.integrationInstance;
|
|
17
|
+
|
|
18
|
+
const handler = this.findEventHandler(instance, event);
|
|
19
|
+
|
|
20
|
+
if (!handler) {
|
|
21
|
+
const name = instance.constructor?.Definition?.name || 'integration';
|
|
22
|
+
throw new Error(`Event ${event} not registered for ${name}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return await handler.call(instance, { req, res, next });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async dispatchJob({ event, data, context }) {
|
|
29
|
+
const instance = this.integrationInstance;
|
|
30
|
+
|
|
31
|
+
const handler = this.findEventHandler(instance, event);
|
|
32
|
+
|
|
33
|
+
if (!handler) {
|
|
34
|
+
const name = instance.constructor?.Definition?.name || 'integration';
|
|
35
|
+
throw new Error(`Event ${event} not registered for ${name}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return await handler.call(instance, { data, context });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
findEventHandler(integration, event) {
|
|
42
|
+
if (integration.events && integration.events[event]) {
|
|
43
|
+
return integration.events[event].handler;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (integration.defaultEvents && integration.defaultEvents[event]) {
|
|
47
|
+
return integration.defaultEvents[event].handler;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { IntegrationEventDispatcher };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { IntegrationEventDispatcher } = require('./integration-event-dispatcher');
|
|
2
|
+
const { IntegrationBase } = require('../integrations/integration-base');
|
|
3
|
+
|
|
4
|
+
class TestIntegration extends IntegrationBase {
|
|
5
|
+
static Definition = {
|
|
6
|
+
name: 'test-integration',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
modules: {},
|
|
9
|
+
routes: [
|
|
10
|
+
{ path: '/auth', method: 'GET', event: 'AUTH_REQUEST' },
|
|
11
|
+
{ path: '/data', method: 'GET', event: 'LOAD_DATA' },
|
|
12
|
+
{ path: '/job', method: 'POST', event: 'TEST_EVENT' },
|
|
13
|
+
{ path: '/dynamic', method: 'GET', event: 'DYNAMIC_EVENT' },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
constructor(params) {
|
|
18
|
+
super(params);
|
|
19
|
+
this.events = {
|
|
20
|
+
AUTH_REQUEST: { handler: this.authRequest.bind(this) },
|
|
21
|
+
LOAD_DATA: { handler: this.loadData.bind(this) },
|
|
22
|
+
TEST_EVENT: { handler: this.testHandler.bind(this) },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async authRequest() {
|
|
27
|
+
TestIntegration.latestInstance = this;
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
hydrated: this.isHydrated,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async loadData() {
|
|
35
|
+
this.assertHydrated('loadData requires hydration');
|
|
36
|
+
return { success: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async testHandler({ data }) {
|
|
40
|
+
TestIntegration.latestInstance = this;
|
|
41
|
+
return { received: data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async initialize() {
|
|
45
|
+
this.events = {
|
|
46
|
+
...this.events,
|
|
47
|
+
DYNAMIC_EVENT: { handler: this.dynamicHandler.bind(this) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async dynamicHandler() {
|
|
52
|
+
TestIntegration.latestInstance = this;
|
|
53
|
+
return { dynamic: true };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('IntegrationEventDispatcher', () => {
|
|
58
|
+
const createDispatcher = () =>
|
|
59
|
+
new IntegrationEventDispatcher(new TestIntegration());
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
TestIntegration.latestInstance = null;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('dispatchHttp', () => {
|
|
66
|
+
it('creates a stateless integration instance for HTTP events', async () => {
|
|
67
|
+
const dispatcher = createDispatcher();
|
|
68
|
+
const result = await dispatcher.dispatchHttp({
|
|
69
|
+
event: 'AUTH_REQUEST',
|
|
70
|
+
req: {},
|
|
71
|
+
res: {},
|
|
72
|
+
next: jest.fn(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({ success: true, hydrated: false });
|
|
76
|
+
expect(TestIntegration.latestInstance).toBeInstanceOf(TestIntegration);
|
|
77
|
+
expect(TestIntegration.latestInstance.isHydrated).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('calls initialize to register dynamic events', async () => {
|
|
81
|
+
const dispatcher = createDispatcher();
|
|
82
|
+
await dispatcher.integrationInstance.initialize();
|
|
83
|
+
const result = await dispatcher.dispatchHttp({
|
|
84
|
+
event: 'DYNAMIC_EVENT',
|
|
85
|
+
req: {},
|
|
86
|
+
res: {},
|
|
87
|
+
next: jest.fn(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({ dynamic: true });
|
|
91
|
+
expect(TestIntegration.latestInstance).toBeInstanceOf(TestIntegration);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('throws when requesting an unknown event', async () => {
|
|
95
|
+
const dispatcher = createDispatcher();
|
|
96
|
+
await expect(
|
|
97
|
+
dispatcher.dispatchHttp({
|
|
98
|
+
event: 'UNKNOWN',
|
|
99
|
+
req: {},
|
|
100
|
+
res: {},
|
|
101
|
+
next: jest.fn(),
|
|
102
|
+
})
|
|
103
|
+
).rejects.toThrow('Event UNKNOWN not registered for test-integration');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not hydrate automatically for handlers that require data', async () => {
|
|
107
|
+
const dispatcher = createDispatcher();
|
|
108
|
+
await expect(
|
|
109
|
+
dispatcher.dispatchHttp({
|
|
110
|
+
event: 'LOAD_DATA',
|
|
111
|
+
req: {},
|
|
112
|
+
res: {},
|
|
113
|
+
next: jest.fn(),
|
|
114
|
+
})
|
|
115
|
+
).rejects.toThrow('loadData requires hydration');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('dispatchJob', () => {
|
|
120
|
+
it('creates a stateless integration instance for job events', async () => {
|
|
121
|
+
const payload = { foo: 'bar' };
|
|
122
|
+
const dispatcher = createDispatcher();
|
|
123
|
+
const result = await dispatcher.dispatchJob({
|
|
124
|
+
event: 'TEST_EVENT',
|
|
125
|
+
data: payload,
|
|
126
|
+
context: {},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result).toEqual({ received: payload });
|
|
130
|
+
expect(TestIntegration.latestInstance).toBeInstanceOf(TestIntegration);
|
|
131
|
+
expect(TestIntegration.latestInstance.isHydrated).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
package/index.js
CHANGED
|
@@ -40,6 +40,9 @@ const {
|
|
|
40
40
|
IntegrationMapping,
|
|
41
41
|
createIntegrationRouter,
|
|
42
42
|
checkRequiredParams,
|
|
43
|
+
IntegrationRepository,
|
|
44
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
45
|
+
LoadIntegrationContextUseCase,
|
|
43
46
|
} = require('./integrations/index');
|
|
44
47
|
const { TimeoutCatcher } = require('./lambda/index');
|
|
45
48
|
const { debug, initDebugLog, flushDebugLog } = require('./logs/index');
|
|
@@ -51,6 +54,8 @@ const {
|
|
|
51
54
|
OAuth2Requester,
|
|
52
55
|
Requester,
|
|
53
56
|
ModuleConstants,
|
|
57
|
+
ModuleFactory,
|
|
58
|
+
ModuleRepository,
|
|
54
59
|
} = require('./modules/index');
|
|
55
60
|
const utils = require('./utils');
|
|
56
61
|
|
|
@@ -104,6 +109,9 @@ module.exports = {
|
|
|
104
109
|
IntegrationMapping,
|
|
105
110
|
checkRequiredParams,
|
|
106
111
|
createIntegrationRouter,
|
|
112
|
+
IntegrationRepository,
|
|
113
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
114
|
+
LoadIntegrationContextUseCase,
|
|
107
115
|
|
|
108
116
|
// lambda
|
|
109
117
|
TimeoutCatcher,
|
|
@@ -121,6 +129,8 @@ module.exports = {
|
|
|
121
129
|
OAuth2Requester,
|
|
122
130
|
Requester,
|
|
123
131
|
ModuleConstants,
|
|
132
|
+
ModuleFactory,
|
|
133
|
+
ModuleRepository,
|
|
124
134
|
// queues
|
|
125
135
|
QueuerUtil,
|
|
126
136
|
|
package/integrations/index.js
CHANGED
|
@@ -3,6 +3,9 @@ const { IntegrationModel } = require('./integration-model');
|
|
|
3
3
|
const { Options } = require('./options');
|
|
4
4
|
const { IntegrationMapping } = require('./integration-mapping');
|
|
5
5
|
const { createIntegrationRouter, checkRequiredParams } = require('./integration-router');
|
|
6
|
+
const { IntegrationRepository } = require('./integration-repository');
|
|
7
|
+
const { getModulesDefinitionFromIntegrationClasses } = require('./utils/map-integration-dto');
|
|
8
|
+
const { LoadIntegrationContextUseCase } = require('./use-cases/load-integration-context');
|
|
6
9
|
|
|
7
10
|
module.exports = {
|
|
8
11
|
IntegrationBase,
|
|
@@ -11,4 +14,7 @@ module.exports = {
|
|
|
11
14
|
IntegrationMapping,
|
|
12
15
|
createIntegrationRouter,
|
|
13
16
|
checkRequiredParams,
|
|
17
|
+
IntegrationRepository,
|
|
18
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
19
|
+
LoadIntegrationContextUseCase,
|
|
14
20
|
};
|
|
@@ -62,37 +62,17 @@ 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
|
-
// Data from database record (when instantiated by use cases)
|
|
74
|
-
this.id = params.id;
|
|
75
|
-
this.userId = params.userId || params.integrationId; // fallback for legacy
|
|
76
|
-
this.entities = params.entities;
|
|
77
|
-
this.config = params.config;
|
|
78
|
-
this.status = params.status;
|
|
79
|
-
this.version = params.version;
|
|
80
|
-
this.messages = params.messages || { errors: [], warnings: [] };
|
|
81
|
-
|
|
82
|
-
// Module instances (injected by factory)
|
|
83
68
|
this.modules = {};
|
|
84
|
-
if (params.modules) {
|
|
85
|
-
for (const mod of params.modules) {
|
|
86
|
-
const key = typeof mod.getName === 'function' ? mod.getName() : mod.name;
|
|
87
|
-
if (key) {
|
|
88
|
-
this.modules[key] = mod;
|
|
89
|
-
this[key] = mod; // Direct access (e.g., this.hubspot)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Initialize events object (will be populated by child classes)
|
|
95
69
|
this.events = this.events || {};
|
|
70
|
+
this.messages = { errors: [], warnings: [] };
|
|
71
|
+
this._isHydrated = false;
|
|
72
|
+
|
|
73
|
+
if (params && Object.keys(params).length > 0) {
|
|
74
|
+
this.setIntegrationRecord(params);
|
|
75
|
+
}
|
|
96
76
|
|
|
97
77
|
this.defaultEvents = {
|
|
98
78
|
[constantsToBeMigrated.defaultEvents.ON_CREATE]: {
|
|
@@ -130,13 +110,93 @@ class IntegrationBase {
|
|
|
130
110
|
};
|
|
131
111
|
}
|
|
132
112
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
113
|
+
// REMOVED: send() - Event dispatching is now done by IntegrationEventDispatcher
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Persist the database record and module instances onto this integration instance.
|
|
117
|
+
* Accepts either a plain object containing the persisted fields or an object with
|
|
118
|
+
* a `record` property plus a `modules` collection.
|
|
119
|
+
* @param {Object} payload
|
|
120
|
+
* @param {Object} [payload.record]
|
|
121
|
+
* @param {Array|Object} [payload.modules]
|
|
122
|
+
*/
|
|
123
|
+
setIntegrationRecord(payload = {}) {
|
|
124
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
125
|
+
throw new Error('setIntegrationRecord requires integration data');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const record = payload.record ? payload.record : payload;
|
|
129
|
+
const modulesInput = payload.modules ?? record.modules;
|
|
130
|
+
|
|
131
|
+
if (!record) {
|
|
132
|
+
throw new Error('Integration record not provided');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const {
|
|
136
|
+
id,
|
|
137
|
+
userId,
|
|
138
|
+
entities,
|
|
139
|
+
config,
|
|
140
|
+
status,
|
|
141
|
+
version,
|
|
142
|
+
messages,
|
|
143
|
+
} = record;
|
|
144
|
+
|
|
145
|
+
this.id = id;
|
|
146
|
+
this.userId = userId || record.integrationId;
|
|
147
|
+
this.entities = entities;
|
|
148
|
+
this.config = config;
|
|
149
|
+
this.status = status;
|
|
150
|
+
this.version = version;
|
|
151
|
+
this.messages = messages || { errors: [], warnings: [] };
|
|
152
|
+
|
|
153
|
+
const existingModuleKeys = Object.keys(this.modules || {});
|
|
154
|
+
for (const key of existingModuleKeys) {
|
|
155
|
+
if (Object.prototype.hasOwnProperty.call(this, key) && this[key] === this.modules[key]) {
|
|
156
|
+
delete this[key];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.modules = {};
|
|
161
|
+
|
|
162
|
+
if (modulesInput) {
|
|
163
|
+
const modulesArray = Array.isArray(modulesInput)
|
|
164
|
+
? modulesInput
|
|
165
|
+
: Object.values(modulesInput);
|
|
166
|
+
|
|
167
|
+
for (const mod of modulesArray) {
|
|
168
|
+
if (!mod) continue;
|
|
169
|
+
const key = typeof mod.getName === 'function' ? mod.getName() : mod.name;
|
|
170
|
+
if (key) {
|
|
171
|
+
this.modules[key] = mod;
|
|
172
|
+
this[key] = mod;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.integrationRecord = {
|
|
178
|
+
id: this.id,
|
|
179
|
+
userId: this.userId,
|
|
180
|
+
entities: this.entities,
|
|
181
|
+
config: this.config,
|
|
182
|
+
status: this.status,
|
|
183
|
+
version: this.version,
|
|
184
|
+
messages: this.messages,
|
|
185
|
+
};
|
|
186
|
+
this.record = this.integrationRecord;
|
|
187
|
+
|
|
188
|
+
this._isHydrated = Boolean(this.id);
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get isHydrated() {
|
|
193
|
+
return this._isHydrated;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
assertHydrated(message = 'Integration instance is not hydrated') {
|
|
197
|
+
if (!this.isHydrated) {
|
|
198
|
+
throw new Error(message);
|
|
138
199
|
}
|
|
139
|
-
return this.on[event].handler.call(this, object);
|
|
140
200
|
}
|
|
141
201
|
|
|
142
202
|
async validateConfig() {
|
|
@@ -336,8 +396,7 @@ class IntegrationBase {
|
|
|
336
396
|
this.addError(e);
|
|
337
397
|
}
|
|
338
398
|
|
|
339
|
-
//
|
|
340
|
-
await this.registerEventHandlers();
|
|
399
|
+
// Event handlers are no longer registered here - handled by IntegrationEventDispatcher
|
|
341
400
|
}
|
|
342
401
|
|
|
343
402
|
getOptionDetails() {
|