@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.
Files changed (38) hide show
  1. package/database/config.js +29 -1
  2. package/database/use-cases/test-encryption-use-case.js +6 -5
  3. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  4. package/handlers/WEBHOOKS.md +653 -0
  5. package/handlers/backend-utils.js +118 -3
  6. package/handlers/integration-event-dispatcher.test.js +68 -0
  7. package/handlers/routers/integration-webhook-routers.js +67 -0
  8. package/handlers/routers/integration-webhook-routers.test.js +126 -0
  9. package/handlers/webhook-flow.integration.test.js +356 -0
  10. package/handlers/workers/integration-defined-workers.test.js +184 -0
  11. package/index.js +16 -0
  12. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  13. package/integrations/integration-base.js +74 -3
  14. package/integrations/repositories/process-repository-factory.js +46 -0
  15. package/integrations/repositories/process-repository-interface.js +90 -0
  16. package/integrations/repositories/process-repository-mongo.js +190 -0
  17. package/integrations/repositories/process-repository-postgres.js +217 -0
  18. package/integrations/tests/doubles/dummy-integration-class.js +1 -8
  19. package/integrations/use-cases/create-process.js +128 -0
  20. package/integrations/use-cases/create-process.test.js +178 -0
  21. package/integrations/use-cases/get-process.js +87 -0
  22. package/integrations/use-cases/get-process.test.js +190 -0
  23. package/integrations/use-cases/index.js +8 -0
  24. package/integrations/use-cases/update-process-metrics.js +201 -0
  25. package/integrations/use-cases/update-process-metrics.test.js +308 -0
  26. package/integrations/use-cases/update-process-state.js +119 -0
  27. package/integrations/use-cases/update-process-state.test.js +256 -0
  28. package/package.json +5 -5
  29. package/prisma-mongodb/schema.prisma +44 -0
  30. package/prisma-postgresql/schema.prisma +45 -0
  31. package/queues/queuer-util.js +10 -0
  32. package/user/repositories/user-repository-mongo.js +53 -12
  33. package/user/repositories/user-repository-postgres.js +53 -14
  34. package/user/tests/use-cases/login-user.test.js +85 -5
  35. package/user/tests/user-password-encryption-isolation.test.js +237 -0
  36. package/user/tests/user-password-hashing.test.js +235 -0
  37. package/user/use-cases/login-user.js +1 -1
  38. package/user/user.js +2 -2
@@ -0,0 +1,151 @@
1
+ # Webhook Quick Start Guide
2
+
3
+ Get webhooks working in your Frigg integration in 3 simple steps.
4
+
5
+ ## Step 1: Enable Webhooks
6
+
7
+ Add `webhooks: true` to your Integration Definition:
8
+
9
+ ```javascript
10
+ class MyIntegration extends IntegrationBase {
11
+ static Definition = {
12
+ name: 'my-integration',
13
+ version: '1.0.0',
14
+ modules: {
15
+ myapi: { definition: MyApiDefinition },
16
+ },
17
+ webhooks: true, // ← Add this line
18
+ };
19
+ }
20
+ ```
21
+
22
+ ## Step 2: Handle Webhook Processing
23
+
24
+ Override the `onWebhook` handler to process webhooks:
25
+
26
+ ```javascript
27
+ class MyIntegration extends IntegrationBase {
28
+ // ... Definition ...
29
+
30
+ async onWebhook({ data }) {
31
+ const { body } = data;
32
+
33
+ // You have full access to:
34
+ // - this.myapi (your API modules)
35
+ // - this.config (integration config)
36
+ // - Database operations
37
+
38
+ if (body.event === 'item.created') {
39
+ await this.myapi.api.createItem(body.data);
40
+ }
41
+
42
+ return { processed: true };
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Step 3: Deploy
48
+
49
+ Deploy your Frigg app - webhook routes are automatically created:
50
+
51
+ ```bash
52
+ POST /api/my-integration-integration/webhooks/:integrationId
53
+ ```
54
+
55
+ ## That's It!
56
+
57
+ The default behavior handles:
58
+ - ✅ Receiving webhooks (instant 200 OK response)
59
+ - ✅ Queuing to SQS
60
+ - ✅ Loading your integration with DB and API modules
61
+ - ✅ Calling your `onWebhook` handler
62
+
63
+ ## Optional: Custom Signature Verification
64
+
65
+ Override `onWebhookReceived` for custom signature checks:
66
+
67
+ ```javascript
68
+ async onWebhookReceived({ req, res }) {
69
+ // Verify signature
70
+ const signature = req.headers['x-webhook-signature'];
71
+ if (!this.verifySignature(req.body, signature)) {
72
+ return res.status(401).json({ error: 'Invalid signature' });
73
+ }
74
+
75
+ // Queue for processing (default behavior)
76
+ await this.queueWebhook({
77
+ integrationId: req.params.integrationId,
78
+ body: req.body,
79
+ });
80
+
81
+ res.status(200).json({ received: true });
82
+ }
83
+ ```
84
+
85
+ ## Two Webhook Routes
86
+
87
+ ### With Integration ID (Recommended)
88
+ ```
89
+ POST /api/{name}-integration/webhooks/:integrationId
90
+ ```
91
+ - Full integration loaded in worker
92
+ - Access to DB, config, and API modules
93
+ - Use `this.myapi`, `this.config`, etc.
94
+
95
+ ### Without Integration ID
96
+ ```
97
+ POST /api/{name}-integration/webhooks
98
+ ```
99
+ - Unhydrated integration
100
+ - Useful for system-wide events
101
+ - Limited context
102
+
103
+ ## Need Help?
104
+
105
+ See full documentation: `packages/core/handlers/WEBHOOKS.md`
106
+
107
+ ## Common Patterns
108
+
109
+ ### Slack
110
+ ```javascript
111
+ async onWebhookReceived({ req, res }) {
112
+ if (req.body.type === 'url_verification') {
113
+ return res.json({ challenge: req.body.challenge });
114
+ }
115
+ // ... verify signature, queue ...
116
+ }
117
+ ```
118
+
119
+ ### Stripe
120
+ ```javascript
121
+ async onWebhookReceived({ req, res }) {
122
+ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
123
+ const event = stripe.webhooks.constructEvent(
124
+ JSON.stringify(req.body),
125
+ req.headers['stripe-signature'],
126
+ process.env.STRIPE_WEBHOOK_SECRET
127
+ );
128
+ await this.queueWebhook({ body: event });
129
+ res.status(200).json({ received: true });
130
+ }
131
+ ```
132
+
133
+ ### GitHub
134
+ ```javascript
135
+ async onWebhookReceived({ req, res }) {
136
+ const crypto = require('crypto');
137
+ const signature = req.headers['x-hub-signature-256'];
138
+ const hash = crypto
139
+ .createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
140
+ .update(JSON.stringify(req.body))
141
+ .digest('hex');
142
+
143
+ if (`sha256=${hash}` !== signature) {
144
+ return res.status(401).json({ error: 'Invalid signature' });
145
+ }
146
+
147
+ await this.queueWebhook({ integrationId: req.params.integrationId, body: req.body });
148
+ res.status(200).json({ received: true });
149
+ }
150
+ ```
151
+
@@ -22,6 +22,8 @@ const constantsToBeMigrated = {
22
22
  GET_USER_ACTIONS: 'GET_USER_ACTIONS',
23
23
  GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
24
24
  REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
25
+ WEBHOOK_RECEIVED: 'WEBHOOK_RECEIVED', // HTTP handler, no DB
26
+ ON_WEBHOOK: 'ON_WEBHOOK', // Queue worker, DB-connected
25
27
  // etc...
26
28
  },
27
29
  types: {
@@ -130,6 +132,14 @@ class IntegrationBase {
130
132
  type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
131
133
  handler: this.refreshActionOptions,
132
134
  },
135
+ [constantsToBeMigrated.defaultEvents.WEBHOOK_RECEIVED]: {
136
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
137
+ handler: this.onWebhookReceived,
138
+ },
139
+ [constantsToBeMigrated.defaultEvents.ON_WEBHOOK]: {
140
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
141
+ handler: this.onWebhook,
142
+ },
133
143
  };
134
144
  }
135
145
 
@@ -294,7 +304,9 @@ class IntegrationBase {
294
304
  await this.updateIntegrationStatus.execute(integrationId, 'ENABLED');
295
305
  }
296
306
 
297
- async onUpdate(params) {}
307
+ async onUpdate(params) {
308
+ return this.validateConfig();
309
+ }
298
310
 
299
311
  async onDelete(params) {}
300
312
 
@@ -357,6 +369,50 @@ class IntegrationBase {
357
369
  return options;
358
370
  }
359
371
 
372
+ /**
373
+ * WEBHOOK EVENT HANDLERS
374
+ */
375
+ async onWebhookReceived({ req, res }) {
376
+ // Default: queue webhook for processing
377
+ const body = req.body;
378
+ const integrationId = req.params.integrationId || null;
379
+
380
+ await this.queueWebhook({
381
+ integrationId,
382
+ body,
383
+ headers: req.headers,
384
+ query: req.query,
385
+ });
386
+
387
+ res.status(200).json({ received: true });
388
+ }
389
+
390
+ async onWebhook({ data }) {
391
+ // Default: no-op, integrations override this
392
+ console.log('Webhook received:', data);
393
+ }
394
+
395
+ async queueWebhook(data) {
396
+ const { QueuerUtil } = require('../queues');
397
+
398
+ const queueName = `${this.constructor.Definition.name
399
+ .toUpperCase()
400
+ .replace(/-/g, '_')}_QUEUE_URL`;
401
+ const queueUrl = process.env[queueName];
402
+
403
+ if (!queueUrl) {
404
+ throw new Error(`Queue URL not found for ${queueName}`);
405
+ }
406
+
407
+ return QueuerUtil.send(
408
+ {
409
+ event: 'ON_WEBHOOK',
410
+ data,
411
+ },
412
+ queueUrl
413
+ );
414
+ }
415
+
360
416
  // === Domain Methods (moved from Integration.js) ===
361
417
 
362
418
  getConfig() {
@@ -403,8 +459,14 @@ class IntegrationBase {
403
459
  return this.userId.toString() === userId.toString();
404
460
  }
405
461
 
462
+ registerEventHandlers() {
463
+ this.on = {
464
+ ...this.defaultEvents,
465
+ ...this.events,
466
+ };
467
+ }
468
+
406
469
  async initialize() {
407
- // Load dynamic user actions
408
470
  try {
409
471
  const additionalUserActions = await this.loadDynamicUserActions();
410
472
  this.events = { ...this.events, ...additionalUserActions };
@@ -412,7 +474,16 @@ class IntegrationBase {
412
474
  this.addError(e);
413
475
  }
414
476
 
415
- // Event handlers are no longer registered here - handled by IntegrationEventDispatcher
477
+ this.registerEventHandlers();
478
+ }
479
+
480
+ async send(event, object) {
481
+ if (!this.on[event]) {
482
+ throw new Error(
483
+ `Event ${event} is not defined in the Integration event object`
484
+ );
485
+ }
486
+ return this.on[event].handler.call(this, object);
416
487
  }
417
488
 
418
489
  getOptionDetails() {
@@ -0,0 +1,46 @@
1
+ const { ProcessRepositoryMongo } = require('./process-repository-mongo');
2
+ const { ProcessRepositoryPostgres } = require('./process-repository-postgres');
3
+ const config = require('../../database/config');
4
+
5
+ /**
6
+ * Process Repository Factory
7
+ * Creates the appropriate repository adapter based on database type
8
+ *
9
+ * This implements the Factory pattern for Hexagonal Architecture:
10
+ * - Reads database type from app definition (backend/index.js)
11
+ * - Returns correct adapter (MongoDB or PostgreSQL)
12
+ * - Provides clear error for unsupported databases
13
+ *
14
+ * Usage:
15
+ * ```javascript
16
+ * const repository = createProcessRepository();
17
+ * await repository.create({ userId, integrationId, name, type, state });
18
+ * ```
19
+ *
20
+ * @returns {ProcessRepositoryInterface} Configured repository adapter
21
+ * @throws {Error} If database type is not supported
22
+ */
23
+ function createProcessRepository() {
24
+ const dbType = config.DB_TYPE;
25
+
26
+ switch (dbType) {
27
+ case 'mongodb':
28
+ return new ProcessRepositoryMongo();
29
+
30
+ case 'postgresql':
31
+ return new ProcessRepositoryPostgres();
32
+
33
+ default:
34
+ throw new Error(
35
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
36
+ );
37
+ }
38
+ }
39
+
40
+ module.exports = {
41
+ createProcessRepository,
42
+ // Export adapters for direct testing
43
+ ProcessRepositoryMongo,
44
+ ProcessRepositoryPostgres,
45
+ };
46
+
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ProcessRepository Interface
3
+ *
4
+ * Defines the contract for Process data access operations.
5
+ * Implementations must provide concrete methods for all operations.
6
+ *
7
+ * This interface supports the Hexagonal Architecture pattern by:
8
+ * - Defining clear boundaries between domain logic and data access
9
+ * - Allowing multiple implementations (MongoDB, PostgreSQL, in-memory)
10
+ * - Enabling dependency injection and testability
11
+ */
12
+ class ProcessRepositoryInterface {
13
+ /**
14
+ * Create a new process record
15
+ * @param {Object} processData - Process data to create
16
+ * @param {string} processData.userId - User ID
17
+ * @param {string} processData.integrationId - Integration ID
18
+ * @param {string} processData.name - Process name
19
+ * @param {string} processData.type - Process type
20
+ * @param {string} processData.state - Initial state
21
+ * @param {Object} [processData.context] - Process context
22
+ * @param {Object} [processData.results] - Process results
23
+ * @param {string[]} [processData.childProcesses] - Child process IDs
24
+ * @param {string} [processData.parentProcessId] - Parent process ID
25
+ * @returns {Promise<Object>} Created process record
26
+ */
27
+ async create(processData) {
28
+ throw new Error('Method create() must be implemented');
29
+ }
30
+
31
+ /**
32
+ * Find a process by ID
33
+ * @param {string} processId - Process ID to find
34
+ * @returns {Promise<Object|null>} Process record or null if not found
35
+ */
36
+ async findById(processId) {
37
+ throw new Error('Method findById() must be implemented');
38
+ }
39
+
40
+ /**
41
+ * Update a process record
42
+ * @param {string} processId - Process ID to update
43
+ * @param {Object} updates - Fields to update
44
+ * @returns {Promise<Object>} Updated process record
45
+ */
46
+ async update(processId, updates) {
47
+ throw new Error('Method update() must be implemented');
48
+ }
49
+
50
+ /**
51
+ * Find processes by integration and type
52
+ * @param {string} integrationId - Integration ID
53
+ * @param {string} type - Process type
54
+ * @returns {Promise<Array>} Array of process records
55
+ */
56
+ async findByIntegrationAndType(integrationId, type) {
57
+ throw new Error('Method findByIntegrationAndType() must be implemented');
58
+ }
59
+
60
+ /**
61
+ * Find active processes (not in excluded states)
62
+ * @param {string} integrationId - Integration ID
63
+ * @param {string[]} [excludeStates=['COMPLETED', 'ERROR']] - States to exclude
64
+ * @returns {Promise<Array>} Array of active process records
65
+ */
66
+ async findActiveProcesses(integrationId, excludeStates = ['COMPLETED', 'ERROR']) {
67
+ throw new Error('Method findActiveProcesses() must be implemented');
68
+ }
69
+
70
+ /**
71
+ * Find a process by name (most recent)
72
+ * @param {string} name - Process name
73
+ * @returns {Promise<Object|null>} Most recent process with given name, or null
74
+ */
75
+ async findByName(name) {
76
+ throw new Error('Method findByName() must be implemented');
77
+ }
78
+
79
+ /**
80
+ * Delete a process by ID
81
+ * @param {string} processId - Process ID to delete
82
+ * @returns {Promise<void>}
83
+ */
84
+ async deleteById(processId) {
85
+ throw new Error('Method deleteById() must be implemented');
86
+ }
87
+ }
88
+
89
+ module.exports = { ProcessRepositoryInterface };
90
+
@@ -0,0 +1,190 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const { ProcessRepositoryInterface } = require('./process-repository-interface');
3
+
4
+ /**
5
+ * MongoDB Process Repository Adapter
6
+ * Handles process persistence using Prisma with MongoDB
7
+ *
8
+ * MongoDB-specific characteristics:
9
+ * - Uses scalar fields for relations (userId, integrationId)
10
+ * - IDs are strings with @db.ObjectId
11
+ * - JSON fields for flexible context and results storage
12
+ * - Array field for childProcesses references
13
+ *
14
+ * Design Philosophy:
15
+ * - Generic Process model supports any type of long-running operation
16
+ * - Context and results stored as JSON for maximum flexibility
17
+ * - Integration-specific logic lives in use cases and services
18
+ */
19
+ class ProcessRepositoryMongo extends ProcessRepositoryInterface {
20
+ constructor() {
21
+ super();
22
+ this.prisma = prisma;
23
+ }
24
+
25
+ /**
26
+ * Create a new process record
27
+ * @param {Object} processData - Process data to create
28
+ * @returns {Promise<Object>} Created process record
29
+ */
30
+ async create(processData) {
31
+ const process = await this.prisma.process.create({
32
+ data: {
33
+ userId: processData.userId,
34
+ integrationId: processData.integrationId,
35
+ name: processData.name,
36
+ type: processData.type,
37
+ state: processData.state || 'INITIALIZING',
38
+ context: processData.context || {},
39
+ results: processData.results || {},
40
+ childProcesses: processData.childProcesses || [],
41
+ parentProcessId: processData.parentProcessId || null,
42
+ },
43
+ });
44
+
45
+ return this._toPlainObject(process);
46
+ }
47
+
48
+ /**
49
+ * Find a process by ID
50
+ * @param {string} processId - Process ID to find
51
+ * @returns {Promise<Object|null>} Process record or null if not found
52
+ */
53
+ async findById(processId) {
54
+ const process = await this.prisma.process.findUnique({
55
+ where: { id: processId },
56
+ });
57
+
58
+ return process ? this._toPlainObject(process) : null;
59
+ }
60
+
61
+ /**
62
+ * Update a process record
63
+ * @param {string} processId - Process ID to update
64
+ * @param {Object} updates - Fields to update
65
+ * @returns {Promise<Object>} Updated process record
66
+ */
67
+ async update(processId, updates) {
68
+ // Prepare update data, excluding undefined values
69
+ const updateData = {};
70
+
71
+ if (updates.state !== undefined) {
72
+ updateData.state = updates.state;
73
+ }
74
+ if (updates.context !== undefined) {
75
+ updateData.context = updates.context;
76
+ }
77
+ if (updates.results !== undefined) {
78
+ updateData.results = updates.results;
79
+ }
80
+ if (updates.childProcesses !== undefined) {
81
+ updateData.childProcesses = updates.childProcesses;
82
+ }
83
+ if (updates.parentProcessId !== undefined) {
84
+ updateData.parentProcessId = updates.parentProcessId;
85
+ }
86
+
87
+ const process = await this.prisma.process.update({
88
+ where: { id: processId },
89
+ data: updateData,
90
+ });
91
+
92
+ return this._toPlainObject(process);
93
+ }
94
+
95
+ /**
96
+ * Find processes by integration and type
97
+ * @param {string} integrationId - Integration ID
98
+ * @param {string} type - Process type
99
+ * @returns {Promise<Array>} Array of process records
100
+ */
101
+ async findByIntegrationAndType(integrationId, type) {
102
+ const processes = await this.prisma.process.findMany({
103
+ where: {
104
+ integrationId,
105
+ type,
106
+ },
107
+ orderBy: {
108
+ createdAt: 'desc',
109
+ },
110
+ });
111
+
112
+ return processes.map((p) => this._toPlainObject(p));
113
+ }
114
+
115
+ /**
116
+ * Find active processes (not in excluded states)
117
+ * @param {string} integrationId - Integration ID
118
+ * @param {string[]} [excludeStates=['COMPLETED', 'ERROR']] - States to exclude
119
+ * @returns {Promise<Array>} Array of active process records
120
+ */
121
+ async findActiveProcesses(integrationId, excludeStates = ['COMPLETED', 'ERROR']) {
122
+ const processes = await this.prisma.process.findMany({
123
+ where: {
124
+ integrationId,
125
+ state: {
126
+ notIn: excludeStates,
127
+ },
128
+ },
129
+ orderBy: {
130
+ createdAt: 'desc',
131
+ },
132
+ });
133
+
134
+ return processes.map((p) => this._toPlainObject(p));
135
+ }
136
+
137
+ /**
138
+ * Find a process by name (most recent)
139
+ * @param {string} name - Process name
140
+ * @returns {Promise<Object|null>} Most recent process with given name, or null
141
+ */
142
+ async findByName(name) {
143
+ const process = await this.prisma.process.findFirst({
144
+ where: { name },
145
+ orderBy: {
146
+ createdAt: 'desc',
147
+ },
148
+ });
149
+
150
+ return process ? this._toPlainObject(process) : null;
151
+ }
152
+
153
+ /**
154
+ * Delete a process by ID
155
+ * @param {string} processId - Process ID to delete
156
+ * @returns {Promise<void>}
157
+ */
158
+ async deleteById(processId) {
159
+ await this.prisma.process.delete({
160
+ where: { id: processId },
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Convert Prisma model to plain JavaScript object
166
+ * Ensures consistent API across repository implementations
167
+ * @private
168
+ * @param {Object} process - Prisma process model
169
+ * @returns {Object} Plain process object
170
+ */
171
+ _toPlainObject(process) {
172
+ return {
173
+ id: process.id,
174
+ userId: process.userId,
175
+ integrationId: process.integrationId,
176
+ name: process.name,
177
+ type: process.type,
178
+ state: process.state,
179
+ context: process.context,
180
+ results: process.results,
181
+ childProcesses: process.childProcesses,
182
+ parentProcessId: process.parentProcessId,
183
+ createdAt: process.createdAt,
184
+ updatedAt: process.updatedAt,
185
+ };
186
+ }
187
+ }
188
+
189
+ module.exports = { ProcessRepositoryMongo };
190
+