@friggframework/core 2.0.0-next.40 → 2.0.0-next.42
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/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +321 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
package/core/CLAUDE.md
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
# CLAUDE.md - Frigg Core Runtime System
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code when working with the Frigg Framework's core runtime system in `packages/core/core/`.
|
|
4
|
+
|
|
5
|
+
## Critical Context (Read First)
|
|
6
|
+
|
|
7
|
+
- **Package Purpose**: Core runtime system and foundational classes for Frigg Lambda execution
|
|
8
|
+
- **Main Components**: Handler factory, Worker base class, Delegate pattern, Module loading
|
|
9
|
+
- **Core Architecture**: Lambda-optimized runtime with connection pooling, error handling, secrets management
|
|
10
|
+
- **Key Integration**: AWS Lambda, SQS job processing, MongoDB connections, AWS Secrets Manager
|
|
11
|
+
- **Security Model**: Automatic secrets injection, database connection management, user-facing error sanitization
|
|
12
|
+
- **DO NOT**: Expose internal errors to users, bypass connection pooling, skip database initialization
|
|
13
|
+
|
|
14
|
+
## Core Components Architecture
|
|
15
|
+
|
|
16
|
+
### Handler Creation System (`create-handler.js:9-67`)
|
|
17
|
+
|
|
18
|
+
**Purpose**: Factory for creating Lambda handlers with consistent infrastructure setup
|
|
19
|
+
|
|
20
|
+
**Key Features**:
|
|
21
|
+
- **Database Connection Management**: Automatic MongoDB connection with pooling
|
|
22
|
+
- **Secrets Management**: AWS Secrets Manager integration via `SECRET_ARN` env var
|
|
23
|
+
- **Error Sanitization**: Prevents internal details from leaking to end users
|
|
24
|
+
- **Debug Logging**: Request/response logging with structured debug info
|
|
25
|
+
- **Connection Optimization**: `context.callbackWaitsForEmptyEventLoop = false` for reuse
|
|
26
|
+
|
|
27
|
+
**Handler Configuration Options**:
|
|
28
|
+
```javascript
|
|
29
|
+
const handler = createHandler({
|
|
30
|
+
eventName: 'MyIntegration', // For logging/debugging
|
|
31
|
+
isUserFacingResponse: true, // true = sanitize errors, false = pass through
|
|
32
|
+
method: async (event, context) => {}, // Your Lambda function logic
|
|
33
|
+
shouldUseDatabase: true // false = skip MongoDB connection
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Error Handling Patterns**:
|
|
38
|
+
- **User-Facing**: Returns 500 with generic "Internal Error Occurred" message
|
|
39
|
+
- **Server-to-Server**: Re-throws errors for AWS to handle
|
|
40
|
+
- **Halt Errors**: `error.isHaltError = true` logs but returns success (no retry)
|
|
41
|
+
|
|
42
|
+
### Worker Base Class (`Worker.js:9-83`)
|
|
43
|
+
|
|
44
|
+
**Purpose**: Base class for SQS job processing with standardized patterns
|
|
45
|
+
|
|
46
|
+
**Core Responsibilities**:
|
|
47
|
+
- **Queue Management**: Get SQS queue URLs and send messages
|
|
48
|
+
- **Batch Processing**: Process multiple SQS records in sequence
|
|
49
|
+
- **Message Validation**: Extensible parameter validation system
|
|
50
|
+
- **Error Handling**: Structured error handling for async job processing
|
|
51
|
+
|
|
52
|
+
**Usage Pattern**:
|
|
53
|
+
```javascript
|
|
54
|
+
class MyWorker extends Worker {
|
|
55
|
+
async _run(params, context = {}) {
|
|
56
|
+
// Your job processing logic here
|
|
57
|
+
// params are already JSON.parsed from SQS message body
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_validateParams(params) {
|
|
61
|
+
// Validate required parameters
|
|
62
|
+
this._verifyParamExists(params, 'requiredField');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// In your Lambda handler
|
|
67
|
+
const worker = new MyWorker();
|
|
68
|
+
await worker.run(event, context); // Process SQS Records
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Message Sending**:
|
|
72
|
+
```javascript
|
|
73
|
+
await worker.send({
|
|
74
|
+
QueueUrl: 'https://sqs.region.amazonaws.com/account/queue',
|
|
75
|
+
jobType: 'processAttachment',
|
|
76
|
+
integrationId: 'abc123',
|
|
77
|
+
// ... other job parameters
|
|
78
|
+
}, delaySeconds);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Delegate Pattern System (`Delegate.js:3-27`)
|
|
82
|
+
|
|
83
|
+
**Purpose**: Observer/delegation pattern for decoupled component communication
|
|
84
|
+
|
|
85
|
+
**Core Concepts**:
|
|
86
|
+
- **Notification System**: Components notify delegates of events/state changes
|
|
87
|
+
- **Type Safety**: `delegateTypes` array defines valid notification strings
|
|
88
|
+
- **Bidirectional**: Supports both sending and receiving notifications
|
|
89
|
+
- **Null Safety**: Gracefully handles missing delegates
|
|
90
|
+
|
|
91
|
+
**Implementation Pattern**:
|
|
92
|
+
```javascript
|
|
93
|
+
class MyIntegration extends Delegate {
|
|
94
|
+
constructor(params) {
|
|
95
|
+
super(params);
|
|
96
|
+
this.delegateTypes = ['processComplete', 'errorOccurred', 'statusUpdate'];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async processData(data) {
|
|
100
|
+
// Do work
|
|
101
|
+
await this.notify('statusUpdate', { progress: 50 });
|
|
102
|
+
// More work
|
|
103
|
+
await this.notify('processComplete', { result: data });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async receiveNotification(notifier, delegateString, object) {
|
|
107
|
+
// Handle notifications from other components
|
|
108
|
+
switch(delegateString) {
|
|
109
|
+
case 'dataReady':
|
|
110
|
+
await this.processData(object);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Module Loading System (`load-installed-modules.js:1-1085`)
|
|
118
|
+
|
|
119
|
+
**Purpose**: Dynamic loading and registration of integration modules
|
|
120
|
+
|
|
121
|
+
**Key Features**:
|
|
122
|
+
- **Package Discovery**: Automatically find `@friggframework/api-module-*` packages
|
|
123
|
+
- **Module Registration**: Load and register integration classes
|
|
124
|
+
- **Configuration Management**: Handle module-specific configuration
|
|
125
|
+
- **Dependency Resolution**: Manage inter-module dependencies
|
|
126
|
+
|
|
127
|
+
## Runtime Lifecycle & Patterns
|
|
128
|
+
|
|
129
|
+
### Lambda Handler Lifecycle
|
|
130
|
+
1. **Pre-Execution Setup**:
|
|
131
|
+
```javascript
|
|
132
|
+
initDebugLog(eventName, event); // Debug logging setup
|
|
133
|
+
await secretsToEnv(); // Secrets Manager injection
|
|
134
|
+
context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
2. **Database Connection**:
|
|
138
|
+
```javascript
|
|
139
|
+
if (shouldUseDatabase) {
|
|
140
|
+
await connectToDatabase(); // MongoDB connection with pooling
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
3. **Method Execution**:
|
|
145
|
+
```javascript
|
|
146
|
+
return await method(event, context); // Your integration logic
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
4. **Error Handling & Cleanup**:
|
|
150
|
+
```javascript
|
|
151
|
+
flushDebugLog(error); // Debug info flush on error
|
|
152
|
+
// Sanitized error response for user-facing endpoints
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### SQS Job Processing Lifecycle
|
|
156
|
+
1. **Batch Processing**: Process all records in `event.Records` sequentially
|
|
157
|
+
2. **Message Parsing**: JSON.parse message body for parameters
|
|
158
|
+
3. **Validation**: Run custom validation on parsed parameters
|
|
159
|
+
4. **Execution**: Call `_run()` method with validated parameters
|
|
160
|
+
5. **Error Propagation**: Let AWS handle retries/DLQ for failed jobs
|
|
161
|
+
|
|
162
|
+
### Secrets Management Integration
|
|
163
|
+
- **Automatic Injection**: If `SECRET_ARN` environment variable is set
|
|
164
|
+
- **Environment Variables**: Secrets automatically set as `process.env` variables
|
|
165
|
+
- **Security**: No secrets logging or exposure in error messages
|
|
166
|
+
- **Caching**: Secrets cached for Lambda container lifetime
|
|
167
|
+
|
|
168
|
+
## Database Connection Patterns
|
|
169
|
+
|
|
170
|
+
### Connection Pooling Strategy
|
|
171
|
+
```javascript
|
|
172
|
+
// Mongoose connection reuse across Lambda invocations
|
|
173
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
174
|
+
await connectToDatabase(); // Reuses existing connection if available
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Database Usage Patterns
|
|
178
|
+
```javascript
|
|
179
|
+
// Conditional database connection
|
|
180
|
+
const handler = createHandler({
|
|
181
|
+
shouldUseDatabase: false, // Skip for database-free operations
|
|
182
|
+
method: async (event) => {
|
|
183
|
+
// No DB operations needed
|
|
184
|
+
return { statusCode: 200, body: 'OK' };
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Error Handling Architecture
|
|
190
|
+
|
|
191
|
+
### Error Classification
|
|
192
|
+
1. **User-Facing Errors**: `isUserFacingResponse: true`
|
|
193
|
+
- Returns generic 500 error message
|
|
194
|
+
- Prevents information disclosure
|
|
195
|
+
- Logs full error details internally
|
|
196
|
+
|
|
197
|
+
2. **Server-to-Server Errors**: `isUserFacingResponse: false`
|
|
198
|
+
- Re-throws original error for AWS handling
|
|
199
|
+
- Used for SQS, SNS, and internal API calls
|
|
200
|
+
- Enables proper retry mechanisms
|
|
201
|
+
|
|
202
|
+
3. **Halt Errors**: `error.isHaltError = true`
|
|
203
|
+
- Logs error but returns success
|
|
204
|
+
- Prevents infinite retries for known issues
|
|
205
|
+
- Used for graceful degradation scenarios
|
|
206
|
+
|
|
207
|
+
### Debug Logging Strategy
|
|
208
|
+
```javascript
|
|
209
|
+
initDebugLog(eventName, event); // Start logging context
|
|
210
|
+
// ... your code ...
|
|
211
|
+
flushDebugLog(error); // Flush on error (includes full context)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Integration Development Patterns
|
|
215
|
+
|
|
216
|
+
### Extending Worker for Job Processing
|
|
217
|
+
```javascript
|
|
218
|
+
class AttachmentWorker extends Worker {
|
|
219
|
+
_validateParams(params) {
|
|
220
|
+
this._verifyParamExists(params, 'integrationId');
|
|
221
|
+
this._verifyParamExists(params, 'attachmentUrl');
|
|
222
|
+
this._verifyParamExists(params, 'destination');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async _run(params, context) {
|
|
226
|
+
const { integrationId, attachmentUrl, destination } = params;
|
|
227
|
+
// Process attachment upload/download
|
|
228
|
+
// Handle errors gracefully
|
|
229
|
+
// Update job status
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Creating Custom Handlers
|
|
235
|
+
```javascript
|
|
236
|
+
const myIntegrationHandler = createHandler({
|
|
237
|
+
eventName: 'MyIntegration',
|
|
238
|
+
isUserFacingResponse: true, // Sanitize errors for users
|
|
239
|
+
shouldUseDatabase: true, // Need database access
|
|
240
|
+
method: async (event, context) => {
|
|
241
|
+
// Your integration logic here
|
|
242
|
+
// Database is already connected
|
|
243
|
+
// Secrets are in process.env
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
statusCode: 200,
|
|
247
|
+
body: JSON.stringify({ success: true })
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Delegate Pattern for Integration Communication
|
|
254
|
+
```javascript
|
|
255
|
+
class IntegrationManager extends Delegate {
|
|
256
|
+
constructor() {
|
|
257
|
+
super();
|
|
258
|
+
this.delegateTypes = [
|
|
259
|
+
'authenticationComplete',
|
|
260
|
+
'syncStarted',
|
|
261
|
+
'syncComplete',
|
|
262
|
+
'errorOccurred'
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async startSync(integrationId) {
|
|
267
|
+
await this.notify('syncStarted', { integrationId });
|
|
268
|
+
// ... sync logic ...
|
|
269
|
+
await this.notify('syncComplete', { integrationId, recordCount: 100 });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Performance Optimization Patterns
|
|
275
|
+
|
|
276
|
+
### Connection Reuse
|
|
277
|
+
```javascript
|
|
278
|
+
// ALWAYS set this in handlers for performance
|
|
279
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Conditional Database Usage
|
|
283
|
+
```javascript
|
|
284
|
+
// Skip database for lightweight operations
|
|
285
|
+
const handler = createHandler({
|
|
286
|
+
shouldUseDatabase: false, // Faster cold starts
|
|
287
|
+
method: healthCheckMethod
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### SQS Batch Processing Optimization
|
|
292
|
+
```javascript
|
|
293
|
+
// Process records sequentially (not parallel) for resource control
|
|
294
|
+
for (const record of records) {
|
|
295
|
+
await this._run(JSON.parse(record.body), context);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Repository & Use Case Architecture
|
|
300
|
+
|
|
301
|
+
The Frigg Framework follows DDD/Hexagonal Architecture with clear separation between handlers, use cases, and repositories.
|
|
302
|
+
|
|
303
|
+
### Repository Pattern in Core
|
|
304
|
+
|
|
305
|
+
**Purpose**: Abstract database and external system access into dedicated repository classes.
|
|
306
|
+
|
|
307
|
+
**Structure**:
|
|
308
|
+
```javascript
|
|
309
|
+
// Example: packages/core/database/websocket-connection-repository.js
|
|
310
|
+
class WebsocketConnectionRepository {
|
|
311
|
+
/**
|
|
312
|
+
* Create a new WebSocket connection record
|
|
313
|
+
* Pure database operation - no business logic
|
|
314
|
+
*/
|
|
315
|
+
async createConnection(connectionId) {
|
|
316
|
+
return await WebsocketConnection.create({ connectionId });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Delete a WebSocket connection record
|
|
321
|
+
* Returns raw deletion result
|
|
322
|
+
*/
|
|
323
|
+
async deleteConnection(connectionId) {
|
|
324
|
+
return await WebsocketConnection.deleteOne({ connectionId });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get all active connections
|
|
329
|
+
* Returns raw data from database
|
|
330
|
+
*/
|
|
331
|
+
async getActiveConnections() {
|
|
332
|
+
return await WebsocketConnection.getActiveConnections();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Repository Responsibilities**:
|
|
338
|
+
- ✅ **CRUD operations** - Create, Read, Update, Delete database records
|
|
339
|
+
- ✅ **Query execution** - Run database queries and return results
|
|
340
|
+
- ✅ **Data access only** - No interpretation or decision-making
|
|
341
|
+
- ✅ **Atomic operations** - Each method performs one database operation
|
|
342
|
+
- ❌ **NO business logic** - Don't decide what data means or what to do with it
|
|
343
|
+
- ❌ **NO orchestration** - Don't coordinate multiple operations
|
|
344
|
+
|
|
345
|
+
**Real Repository Examples**:
|
|
346
|
+
- `WebsocketConnectionRepository` - WebSocket persistence (packages/core/database/websocket-connection-repository.js)
|
|
347
|
+
- `SyncRepository` - Sync object management (packages/core/syncs/sync-repository.js)
|
|
348
|
+
- `IntegrationMappingRepository` - Integration mappings (packages/core/integrations/integration-mapping-repository.js)
|
|
349
|
+
- `TokenRepository` - Token operations (packages/core/database/token-repository.js)
|
|
350
|
+
- `HealthCheckRepository` - Health check data access (packages/core/database/health-check-repository.js)
|
|
351
|
+
|
|
352
|
+
### Use Case Pattern in Core
|
|
353
|
+
|
|
354
|
+
**Purpose**: Contain business logic, orchestration, and workflow coordination.
|
|
355
|
+
|
|
356
|
+
**Structure**:
|
|
357
|
+
```javascript
|
|
358
|
+
// Example: packages/core/database/use-cases/check-database-health-use-case.js
|
|
359
|
+
class CheckDatabaseHealthUseCase {
|
|
360
|
+
constructor({ healthCheckRepository }) {
|
|
361
|
+
// Dependency injection - receive repository via constructor
|
|
362
|
+
this.repository = healthCheckRepository;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async execute() {
|
|
366
|
+
// 1. Get raw data from repository
|
|
367
|
+
const { stateName, isConnected } = this.repository.getDatabaseConnectionState();
|
|
368
|
+
|
|
369
|
+
// 2. Apply business logic - determine health status
|
|
370
|
+
const result = {
|
|
371
|
+
status: isConnected ? 'healthy' : 'unhealthy',
|
|
372
|
+
state: stateName,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// 3. Orchestration - conditionally perform additional checks
|
|
376
|
+
if (isConnected) {
|
|
377
|
+
result.responseTime = await this.repository.pingDatabase(2000);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Use Case Responsibilities**:
|
|
386
|
+
- ✅ **Business logic** - Make decisions based on data
|
|
387
|
+
- ✅ **Orchestration** - Coordinate multiple repository calls
|
|
388
|
+
- ✅ **Validation** - Enforce business rules
|
|
389
|
+
- ✅ **Workflow** - Determine what happens next
|
|
390
|
+
- ✅ **Error handling** - Handle domain-specific errors
|
|
391
|
+
- ❌ **NO direct database access** - Always use repositories
|
|
392
|
+
- ❌ **NO HTTP concerns** - Don't know about status codes or headers
|
|
393
|
+
|
|
394
|
+
**Real Use Case Examples**:
|
|
395
|
+
- `CheckDatabaseHealthUseCase` - Database health business logic (packages/core/database/use-cases/check-database-health-use-case.js)
|
|
396
|
+
- `TestEncryptionUseCase` - Encryption testing workflow (packages/core/database/use-cases/test-encryption-use-case.js)
|
|
397
|
+
|
|
398
|
+
### Handler Pattern in Core
|
|
399
|
+
|
|
400
|
+
**Purpose**: Translate Lambda/HTTP/SQS events into use case calls.
|
|
401
|
+
|
|
402
|
+
**Handler Should ONLY**:
|
|
403
|
+
- Define routes and event handlers
|
|
404
|
+
- Call use cases (NOT repositories)
|
|
405
|
+
- Map use case results to HTTP/Lambda responses
|
|
406
|
+
- Handle protocol-specific concerns (status codes, headers)
|
|
407
|
+
|
|
408
|
+
**❌ WRONG - Handler contains business logic**:
|
|
409
|
+
```javascript
|
|
410
|
+
// BAD: Business logic in handler
|
|
411
|
+
router.get('/health', async (req, res) => {
|
|
412
|
+
const state = mongoose.connection.readyState;
|
|
413
|
+
const isHealthy = state === 1; // ❌ Business logic in handler
|
|
414
|
+
|
|
415
|
+
if (isHealthy) { // ❌ Orchestration in handler
|
|
416
|
+
const pingStart = Date.now();
|
|
417
|
+
await mongoose.connection.db.admin().ping(); // ❌ Direct DB access
|
|
418
|
+
const responseTime = Date.now() - pingStart;
|
|
419
|
+
res.json({ status: 'healthy', responseTime });
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**✅ CORRECT - Handler delegates to use case**:
|
|
425
|
+
```javascript
|
|
426
|
+
// GOOD: Handler calls use case
|
|
427
|
+
const healthCheckRepository = new HealthCheckRepository();
|
|
428
|
+
const checkDatabaseHealthUseCase = new CheckDatabaseHealthUseCase({
|
|
429
|
+
healthCheckRepository
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
router.get('/health', async (req, res) => {
|
|
433
|
+
// Call use case - all business logic is there
|
|
434
|
+
const health = await checkDatabaseHealthUseCase.execute();
|
|
435
|
+
|
|
436
|
+
// Handler only maps to HTTP response
|
|
437
|
+
const statusCode = health.status === 'healthy' ? 200 : 503;
|
|
438
|
+
res.status(statusCode).json(health);
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Dependency Direction
|
|
443
|
+
|
|
444
|
+
**The Golden Rule**:
|
|
445
|
+
> "Handlers ONLY call Use Cases, NEVER Repositories or Business Logic directly"
|
|
446
|
+
|
|
447
|
+
**Correct Flow**:
|
|
448
|
+
```
|
|
449
|
+
Handler/Router (createHandler)
|
|
450
|
+
↓ calls
|
|
451
|
+
Use Case (execute)
|
|
452
|
+
↓ calls
|
|
453
|
+
Repository (CRUD methods)
|
|
454
|
+
↓ accesses
|
|
455
|
+
Database/External System
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Why This Matters**:
|
|
459
|
+
- **Testability**: Use cases can be tested with mocked repositories
|
|
460
|
+
- **Reusability**: Use cases can be called from handlers, CLI, background jobs
|
|
461
|
+
- **Maintainability**: Business logic is centralized, not scattered across handlers
|
|
462
|
+
- **Flexibility**: Swap repository implementations without changing use cases
|
|
463
|
+
|
|
464
|
+
### Migration from Old Patterns
|
|
465
|
+
|
|
466
|
+
**Old Pattern (Mongoose models everywhere)**:
|
|
467
|
+
```javascript
|
|
468
|
+
// BAD: Direct model access in handlers
|
|
469
|
+
const handler = createHandler({
|
|
470
|
+
method: async (event) => {
|
|
471
|
+
const user = await User.findById(event.userId); // ❌ Direct model access
|
|
472
|
+
if (!user.isActive) { // ❌ Business logic in handler
|
|
473
|
+
throw new Error('User not active');
|
|
474
|
+
}
|
|
475
|
+
await Sync.create({ userId: user.id }); // ❌ Direct model access
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**New Pattern (Repository + Use Case)**:
|
|
481
|
+
```javascript
|
|
482
|
+
// GOOD: Repository abstracts data access
|
|
483
|
+
class UserRepository {
|
|
484
|
+
async findById(userId) {
|
|
485
|
+
return await User.findById(userId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
class SyncRepository {
|
|
490
|
+
async createSync(data) {
|
|
491
|
+
return await Sync.create(data);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// GOOD: Use case contains business logic
|
|
496
|
+
class ActivateUserSyncUseCase {
|
|
497
|
+
constructor({ userRepository, syncRepository }) {
|
|
498
|
+
this.userRepo = userRepository;
|
|
499
|
+
this.syncRepo = syncRepository;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async execute(userId) {
|
|
503
|
+
const user = await this.userRepo.findById(userId);
|
|
504
|
+
|
|
505
|
+
if (!user.isActive) { // ✅ Business logic in use case
|
|
506
|
+
throw new Error('User not active');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return await this.syncRepo.createSync({ userId: user.id });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// GOOD: Handler delegates to use case
|
|
514
|
+
const handler = createHandler({
|
|
515
|
+
method: async (event) => {
|
|
516
|
+
const useCase = new ActivateUserSyncUseCase({
|
|
517
|
+
userRepository: new UserRepository(),
|
|
518
|
+
syncRepository: new SyncRepository()
|
|
519
|
+
});
|
|
520
|
+
return await useCase.execute(event.userId);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Integration with Worker Pattern
|
|
526
|
+
|
|
527
|
+
**Workers should also follow this pattern**:
|
|
528
|
+
|
|
529
|
+
```javascript
|
|
530
|
+
class ProcessAttachmentWorker extends Worker {
|
|
531
|
+
constructor() {
|
|
532
|
+
super();
|
|
533
|
+
// Inject repositories into use case
|
|
534
|
+
this.useCase = new ProcessAttachmentUseCase({
|
|
535
|
+
asanaRepository: new AsanaRepository(),
|
|
536
|
+
frontifyRepository: new FrontifyRepository()
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
_validateParams(params) {
|
|
541
|
+
this._verifyParamExists(params, 'attachmentId');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async _run(params, context) {
|
|
545
|
+
// Worker delegates to use case
|
|
546
|
+
return await this.useCase.execute(params.attachmentId);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### When to Extract to Repository/Use Case
|
|
552
|
+
|
|
553
|
+
**Extract to Repository when you see**:
|
|
554
|
+
- Direct Mongoose model calls (`User.findById()`, `Sync.create()`)
|
|
555
|
+
- Database queries in handlers or business logic
|
|
556
|
+
- External API calls scattered across codebase
|
|
557
|
+
- File system or AWS SDK operations in handlers
|
|
558
|
+
|
|
559
|
+
**Extract to Use Case when you see**:
|
|
560
|
+
- Business logic in handlers (if/else based on data)
|
|
561
|
+
- Orchestration of multiple operations
|
|
562
|
+
- Validation and error handling logic
|
|
563
|
+
- Workflow coordination
|
|
564
|
+
|
|
565
|
+
### Testing with Repository/Use Case Pattern
|
|
566
|
+
|
|
567
|
+
**Repository Tests** (Integration tests with real DB):
|
|
568
|
+
```javascript
|
|
569
|
+
describe('WebsocketConnectionRepository', () => {
|
|
570
|
+
it('creates connection record', async () => {
|
|
571
|
+
const repo = new WebsocketConnectionRepository();
|
|
572
|
+
const result = await repo.createConnection('conn-123');
|
|
573
|
+
expect(result.connectionId).toBe('conn-123');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Use Case Tests** (Unit tests with mocked repositories):
|
|
579
|
+
```javascript
|
|
580
|
+
describe('CheckDatabaseHealthUseCase', () => {
|
|
581
|
+
it('returns unhealthy when disconnected', async () => {
|
|
582
|
+
const mockRepo = {
|
|
583
|
+
getDatabaseConnectionState: () => ({
|
|
584
|
+
stateName: 'disconnected',
|
|
585
|
+
isConnected: false
|
|
586
|
+
})
|
|
587
|
+
};
|
|
588
|
+
const useCase = new CheckDatabaseHealthUseCase({
|
|
589
|
+
healthCheckRepository: mockRepo
|
|
590
|
+
});
|
|
591
|
+
const result = await useCase.execute();
|
|
592
|
+
expect(result.status).toBe('unhealthy');
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Handler Tests** (HTTP/Lambda response tests):
|
|
598
|
+
```javascript
|
|
599
|
+
describe('Health Handler', () => {
|
|
600
|
+
it('returns 503 when unhealthy', async () => {
|
|
601
|
+
// Mock use case
|
|
602
|
+
const mockUseCase = {
|
|
603
|
+
execute: async () => ({ status: 'unhealthy' })
|
|
604
|
+
};
|
|
605
|
+
// Test HTTP response
|
|
606
|
+
const response = await handler(mockEvent, mockContext);
|
|
607
|
+
expect(response.statusCode).toBe(503);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Anti-Patterns to Avoid
|
|
613
|
+
|
|
614
|
+
### Core Runtime Anti-Patterns
|
|
615
|
+
❌ **Don't expose internal errors** to user-facing endpoints - use `isUserFacingResponse: true`
|
|
616
|
+
❌ **Don't skip connection optimization** - always set `callbackWaitsForEmptyEventLoop = false`
|
|
617
|
+
❌ **Don't parallel process SQS records** - sequential processing prevents resource exhaustion
|
|
618
|
+
❌ **Don't hardcode queue URLs** - use the Worker's `getQueueURL()` method
|
|
619
|
+
❌ **Don't bypass parameter validation** - always implement `_validateParams()` in Workers
|
|
620
|
+
❌ **Don't leak secrets in logs** - the system handles this, don't override
|
|
621
|
+
❌ **Don't ignore delegate types** - define valid `delegateTypes` array for type safety
|
|
622
|
+
|
|
623
|
+
### DDD/Hexagonal Architecture Anti-Patterns
|
|
624
|
+
❌ **Don't access models directly in handlers** - create repositories to abstract data access
|
|
625
|
+
❌ **Don't put business logic in handlers** - extract to use cases
|
|
626
|
+
❌ **Don't call repositories from handlers** - always go through use cases
|
|
627
|
+
❌ **Don't put orchestration in repositories** - repositories should be atomic CRUD operations
|
|
628
|
+
❌ **Don't skip dependency injection** - inject repositories into use cases via constructor
|
|
629
|
+
❌ **Don't create "god" use cases** - keep use cases focused on single business operations
|
|
630
|
+
❌ **Don't mix database queries with business logic** - separate into repository + use case
|
|
631
|
+
|
|
632
|
+
## Testing Patterns
|
|
633
|
+
|
|
634
|
+
### Handler Testing
|
|
635
|
+
```javascript
|
|
636
|
+
const { createHandler } = require('@friggframework/core/core');
|
|
637
|
+
|
|
638
|
+
const testHandler = createHandler({
|
|
639
|
+
isUserFacingResponse: false, // Get full errors in tests
|
|
640
|
+
shouldUseDatabase: false, // Mock/skip DB in tests
|
|
641
|
+
method: yourTestMethod
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// Test with mock event/context
|
|
645
|
+
const result = await testHandler(mockEvent, mockContext);
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Worker Testing
|
|
649
|
+
```javascript
|
|
650
|
+
class TestWorker extends Worker {
|
|
651
|
+
_validateParams(params) {
|
|
652
|
+
this._verifyParamExists(params, 'testField');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async _run(params, context) {
|
|
656
|
+
// Your test logic
|
|
657
|
+
return { processed: true };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Test SQS record processing
|
|
662
|
+
const worker = new TestWorker();
|
|
663
|
+
await worker.run({
|
|
664
|
+
Records: [{
|
|
665
|
+
body: JSON.stringify({ testField: 'value' })
|
|
666
|
+
}]
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
## Environment Variables
|
|
671
|
+
|
|
672
|
+
### Required Variables
|
|
673
|
+
- `AWS_REGION`: AWS region for SQS operations
|
|
674
|
+
- `SECRET_ARN`: (Optional) AWS Secrets Manager secret ARN for automatic injection
|
|
675
|
+
|
|
676
|
+
### Database Variables
|
|
677
|
+
- MongoDB connection variables (handled by `../database/mongo`)
|
|
678
|
+
- See database module documentation for complete list
|
|
679
|
+
|
|
680
|
+
### Queue Variables
|
|
681
|
+
- Queue URLs typically passed as parameters, not environment variables
|
|
682
|
+
- Use Worker's `getQueueURL()` method for dynamic queue discovery
|
|
683
|
+
|
|
684
|
+
## Security Considerations
|
|
685
|
+
|
|
686
|
+
- **Secrets**: Never log or expose secrets in error messages
|
|
687
|
+
- **Error Messages**: Always sanitize errors for user-facing responses
|
|
688
|
+
- **Database**: Connection pooling reuses connections securely
|
|
689
|
+
- **SQS**: Message validation prevents injection attacks
|
|
690
|
+
- **Logging**: Debug logs include sensitive data - handle carefully in production
|