@friggframework/core 2.0.0-next.57 → 2.0.0-next.59
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/application/commands/README.md +90 -60
- package/application/commands/user-commands.js +68 -5
- package/database/encryption/documentdb-encryption-service.md +1537 -1232
- package/package.json +5 -5
- package/user/repositories/user-repository-documentdb.js +0 -11
- package/user/repositories/user-repository-factory.js +2 -1
- package/user/repositories/user-repository-interface.js +0 -11
- package/user/repositories/user-repository-mongo.js +0 -11
- package/user/repositories/user-repository-postgres.js +0 -13
|
@@ -7,22 +7,28 @@ Frigg Commands provide a clean, stable application service layer for all databas
|
|
|
7
7
|
## Why Use Commands?
|
|
8
8
|
|
|
9
9
|
### 1. **ORM Independence**
|
|
10
|
+
|
|
10
11
|
Commands isolate your integration code from the underlying database implementation. This allows Frigg to migrate between ORMs (e.g., Mongoose to Prisma) without breaking your integration code.
|
|
11
12
|
|
|
12
13
|
### 2. **Hexagonal Architecture**
|
|
14
|
+
|
|
13
15
|
Commands act as the **application service layer** in hexagonal architecture:
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
16
|
+
|
|
17
|
+
- **Domain Layer**: Your use cases and business logic
|
|
18
|
+
- **Application Layer**: Frigg Commands (this layer)
|
|
19
|
+
- **Infrastructure Layer**: Repositories and database models (hidden from you)
|
|
17
20
|
|
|
18
21
|
### 3. **Single Source of Truth**
|
|
22
|
+
|
|
19
23
|
All database operations flow through commands, making it easier to:
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
+
|
|
25
|
+
- Add caching, logging, or monitoring
|
|
26
|
+
- Enforce data validation rules
|
|
27
|
+
- Maintain consistent error handling
|
|
28
|
+
- Track data access patterns
|
|
24
29
|
|
|
25
30
|
### 4. **Future-Proof**
|
|
31
|
+
|
|
26
32
|
When Frigg upgrades its internals, commands maintain backward compatibility. Your integration code continues working without changes.
|
|
27
33
|
|
|
28
34
|
## Installation
|
|
@@ -43,7 +49,7 @@ const MyIntegration = require('./MyIntegration');
|
|
|
43
49
|
|
|
44
50
|
// Create command set with your integration class
|
|
45
51
|
const commands = createFriggCommands({
|
|
46
|
-
integrationClass: MyIntegration
|
|
52
|
+
integrationClass: MyIntegration,
|
|
47
53
|
});
|
|
48
54
|
```
|
|
49
55
|
|
|
@@ -54,15 +60,16 @@ class MyIntegration extends IntegrationBase {
|
|
|
54
60
|
constructor() {
|
|
55
61
|
super();
|
|
56
62
|
this.commands = createFriggCommands({
|
|
57
|
-
integrationClass: MyIntegration
|
|
63
|
+
integrationClass: MyIntegration,
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
async hydrateFromExternalUser(externalUserId) {
|
|
62
68
|
// Find integration context by external entity ID
|
|
63
|
-
const result =
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
const result =
|
|
70
|
+
await this.commands.findIntegrationContextByExternalEntityId(
|
|
71
|
+
externalUserId
|
|
72
|
+
);
|
|
66
73
|
|
|
67
74
|
if (result.error) {
|
|
68
75
|
return { error: result.error };
|
|
@@ -83,9 +90,11 @@ const { createFriggCommands } = require('@friggframework/core');
|
|
|
83
90
|
class AuthenticateUserUseCase {
|
|
84
91
|
constructor({ commands } = {}) {
|
|
85
92
|
// Accept injected commands for testing, or create default
|
|
86
|
-
this.commands =
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
this.commands =
|
|
94
|
+
commands ||
|
|
95
|
+
createFriggCommands({
|
|
96
|
+
integrationClass: MyIntegration,
|
|
97
|
+
});
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
async execute({ appUserId, username, email }) {
|
|
@@ -96,7 +105,7 @@ class AuthenticateUserUseCase {
|
|
|
96
105
|
user = await this.commands.createUser({
|
|
97
106
|
appUserId,
|
|
98
107
|
username,
|
|
99
|
-
email
|
|
108
|
+
email,
|
|
100
109
|
});
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -117,7 +126,7 @@ const user = await commands.createUser({
|
|
|
117
126
|
username: 'john@example.com',
|
|
118
127
|
email: 'john@example.com',
|
|
119
128
|
appUserId: 'external-user-123',
|
|
120
|
-
password: 'optional-password' // For password-based auth
|
|
129
|
+
password: 'optional-password', // For password-based auth
|
|
121
130
|
});
|
|
122
131
|
|
|
123
132
|
// Find user by app-specific user ID
|
|
@@ -127,11 +136,11 @@ const user = await commands.findUserByAppUserId('external-user-123');
|
|
|
127
136
|
const user = await commands.findUserByUsername('john@example.com');
|
|
128
137
|
|
|
129
138
|
// Find user by Frigg internal ID
|
|
130
|
-
const user = await commands.
|
|
139
|
+
const user = await commands.findIndividualUserById('frigg-user-id');
|
|
131
140
|
|
|
132
141
|
// Update user
|
|
133
142
|
const updatedUser = await commands.updateUser('frigg-user-id', {
|
|
134
|
-
email: 'newemail@example.com'
|
|
143
|
+
email: 'newemail@example.com',
|
|
135
144
|
});
|
|
136
145
|
```
|
|
137
146
|
|
|
@@ -148,19 +157,19 @@ const credential = await commands.createCredential({
|
|
|
148
157
|
refresh_token: 'refresh_token_value',
|
|
149
158
|
expires_at: new Date('2024-12-31'),
|
|
150
159
|
moduleName: 'asana',
|
|
151
|
-
authIsValid: true
|
|
160
|
+
authIsValid: true,
|
|
152
161
|
});
|
|
153
162
|
|
|
154
163
|
// Find credential
|
|
155
164
|
const credential = await commands.findCredential({
|
|
156
165
|
userId: 'frigg-user-id',
|
|
157
|
-
moduleName: 'asana'
|
|
166
|
+
moduleName: 'asana',
|
|
158
167
|
});
|
|
159
168
|
|
|
160
169
|
// Update credential (e.g., after token refresh)
|
|
161
170
|
const updated = await commands.updateCredential('credential-id', {
|
|
162
171
|
access_token: 'new_access_token',
|
|
163
|
-
expires_at: new Date('2025-01-31')
|
|
172
|
+
expires_at: new Date('2025-01-31'),
|
|
164
173
|
});
|
|
165
174
|
|
|
166
175
|
// Delete credential
|
|
@@ -178,14 +187,14 @@ const entity = await commands.createEntity({
|
|
|
178
187
|
externalId: 'asana-workspace-123',
|
|
179
188
|
name: 'My Workspace',
|
|
180
189
|
moduleName: 'asana',
|
|
181
|
-
credentialId: 'credential-id'
|
|
190
|
+
credentialId: 'credential-id',
|
|
182
191
|
});
|
|
183
192
|
|
|
184
193
|
// Find single entity
|
|
185
194
|
const entity = await commands.findEntity({
|
|
186
195
|
userId: 'frigg-user-id',
|
|
187
196
|
externalId: 'asana-workspace-123',
|
|
188
|
-
moduleName: 'asana'
|
|
197
|
+
moduleName: 'asana',
|
|
189
198
|
});
|
|
190
199
|
|
|
191
200
|
// Find entity by ID
|
|
@@ -201,11 +210,14 @@ const asanaEntities = await commands.findEntitiesByUserIdAndModuleName(
|
|
|
201
210
|
);
|
|
202
211
|
|
|
203
212
|
// Find multiple entities by IDs
|
|
204
|
-
const entities = await commands.findEntitiesByIds([
|
|
213
|
+
const entities = await commands.findEntitiesByIds([
|
|
214
|
+
'entity-id-1',
|
|
215
|
+
'entity-id-2',
|
|
216
|
+
]);
|
|
205
217
|
|
|
206
218
|
// Update entity
|
|
207
219
|
const updated = await commands.updateEntity('entity-id', {
|
|
208
|
-
name: 'Updated Workspace Name'
|
|
220
|
+
name: 'Updated Workspace Name',
|
|
209
221
|
});
|
|
210
222
|
|
|
211
223
|
// Delete entity
|
|
@@ -248,7 +260,7 @@ const commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
|
248
260
|
// Test code - inject mocks
|
|
249
261
|
const mockCommands = {
|
|
250
262
|
createUser: jest.fn().mockResolvedValue({ id: 'user-123' }),
|
|
251
|
-
findUserByAppUserId: jest.fn().mockResolvedValue(null)
|
|
263
|
+
findUserByAppUserId: jest.fn().mockResolvedValue(null),
|
|
252
264
|
};
|
|
253
265
|
|
|
254
266
|
const useCase = new MyUseCase({ commands: mockCommands });
|
|
@@ -261,12 +273,12 @@ const useCase = new MyUseCase({ commands: mockCommands });
|
|
|
261
273
|
```javascript
|
|
262
274
|
// ❌ Don't do this - commands always use real repositories
|
|
263
275
|
const commands = createFriggCommands({
|
|
264
|
-
userRepository: mockUserRepo
|
|
276
|
+
userRepository: mockUserRepo, // This parameter doesn't exist
|
|
265
277
|
});
|
|
266
278
|
|
|
267
279
|
// ✅ Do this - inject mocked commands into your use cases
|
|
268
280
|
const useCase = new MyUseCase({
|
|
269
|
-
commands: mockCommands
|
|
281
|
+
commands: mockCommands,
|
|
270
282
|
});
|
|
271
283
|
```
|
|
272
284
|
|
|
@@ -301,6 +313,7 @@ if (result.error) {
|
|
|
301
313
|
### From Direct Model Access
|
|
302
314
|
|
|
303
315
|
**Before (❌ Don't do this):**
|
|
316
|
+
|
|
304
317
|
```javascript
|
|
305
318
|
const { User } = require('@friggframework/core');
|
|
306
319
|
|
|
@@ -308,6 +321,7 @@ const user = await User.findOne({ appUserId: '123' });
|
|
|
308
321
|
```
|
|
309
322
|
|
|
310
323
|
**After (✅ Do this):**
|
|
324
|
+
|
|
311
325
|
```javascript
|
|
312
326
|
const { createFriggCommands } = require('@friggframework/core');
|
|
313
327
|
|
|
@@ -318,49 +332,63 @@ const user = await commands.findUserByAppUserId('123');
|
|
|
318
332
|
### From IntegrationRepository (Backend Pattern)
|
|
319
333
|
|
|
320
334
|
**Before (❌ Old pattern):**
|
|
335
|
+
|
|
321
336
|
```javascript
|
|
322
|
-
const {
|
|
337
|
+
const {
|
|
338
|
+
IntegrationRepository,
|
|
339
|
+
} = require('./repositories/IntegrationRepository');
|
|
323
340
|
|
|
324
341
|
this.integrationRepository = new IntegrationRepository(MyIntegration);
|
|
325
|
-
const result =
|
|
342
|
+
const result =
|
|
343
|
+
await this.integrationRepository.loadIntegrationRecordByAsanaUser(userId);
|
|
326
344
|
```
|
|
327
345
|
|
|
328
346
|
**After (✅ New pattern):**
|
|
347
|
+
|
|
329
348
|
```javascript
|
|
330
349
|
const { createFriggCommands } = require('@friggframework/core');
|
|
331
350
|
|
|
332
351
|
this.commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
333
|
-
const result = await this.commands.findIntegrationContextByExternalEntityId(
|
|
352
|
+
const result = await this.commands.findIntegrationContextByExternalEntityId(
|
|
353
|
+
userId
|
|
354
|
+
);
|
|
334
355
|
```
|
|
335
356
|
|
|
336
357
|
## Best Practices
|
|
337
358
|
|
|
338
359
|
### 1. Create Commands Once
|
|
360
|
+
|
|
339
361
|
Initialize commands in your constructor:
|
|
340
362
|
|
|
341
363
|
```javascript
|
|
342
364
|
class MyIntegration extends IntegrationBase {
|
|
343
365
|
constructor() {
|
|
344
366
|
super();
|
|
345
|
-
this.commands = createFriggCommands({
|
|
367
|
+
this.commands = createFriggCommands({
|
|
368
|
+
integrationClass: MyIntegration,
|
|
369
|
+
});
|
|
346
370
|
}
|
|
347
371
|
}
|
|
348
372
|
```
|
|
349
373
|
|
|
350
374
|
### 2. Pass Commands to Use Cases
|
|
375
|
+
|
|
351
376
|
Use dependency injection for testability:
|
|
352
377
|
|
|
353
378
|
```javascript
|
|
354
379
|
class MyUseCase {
|
|
355
380
|
constructor({ commands } = {}) {
|
|
356
|
-
this.commands =
|
|
357
|
-
|
|
358
|
-
|
|
381
|
+
this.commands =
|
|
382
|
+
commands ||
|
|
383
|
+
createFriggCommands({
|
|
384
|
+
integrationClass: MyIntegration,
|
|
385
|
+
});
|
|
359
386
|
}
|
|
360
387
|
}
|
|
361
388
|
```
|
|
362
389
|
|
|
363
390
|
### 3. Use Specific Finders
|
|
391
|
+
|
|
364
392
|
Use the most specific finder method:
|
|
365
393
|
|
|
366
394
|
```javascript
|
|
@@ -372,6 +400,7 @@ const user = await commands.findUser({ appUserId: '123' });
|
|
|
372
400
|
```
|
|
373
401
|
|
|
374
402
|
### 4. Handle Null Returns
|
|
403
|
+
|
|
375
404
|
Most finders return `null` if not found:
|
|
376
405
|
|
|
377
406
|
```javascript
|
|
@@ -385,37 +414,38 @@ if (!user) {
|
|
|
385
414
|
|
|
386
415
|
## Command Reference
|
|
387
416
|
|
|
388
|
-
| Category
|
|
389
|
-
|
|
390
|
-
| **User**
|
|
391
|
-
|
|
|
392
|
-
|
|
|
393
|
-
|
|
|
394
|
-
|
|
|
395
|
-
| **Credential**
|
|
396
|
-
|
|
|
397
|
-
|
|
|
398
|
-
|
|
|
399
|
-
| **Entity**
|
|
400
|
-
|
|
|
401
|
-
|
|
|
402
|
-
|
|
|
403
|
-
|
|
|
404
|
-
|
|
|
405
|
-
|
|
|
406
|
-
|
|
|
407
|
-
| **Integration** | `findIntegrationContextByExternalEntityId(externalId)`
|
|
408
|
-
|
|
|
417
|
+
| Category | Command | Description |
|
|
418
|
+
| --------------- | ------------------------------------------------------- | ----------------------------------------- |
|
|
419
|
+
| **User** | `createUser(data)` | Create new Frigg user |
|
|
420
|
+
| | `findUserByAppUserId(appUserId)` | Find by external app user ID |
|
|
421
|
+
| | `findUserByUsername(username)` | Find by username |
|
|
422
|
+
| | `findIndividualUserById(id)` | Find by Frigg user ID |
|
|
423
|
+
| | `updateUser(id, updates)` | Update user properties |
|
|
424
|
+
| **Credential** | `createCredential(data)` | Create OAuth credential |
|
|
425
|
+
| | `findCredential(filter)` | Find credential by filter |
|
|
426
|
+
| | `updateCredential(id, updates)` | Update credential (token refresh) |
|
|
427
|
+
| | `deleteCredential(id)` | Delete credential |
|
|
428
|
+
| **Entity** | `createEntity(data)` | Create module entity |
|
|
429
|
+
| | `findEntity(filter)` | Find entity by filter |
|
|
430
|
+
| | `findEntityById(id)` | Find by entity ID |
|
|
431
|
+
| | `findEntitiesByUserId(userId)` | Find all user entities |
|
|
432
|
+
| | `findEntitiesByUserIdAndModuleName(userId, moduleName)` | Find user entities for module |
|
|
433
|
+
| | `findEntitiesByIds(ids)` | Find multiple by IDs |
|
|
434
|
+
| | `updateEntity(id, updates)` | Update entity properties |
|
|
435
|
+
| | `deleteEntity(id)` | Delete entity |
|
|
436
|
+
| **Integration** | `findIntegrationContextByExternalEntityId(externalId)` | Load integration + modules by external ID |
|
|
437
|
+
| | `loadIntegrationContextById(integrationId)` | Load integration + modules by ID |
|
|
409
438
|
|
|
410
439
|
## Support
|
|
411
440
|
|
|
412
441
|
For questions or issues with commands:
|
|
442
|
+
|
|
413
443
|
1. Check this README
|
|
414
444
|
2. Review the main Frigg documentation
|
|
415
445
|
3. Open an issue on the Frigg Framework repository
|
|
416
446
|
|
|
417
447
|
## Related Documentation
|
|
418
448
|
|
|
419
|
-
-
|
|
420
|
-
-
|
|
421
|
-
-
|
|
449
|
+
- [Frigg Framework Overview](../../README.md)
|
|
450
|
+
- [Integration Development Guide](../../docs/integration-guide.md)
|
|
451
|
+
- [Hexagonal Architecture](../../docs/architecture.md)
|
|
@@ -138,11 +138,11 @@ function createUserCommands() {
|
|
|
138
138
|
},
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Find
|
|
142
|
-
* @param {string} userId -
|
|
143
|
-
* @returns {Promise<Object|null>}
|
|
141
|
+
* Find an individual user by their ID
|
|
142
|
+
* @param {string} userId - Individual user ID to search for
|
|
143
|
+
* @returns {Promise<Object|null>} Individual user object or null if not found
|
|
144
144
|
*/
|
|
145
|
-
async
|
|
145
|
+
async findIndividualUserById(userId) {
|
|
146
146
|
try {
|
|
147
147
|
if (!userId) {
|
|
148
148
|
const error = new Error('userId is required');
|
|
@@ -159,7 +159,7 @@ function createUserCommands() {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
return {
|
|
162
|
-
id: user._id
|
|
162
|
+
id: user._id?.toString() || user.id,
|
|
163
163
|
username: user.username,
|
|
164
164
|
email: user.email,
|
|
165
165
|
appUserId: user.appUserId,
|
|
@@ -169,6 +169,37 @@ function createUserCommands() {
|
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Find an organization user by their ID
|
|
174
|
+
* @param {string} userId - Organization user ID to search for
|
|
175
|
+
* @returns {Promise<Object|null>} Organization user object or null if not found
|
|
176
|
+
*/
|
|
177
|
+
async findOrganizationUserById(userId) {
|
|
178
|
+
try {
|
|
179
|
+
if (!userId) {
|
|
180
|
+
const error = new Error('userId is required');
|
|
181
|
+
error.code = 'INVALID_USER_DATA';
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const user = await userRepository.findOrganizationUserById(
|
|
186
|
+
userId
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!user) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
id: user.id,
|
|
195
|
+
appOrgId: user.appOrgId,
|
|
196
|
+
name: user.name,
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return mapErrorToResponse(error);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
|
|
172
203
|
/**
|
|
173
204
|
* Update a user by ID
|
|
174
205
|
* @param {string} userId - User ID to update
|
|
@@ -204,6 +235,38 @@ function createUserCommands() {
|
|
|
204
235
|
return mapErrorToResponse(error);
|
|
205
236
|
}
|
|
206
237
|
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Delete a user by ID
|
|
241
|
+
* Cascades to all related records (credentials, entities, integrations, etc.)
|
|
242
|
+
* @param {string} userId - User ID to delete
|
|
243
|
+
* @returns {Promise<Object>} Deletion result
|
|
244
|
+
*/
|
|
245
|
+
async deleteUserById(userId) {
|
|
246
|
+
try {
|
|
247
|
+
if (!userId) {
|
|
248
|
+
const error = new Error('userId is required');
|
|
249
|
+
error.code = 'INVALID_USER_DATA';
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const deleted = await userRepository.deleteUser(userId);
|
|
254
|
+
|
|
255
|
+
if (!deleted) {
|
|
256
|
+
const error = new Error(`User ${userId} not found`);
|
|
257
|
+
error.code = 'USER_NOT_FOUND';
|
|
258
|
+
return mapErrorToResponse(error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
userId,
|
|
264
|
+
message: 'User and all related data deleted successfully',
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return mapErrorToResponse(error);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
207
270
|
};
|
|
208
271
|
}
|
|
209
272
|
|