@friggframework/core 2.0.0-next.43 → 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.
- package/database/config.js +29 -1
- package/database/use-cases/test-encryption-use-case.js +6 -5
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/backend-utils.js +118 -3
- package/handlers/integration-event-dispatcher.test.js +68 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/integration-webhook-routers.test.js +126 -0
- package/handlers/webhook-flow.integration.test.js +356 -0
- package/handlers/workers/integration-defined-workers.test.js +184 -0
- package/index.js +16 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/integration-base.js +74 -3
- package/integrations/repositories/process-repository-factory.js +46 -0
- package/integrations/repositories/process-repository-interface.js +90 -0
- package/integrations/repositories/process-repository-mongo.js +190 -0
- package/integrations/repositories/process-repository-postgres.js +217 -0
- package/integrations/tests/doubles/dummy-integration-class.js +1 -8
- package/integrations/use-cases/create-process.js +128 -0
- package/integrations/use-cases/create-process.test.js +178 -0
- package/integrations/use-cases/get-process.js +87 -0
- package/integrations/use-cases/get-process.test.js +190 -0
- package/integrations/use-cases/index.js +8 -0
- package/integrations/use-cases/update-process-metrics.js +201 -0
- package/integrations/use-cases/update-process-metrics.test.js +308 -0
- package/integrations/use-cases/update-process-state.js +119 -0
- package/integrations/use-cases/update-process-state.test.js +256 -0
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +44 -0
- package/prisma-postgresql/schema.prisma +45 -0
- package/queues/queuer-util.js +10 -0
- package/user/repositories/user-repository-mongo.js +53 -12
- package/user/repositories/user-repository-postgres.js +53 -14
- package/user/tests/use-cases/login-user.test.js +85 -5
- package/user/tests/user-password-encryption-isolation.test.js +237 -0
- package/user/tests/user-password-hashing.test.js +235 -0
- package/user/use-cases/login-user.js +1 -1
- package/user/user.js +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
|