@friggframework/core 2.0.0-next.44 → 2.0.0-next.45

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.
@@ -3,6 +3,22 @@ const { Worker } = require('@friggframework/core');
3
3
  const {
4
4
  IntegrationEventDispatcher,
5
5
  } = require('./integration-event-dispatcher');
6
+ const {
7
+ GetIntegrationInstance,
8
+ } = require('../integrations/use-cases/get-integration-instance');
9
+ const { ModuleFactory } = require('../modules/module-factory');
10
+ const {
11
+ createProcessRepository,
12
+ } = require('../integrations/repositories/process-repository-factory');
13
+ const {
14
+ createIntegrationRepository,
15
+ } = require('../integrations/repositories/integration-repository-factory');
16
+ const {
17
+ createModuleRepository,
18
+ } = require('../modules/repositories/module-repository-factory');
19
+ const {
20
+ getModulesDefinitionFromIntegrationClasses,
21
+ } = require('../integrations/utils/map-integration-dto');
6
22
 
7
23
  const loadRouterFromObject = (IntegrationClass, routerObject) => {
8
24
  const router = Router();
@@ -33,20 +49,119 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
33
49
  return router;
34
50
  };
35
51
 
52
+ const initializeRepositories = () => {
53
+ const processRepository = createProcessRepository();
54
+ const integrationRepository = createIntegrationRepository();
55
+ const moduleRepository = createModuleRepository();
56
+
57
+ return { processRepository, integrationRepository, moduleRepository };
58
+ };
59
+
60
+ const createModuleFactoryWithDefinitions = (
61
+ moduleRepository,
62
+ integrationClasses
63
+ ) => {
64
+ const moduleDefinitions =
65
+ getModulesDefinitionFromIntegrationClasses(integrationClasses);
66
+
67
+ return new ModuleFactory({
68
+ moduleRepository,
69
+ moduleDefinitions,
70
+ });
71
+ };
72
+
73
+ const loadIntegrationForWebhook = async (integrationId) => {
74
+ const { loadAppDefinition } = require('./app-definition-loader');
75
+ const { integrations: integrationClasses } = loadAppDefinition();
76
+
77
+ const { integrationRepository, moduleRepository } =
78
+ initializeRepositories();
79
+
80
+ const moduleFactory = createModuleFactoryWithDefinitions(
81
+ moduleRepository,
82
+ integrationClasses
83
+ );
84
+
85
+ const getIntegrationInstance = new GetIntegrationInstance({
86
+ integrationRepository,
87
+ integrationClasses,
88
+ moduleFactory,
89
+ });
90
+
91
+ const integrationRecord = await integrationRepository.findIntegrationById(
92
+ integrationId
93
+ );
94
+
95
+ return await getIntegrationInstance.execute(
96
+ integrationId,
97
+ integrationRecord.userId
98
+ );
99
+ };
100
+
101
+ const loadIntegrationForProcess = async (processId, integrationClass) => {
102
+ const { processRepository, integrationRepository, moduleRepository } =
103
+ initializeRepositories();
104
+
105
+ const moduleFactory = createModuleFactoryWithDefinitions(moduleRepository, [
106
+ integrationClass,
107
+ ]);
108
+
109
+ const getIntegrationInstance = new GetIntegrationInstance({
110
+ integrationRepository,
111
+ integrationClasses: [integrationClass],
112
+ moduleFactory,
113
+ });
114
+
115
+ if (!processId) {
116
+ throw new Error('processId is required in queue message data');
117
+ }
118
+
119
+ const process = await processRepository.findById(processId);
120
+
121
+ if (!process) {
122
+ throw new Error(`Process not found: ${processId}`);
123
+ }
124
+
125
+ return await getIntegrationInstance.execute(
126
+ process.integrationId,
127
+ process.userId
128
+ );
129
+ };
130
+
36
131
  const createQueueWorker = (integrationClass) => {
37
132
  class QueueWorker extends Worker {
38
133
  async _run(params, context) {
39
134
  try {
40
- const integrationInstance = new integrationClass();
135
+ let integrationInstance;
136
+ if (
137
+ params.event === 'ON_WEBHOOK' &&
138
+ params.data?.integrationId
139
+ ) {
140
+ integrationInstance = await loadIntegrationForWebhook(
141
+ params.data.integrationId
142
+ );
143
+ } else if (params.data?.processId) {
144
+ integrationInstance = await loadIntegrationForProcess(
145
+ params.data.processId,
146
+ integrationClass
147
+ );
148
+ } else {
149
+ // Instantiates a DRY integration class without database records.
150
+ // There will be cases where we need to use helpers that the api modules can export.
151
+ // Like for HubSpot, the answer is to do a reverse lookup for the integration by the entity external ID (HubSpot Portal ID),
152
+ // and then you'll have the integration ID available to hydrate from.
153
+ integrationInstance = new integrationClass();
154
+ }
155
+
41
156
  const dispatcher = new IntegrationEventDispatcher(
42
157
  integrationInstance
43
158
  );
44
- const res = await dispatcher.dispatchJob({
159
+
160
+ return await dispatcher.dispatchJob({
45
161
  event: params.event,
46
162
  data: params.data,
47
163
  context: context,
48
164
  });
49
- return res;
50
165
  } catch (error) {
51
166
  console.error(
52
167
  `Error in ${params.event} for ${integrationClass.Definition.name}:`,
@@ -138,4 +138,72 @@ describe('IntegrationEventDispatcher', () => {
138
138
  expect(TestIntegration.latestInstance.isHydrated).toBe(false);
139
139
  });
140
140
  });
141
+
142
+ describe('Webhook Events', () => {
143
+ it('should dispatch WEBHOOK_RECEIVED without hydration', async () => {
144
+ const integration = new TestIntegration();
145
+ integration.events.WEBHOOK_RECEIVED = {
146
+ handler: jest.fn().mockResolvedValue({ received: true })
147
+ };
148
+
149
+ const dispatcher = new IntegrationEventDispatcher(integration);
150
+ const req = { body: { test: 'data' }, params: {} };
151
+ const res = {};
152
+
153
+ await dispatcher.dispatchHttp({
154
+ event: 'WEBHOOK_RECEIVED',
155
+ req,
156
+ res,
157
+ next: jest.fn()
158
+ });
159
+
160
+ expect(integration.events.WEBHOOK_RECEIVED.handler).toHaveBeenCalledWith({
161
+ req,
162
+ res,
163
+ next: expect.any(Function)
164
+ });
165
+ });
166
+
167
+ it('should dispatch ON_WEBHOOK with job context', async () => {
168
+ const integration = new TestIntegration({ id: '123', userId: 'user1' });
169
+ integration.events.ON_WEBHOOK = {
170
+ handler: jest.fn().mockResolvedValue({ processed: true })
171
+ };
172
+
173
+ const dispatcher = new IntegrationEventDispatcher(integration);
174
+ const data = { integrationId: '123', body: { event: 'test' } };
175
+
176
+ await dispatcher.dispatchJob({
177
+ event: 'ON_WEBHOOK',
178
+ data,
179
+ context: {}
180
+ });
181
+
182
+ expect(integration.events.ON_WEBHOOK.handler).toHaveBeenCalledWith({
183
+ data,
184
+ context: {}
185
+ });
186
+ expect(integration.isHydrated).toBe(true);
187
+ });
188
+
189
+ it('should use default WEBHOOK_RECEIVED handler if not overridden', async () => {
190
+ const integration = new TestIntegration();
191
+ const dispatcher = new IntegrationEventDispatcher(integration);
192
+
193
+ const req = { body: { test: 'data' }, params: {}, headers: {}, query: {} };
194
+ const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
195
+
196
+ // Mock queueWebhook
197
+ integration.queueWebhook = jest.fn().mockResolvedValue('message-id');
198
+
199
+ const handler = dispatcher.findEventHandler(integration, 'WEBHOOK_RECEIVED');
200
+ expect(handler).toBeDefined();
201
+
202
+ await handler.call(integration, { req, res });
203
+
204
+ expect(integration.queueWebhook).toHaveBeenCalled();
205
+ expect(res.status).toHaveBeenCalledWith(200);
206
+ expect(res.json).toHaveBeenCalledWith({ received: true });
207
+ });
208
+ });
141
209
  });
@@ -0,0 +1,67 @@
1
+ const { createAppHandler } = require('./../app-handler-helpers');
2
+ const { loadAppDefinition } = require('../app-definition-loader');
3
+ const { Router } = require('express');
4
+ const { IntegrationEventDispatcher } = require('../integration-event-dispatcher');
5
+
6
+ const handlers = {};
7
+ const { integrations: integrationClasses } = loadAppDefinition();
8
+
9
+ for (const IntegrationClass of integrationClasses) {
10
+ const webhookConfig = IntegrationClass.Definition.webhooks;
11
+
12
+ // Skip if webhooks not enabled
13
+ if (!webhookConfig || (typeof webhookConfig === 'object' && !webhookConfig.enabled)) {
14
+ continue;
15
+ }
16
+
17
+ const router = Router();
18
+ const basePath = `/api/${IntegrationClass.Definition.name}-integration/webhooks`;
19
+
20
+ console.log(`\n│ Configuring webhook routes for ${IntegrationClass.Definition.name}:`);
21
+
22
+ // General webhook route (no integration ID)
23
+ router.post('/', async (req, res, next) => {
24
+ try {
25
+ const integrationInstance = new IntegrationClass();
26
+ const dispatcher = new IntegrationEventDispatcher(integrationInstance);
27
+ await dispatcher.dispatchHttp({
28
+ event: 'WEBHOOK_RECEIVED',
29
+ req,
30
+ res,
31
+ next,
32
+ });
33
+ } catch (error) {
34
+ next(error);
35
+ }
36
+ });
37
+ console.log(`│ POST ${basePath}`);
38
+
39
+ // Integration-specific webhook route (with integration ID)
40
+ router.post('/:integrationId', async (req, res, next) => {
41
+ try {
42
+ const integrationInstance = new IntegrationClass();
43
+ const dispatcher = new IntegrationEventDispatcher(integrationInstance);
44
+ await dispatcher.dispatchHttp({
45
+ event: 'WEBHOOK_RECEIVED',
46
+ req,
47
+ res,
48
+ next,
49
+ });
50
+ } catch (error) {
51
+ next(error);
52
+ }
53
+ });
54
+ console.log(`│ POST ${basePath}/:integrationId`);
55
+ console.log('│');
56
+
57
+ handlers[`${IntegrationClass.Definition.name}Webhook`] = {
58
+ handler: createAppHandler(
59
+ `HTTP Event: ${IntegrationClass.Definition.name} Webhook`,
60
+ router,
61
+ false // shouldUseDatabase = false
62
+ ),
63
+ };
64
+ }
65
+
66
+ module.exports = { handlers };
67
+
@@ -0,0 +1,126 @@
1
+ jest.mock('../../database/config', () => ({
2
+ DB_TYPE: 'mongodb',
3
+ getDatabaseType: jest.fn(() => 'mongodb'),
4
+ PRISMA_LOG_LEVEL: 'error,warn',
5
+ PRISMA_QUERY_LOGGING: false,
6
+ }));
7
+
8
+ jest.mock('../app-definition-loader', () => {
9
+ const { IntegrationBase } = require('../../integrations/integration-base');
10
+
11
+ class WebhookEnabledIntegration extends IntegrationBase {
12
+ static Definition = {
13
+ name: 'webhook-enabled',
14
+ version: '1.0.0',
15
+ modules: {},
16
+ webhooks: true,
17
+ };
18
+
19
+ constructor(params) {
20
+ super(params);
21
+ this.queueWebhook = jest.fn().mockResolvedValue('message-id');
22
+ }
23
+ }
24
+
25
+ class AdvancedWebhookIntegration extends IntegrationBase {
26
+ static Definition = {
27
+ name: 'advanced-webhook',
28
+ version: '1.0.0',
29
+ modules: {},
30
+ webhooks: {
31
+ enabled: true,
32
+ },
33
+ };
34
+
35
+ constructor(params) {
36
+ super(params);
37
+ this.events = {
38
+ WEBHOOK_RECEIVED: {
39
+ handler: async ({ req, res }) => {
40
+ // Custom signature verification
41
+ const signature = req.headers['x-webhook-signature'];
42
+ if (signature !== 'valid-signature') {
43
+ return res.status(401).json({ error: 'Invalid signature' });
44
+ }
45
+ await this.queueWebhook({ body: req.body });
46
+ res.status(200).json({ verified: true });
47
+ },
48
+ },
49
+ };
50
+ this.queueWebhook = jest.fn().mockResolvedValue('message-id');
51
+ }
52
+ }
53
+
54
+ class NoWebhookIntegration extends IntegrationBase {
55
+ static Definition = {
56
+ name: 'no-webhook',
57
+ version: '1.0.0',
58
+ modules: {},
59
+ };
60
+ }
61
+
62
+ return {
63
+ loadAppDefinition: () => ({
64
+ integrations: [
65
+ WebhookEnabledIntegration,
66
+ AdvancedWebhookIntegration,
67
+ NoWebhookIntegration,
68
+ ],
69
+ }),
70
+ };
71
+ });
72
+
73
+ describe('Integration Webhook Routers', () => {
74
+ let handlers;
75
+
76
+ beforeEach(() => {
77
+ // Clear module cache to get fresh handlers
78
+ jest.resetModules();
79
+ jest.clearAllMocks();
80
+
81
+ // Re-require after mocking
82
+ handlers = require('./integration-webhook-routers').handlers;
83
+ });
84
+
85
+ describe('Handler Creation', () => {
86
+ it('should create webhook handlers for integrations with webhooks: true', () => {
87
+ expect(handlers['webhook-enabledWebhook']).toBeDefined();
88
+ expect(handlers['webhook-enabledWebhook'].handler).toBeDefined();
89
+ });
90
+
91
+ it('should create webhook handlers for integrations with webhooks.enabled: true', () => {
92
+ expect(handlers['advanced-webhookWebhook']).toBeDefined();
93
+ expect(handlers['advanced-webhookWebhook'].handler).toBeDefined();
94
+ });
95
+
96
+ it('should not create webhook handlers for integrations without webhooks', () => {
97
+ expect(handlers['no-webhookWebhook']).toBeUndefined();
98
+ });
99
+
100
+ it('should configure handlers to not use database connection', () => {
101
+ // Handlers are created with createAppHandler(..., false)
102
+ // This means shouldUseDatabase = false
103
+ // Actual behavior is tested in integration tests
104
+ expect(handlers['webhook-enabledWebhook']).toBeDefined();
105
+ expect(handlers['advanced-webhookWebhook']).toBeDefined();
106
+ });
107
+ });
108
+
109
+ describe('Webhook Configuration', () => {
110
+ it('should support boolean webhook configuration', () => {
111
+ // webhooks: true should enable webhook handling
112
+ expect(handlers['webhook-enabledWebhook']).toBeDefined();
113
+ });
114
+
115
+ it('should support object webhook configuration', () => {
116
+ // webhooks: { enabled: true } should enable webhook handling
117
+ expect(handlers['advanced-webhookWebhook']).toBeDefined();
118
+ });
119
+
120
+ it('should skip integrations with webhooks disabled', () => {
121
+ // webhooks: false or missing should not create handlers
122
+ expect(handlers['no-webhookWebhook']).toBeUndefined();
123
+ });
124
+ });
125
+ });
126
+