@friggframework/core 2.0.0-next.41 → 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.
Files changed (196) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +25 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +121 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +321 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  134. package/prisma-postgresql/schema.prisma +303 -0
  135. package/syncs/manager.js +468 -443
  136. package/syncs/repositories/sync-repository-factory.js +38 -0
  137. package/syncs/repositories/sync-repository-interface.js +109 -0
  138. package/syncs/repositories/sync-repository-mongo.js +239 -0
  139. package/syncs/repositories/sync-repository-postgres.js +319 -0
  140. package/syncs/sync.js +0 -1
  141. package/token/repositories/token-repository-factory.js +33 -0
  142. package/token/repositories/token-repository-interface.js +131 -0
  143. package/token/repositories/token-repository-mongo.js +212 -0
  144. package/token/repositories/token-repository-postgres.js +257 -0
  145. package/token/repositories/token-repository.js +219 -0
  146. package/types/integrations/index.d.ts +2 -6
  147. package/types/module-plugin/index.d.ts +5 -57
  148. package/types/syncs/index.d.ts +0 -2
  149. package/user/repositories/user-repository-factory.js +46 -0
  150. package/user/repositories/user-repository-interface.js +198 -0
  151. package/user/repositories/user-repository-mongo.js +250 -0
  152. package/user/repositories/user-repository-postgres.js +311 -0
  153. package/user/tests/doubles/test-user-repository.js +72 -0
  154. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  155. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  156. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  157. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  158. package/user/tests/use-cases/login-user.test.js +140 -0
  159. package/user/use-cases/create-individual-user.js +61 -0
  160. package/user/use-cases/create-organization-user.js +47 -0
  161. package/user/use-cases/create-token-for-user-id.js +30 -0
  162. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  163. package/user/use-cases/login-user.js +122 -0
  164. package/user/user.js +77 -0
  165. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  166. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  167. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  168. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  169. package/websocket/repositories/websocket-connection-repository.js +160 -0
  170. package/database/models/State.js +0 -9
  171. package/database/models/Token.js +0 -70
  172. package/database/mongo.js +0 -171
  173. package/encrypt/Cryptor.test.js +0 -32
  174. package/encrypt/encrypt.js +0 -104
  175. package/encrypt/encrypt.test.js +0 -1069
  176. package/handlers/routers/middleware/loadUser.js +0 -15
  177. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  178. package/integrations/create-frigg-backend.js +0 -31
  179. package/integrations/integration-factory.js +0 -251
  180. package/integrations/integration-mapping.js +0 -43
  181. package/integrations/integration-model.js +0 -46
  182. package/integrations/integration-user.js +0 -144
  183. package/integrations/test/integration-base.test.js +0 -144
  184. package/module-plugin/auther.js +0 -393
  185. package/module-plugin/credential.js +0 -22
  186. package/module-plugin/entity-manager.js +0 -70
  187. package/module-plugin/manager.js +0 -169
  188. package/module-plugin/module-factory.js +0 -61
  189. package/module-plugin/test/auther.test.js +0 -97
  190. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  191. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  192. /package/{module-plugin → modules}/requester/basic.js +0 -0
  193. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  194. /package/{module-plugin → modules}/requester/requester.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  196. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,421 @@
1
+ # Frigg Commands - Application Service Layer
2
+
3
+ ## Overview
4
+
5
+ Frigg Commands provide a clean, stable application service layer for all database operations in the Frigg Integration Framework. They abstract away the underlying ORM (currently Mongoose) and provide a consistent API for managing users, credentials, entities, and integrations.
6
+
7
+ ## Why Use Commands?
8
+
9
+ ### 1. **ORM Independence**
10
+ 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
+ ### 2. **Hexagonal Architecture**
13
+ Commands act as the **application service layer** in hexagonal architecture:
14
+ - **Domain Layer**: Your use cases and business logic
15
+ - **Application Layer**: Frigg Commands (this layer)
16
+ - **Infrastructure Layer**: Repositories and database models (hidden from you)
17
+
18
+ ### 3. **Single Source of Truth**
19
+ All database operations flow through commands, making it easier to:
20
+ - Add caching, logging, or monitoring
21
+ - Enforce data validation rules
22
+ - Maintain consistent error handling
23
+ - Track data access patterns
24
+
25
+ ### 4. **Future-Proof**
26
+ When Frigg upgrades its internals, commands maintain backward compatibility. Your integration code continues working without changes.
27
+
28
+ ## Installation
29
+
30
+ Commands are available through the `@friggframework/core` package:
31
+
32
+ ```javascript
33
+ const { createFriggCommands } = require('@friggframework/core');
34
+ ```
35
+
36
+ ## Basic Usage
37
+
38
+ ### Initialize Commands
39
+
40
+ ```javascript
41
+ const { createFriggCommands } = require('@friggframework/core');
42
+ const MyIntegration = require('./MyIntegration');
43
+
44
+ // Create command set with your integration class
45
+ const commands = createFriggCommands({
46
+ integrationClass: MyIntegration
47
+ });
48
+ ```
49
+
50
+ ### Use Commands in Your Integration
51
+
52
+ ```javascript
53
+ class MyIntegration extends IntegrationBase {
54
+ constructor() {
55
+ super();
56
+ this.commands = createFriggCommands({
57
+ integrationClass: MyIntegration
58
+ });
59
+ }
60
+
61
+ async hydrateFromExternalUser(externalUserId) {
62
+ // Find integration context by external entity ID
63
+ const result = await this.commands.findIntegrationContextByExternalEntityId(
64
+ externalUserId
65
+ );
66
+
67
+ if (result.error) {
68
+ return { error: result.error };
69
+ }
70
+
71
+ // Hydrate integration with retrieved context
72
+ this.setIntegrationRecord(result.context);
73
+ return { record: this.record };
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Use Commands in Use Cases
79
+
80
+ ```javascript
81
+ const { createFriggCommands } = require('@friggframework/core');
82
+
83
+ class AuthenticateUserUseCase {
84
+ constructor({ commands } = {}) {
85
+ // Accept injected commands for testing, or create default
86
+ this.commands = commands || createFriggCommands({
87
+ integrationClass: MyIntegration
88
+ });
89
+ }
90
+
91
+ async execute({ appUserId, username, email }) {
92
+ // Find or create user
93
+ let user = await this.commands.findUserByAppUserId(appUserId);
94
+
95
+ if (!user) {
96
+ user = await this.commands.createUser({
97
+ appUserId,
98
+ username,
99
+ email
100
+ });
101
+ }
102
+
103
+ return user;
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## Available Commands
109
+
110
+ ### User Commands
111
+
112
+ Manage Frigg users (individuals or organizations using your integration).
113
+
114
+ ```javascript
115
+ // Create a new user
116
+ const user = await commands.createUser({
117
+ username: 'john@example.com',
118
+ email: 'john@example.com',
119
+ appUserId: 'external-user-123',
120
+ password: 'optional-password' // For password-based auth
121
+ });
122
+
123
+ // Find user by app-specific user ID
124
+ const user = await commands.findUserByAppUserId('external-user-123');
125
+
126
+ // Find user by username
127
+ const user = await commands.findUserByUsername('john@example.com');
128
+
129
+ // Find user by Frigg internal ID
130
+ const user = await commands.findUserById('frigg-user-id');
131
+
132
+ // Update user
133
+ const updatedUser = await commands.updateUser('frigg-user-id', {
134
+ email: 'newemail@example.com'
135
+ });
136
+ ```
137
+
138
+ ### Credential Commands
139
+
140
+ Manage OAuth tokens and API credentials.
141
+
142
+ ```javascript
143
+ // Create credential
144
+ const credential = await commands.createCredential({
145
+ userId: 'frigg-user-id',
146
+ externalId: 'oauth-user-id',
147
+ access_token: 'access_token_value',
148
+ refresh_token: 'refresh_token_value',
149
+ expires_at: new Date('2024-12-31'),
150
+ moduleName: 'asana',
151
+ authIsValid: true
152
+ });
153
+
154
+ // Find credential
155
+ const credential = await commands.findCredential({
156
+ userId: 'frigg-user-id',
157
+ moduleName: 'asana'
158
+ });
159
+
160
+ // Update credential (e.g., after token refresh)
161
+ const updated = await commands.updateCredential('credential-id', {
162
+ access_token: 'new_access_token',
163
+ expires_at: new Date('2025-01-31')
164
+ });
165
+
166
+ // Delete credential
167
+ await commands.deleteCredential('credential-id');
168
+ ```
169
+
170
+ ### Entity Commands
171
+
172
+ Manage module entities (connections to external services).
173
+
174
+ ```javascript
175
+ // Create entity
176
+ const entity = await commands.createEntity({
177
+ userId: 'frigg-user-id',
178
+ externalId: 'asana-workspace-123',
179
+ name: 'My Workspace',
180
+ moduleName: 'asana',
181
+ credentialId: 'credential-id'
182
+ });
183
+
184
+ // Find single entity
185
+ const entity = await commands.findEntity({
186
+ userId: 'frigg-user-id',
187
+ externalId: 'asana-workspace-123',
188
+ moduleName: 'asana'
189
+ });
190
+
191
+ // Find entity by ID
192
+ const entity = await commands.findEntityById('entity-id');
193
+
194
+ // Find all entities for user
195
+ const entities = await commands.findEntitiesByUserId('frigg-user-id');
196
+
197
+ // Find entities by module
198
+ const asanaEntities = await commands.findEntitiesByUserIdAndModuleName(
199
+ 'frigg-user-id',
200
+ 'asana'
201
+ );
202
+
203
+ // Find multiple entities by IDs
204
+ const entities = await commands.findEntitiesByIds(['entity-id-1', 'entity-id-2']);
205
+
206
+ // Update entity
207
+ const updated = await commands.updateEntity('entity-id', {
208
+ name: 'Updated Workspace Name'
209
+ });
210
+
211
+ // Delete entity
212
+ await commands.deleteEntity('entity-id');
213
+ ```
214
+
215
+ ### Integration Commands
216
+
217
+ Manage integration records and load full integration contexts.
218
+
219
+ ```javascript
220
+ // Find integration context by external entity ID
221
+ // Returns { context, error } where context includes record + hydrated modules
222
+ const result = await commands.findIntegrationContextByExternalEntityId(
223
+ 'external-user-or-workspace-id'
224
+ );
225
+
226
+ if (!result.error) {
227
+ integration.setIntegrationRecord(result.context);
228
+ }
229
+
230
+ // Load integration context by integration ID
231
+ const result = await commands.loadIntegrationContextById('integration-id');
232
+
233
+ if (!result.error) {
234
+ integration.setIntegrationRecord(result.context);
235
+ }
236
+ ```
237
+
238
+ ## Architecture Principles
239
+
240
+ ### Dependency Injection for Testing
241
+
242
+ Commands support dependency injection for testing:
243
+
244
+ ```javascript
245
+ // Production code - uses real repositories
246
+ const commands = createFriggCommands({ integrationClass: MyIntegration });
247
+
248
+ // Test code - inject mocks
249
+ const mockCommands = {
250
+ createUser: jest.fn().mockResolvedValue({ id: 'user-123' }),
251
+ findUserByAppUserId: jest.fn().mockResolvedValue(null)
252
+ };
253
+
254
+ const useCase = new MyUseCase({ commands: mockCommands });
255
+ ```
256
+
257
+ ### Integration vs Unit Testing
258
+
259
+ **Commands are designed for integration testing** - they use real repositories by default:
260
+
261
+ ```javascript
262
+ // ❌ Don't do this - commands always use real repositories
263
+ const commands = createFriggCommands({
264
+ userRepository: mockUserRepo // This parameter doesn't exist
265
+ });
266
+
267
+ // ✅ Do this - inject mocked commands into your use cases
268
+ const useCase = new MyUseCase({
269
+ commands: mockCommands
270
+ });
271
+ ```
272
+
273
+ ### Error Handling
274
+
275
+ Commands return domain objects directly. Handle errors at the use case level:
276
+
277
+ ```javascript
278
+ try {
279
+ const user = await commands.createUser({ username, email });
280
+ return { success: true, user };
281
+ } catch (error) {
282
+ // Handle database errors
283
+ return { success: false, error: error.message };
284
+ }
285
+ ```
286
+
287
+ For integration context operations, errors are returned in the result:
288
+
289
+ ```javascript
290
+ const result = await commands.findIntegrationContextByExternalEntityId(userId);
291
+
292
+ if (result.error) {
293
+ return { error: result.error };
294
+ }
295
+
296
+ // Use result.context
297
+ ```
298
+
299
+ ## Migration Guide
300
+
301
+ ### From Direct Model Access
302
+
303
+ **Before (❌ Don't do this):**
304
+ ```javascript
305
+ const { User } = require('@friggframework/core');
306
+
307
+ const user = await User.findOne({ appUserId: '123' });
308
+ ```
309
+
310
+ **After (✅ Do this):**
311
+ ```javascript
312
+ const { createFriggCommands } = require('@friggframework/core');
313
+
314
+ const commands = createFriggCommands({ integrationClass: MyIntegration });
315
+ const user = await commands.findUserByAppUserId('123');
316
+ ```
317
+
318
+ ### From IntegrationRepository (Backend Pattern)
319
+
320
+ **Before (❌ Old pattern):**
321
+ ```javascript
322
+ const { IntegrationRepository } = require('./repositories/IntegrationRepository');
323
+
324
+ this.integrationRepository = new IntegrationRepository(MyIntegration);
325
+ const result = await this.integrationRepository.loadIntegrationRecordByAsanaUser(userId);
326
+ ```
327
+
328
+ **After (✅ New pattern):**
329
+ ```javascript
330
+ const { createFriggCommands } = require('@friggframework/core');
331
+
332
+ this.commands = createFriggCommands({ integrationClass: MyIntegration });
333
+ const result = await this.commands.findIntegrationContextByExternalEntityId(userId);
334
+ ```
335
+
336
+ ## Best Practices
337
+
338
+ ### 1. Create Commands Once
339
+ Initialize commands in your constructor:
340
+
341
+ ```javascript
342
+ class MyIntegration extends IntegrationBase {
343
+ constructor() {
344
+ super();
345
+ this.commands = createFriggCommands({ integrationClass: MyIntegration });
346
+ }
347
+ }
348
+ ```
349
+
350
+ ### 2. Pass Commands to Use Cases
351
+ Use dependency injection for testability:
352
+
353
+ ```javascript
354
+ class MyUseCase {
355
+ constructor({ commands } = {}) {
356
+ this.commands = commands || createFriggCommands({
357
+ integrationClass: MyIntegration
358
+ });
359
+ }
360
+ }
361
+ ```
362
+
363
+ ### 3. Use Specific Finders
364
+ Use the most specific finder method:
365
+
366
+ ```javascript
367
+ // ✅ Good - specific finder
368
+ const user = await commands.findUserByAppUserId('123');
369
+
370
+ // ❌ Less efficient - generic finder
371
+ const user = await commands.findUser({ appUserId: '123' });
372
+ ```
373
+
374
+ ### 4. Handle Null Returns
375
+ Most finders return `null` if not found:
376
+
377
+ ```javascript
378
+ const user = await commands.findUserByAppUserId('123');
379
+
380
+ if (!user) {
381
+ // Handle user not found
382
+ user = await commands.createUser({ ... });
383
+ }
384
+ ```
385
+
386
+ ## Command Reference
387
+
388
+ | Category | Command | Description |
389
+ |----------|---------|-------------|
390
+ | **User** | `createUser(data)` | Create new Frigg user |
391
+ | | `findUserByAppUserId(appUserId)` | Find by external app user ID |
392
+ | | `findUserByUsername(username)` | Find by username |
393
+ | | `findUserById(id)` | Find by Frigg user ID |
394
+ | | `updateUser(id, updates)` | Update user properties |
395
+ | **Credential** | `createCredential(data)` | Create OAuth credential |
396
+ | | `findCredential(filter)` | Find credential by filter |
397
+ | | `updateCredential(id, updates)` | Update credential (token refresh) |
398
+ | | `deleteCredential(id)` | Delete credential |
399
+ | **Entity** | `createEntity(data)` | Create module entity |
400
+ | | `findEntity(filter)` | Find entity by filter |
401
+ | | `findEntityById(id)` | Find by entity ID |
402
+ | | `findEntitiesByUserId(userId)` | Find all user entities |
403
+ | | `findEntitiesByUserIdAndModuleName(userId, moduleName)` | Find user entities for module |
404
+ | | `findEntitiesByIds(ids)` | Find multiple by IDs |
405
+ | | `updateEntity(id, updates)` | Update entity properties |
406
+ | | `deleteEntity(id)` | Delete entity |
407
+ | **Integration** | `findIntegrationContextByExternalEntityId(externalId)` | Load integration + modules by external ID |
408
+ | | `loadIntegrationContextById(integrationId)` | Load integration + modules by ID |
409
+
410
+ ## Support
411
+
412
+ For questions or issues with commands:
413
+ 1. Check this README
414
+ 2. Review the main Frigg documentation
415
+ 3. Open an issue on the Frigg Framework repository
416
+
417
+ ## Related Documentation
418
+
419
+ - [Frigg Framework Overview](../../README.md)
420
+ - [Integration Development Guide](../../docs/integration-guide.md)
421
+ - [Hexagonal Architecture](../../docs/architecture.md)
@@ -0,0 +1,224 @@
1
+ const {
2
+ createCredentialRepository,
3
+ } = require('../../credential/repositories/credential-repository-factory');
4
+
5
+ const ERROR_CODE_MAP = {
6
+ CREDENTIAL_NOT_FOUND: 404,
7
+ INVALID_CREDENTIAL_DATA: 400,
8
+ };
9
+
10
+ function mapErrorToResponse(error) {
11
+ const status = ERROR_CODE_MAP[error?.code] || 500;
12
+ return {
13
+ error: status,
14
+ reason: error?.message,
15
+ code: error?.code,
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Create credential command factory
21
+ *
22
+ * NOTE: This is an internal API. Integration developers should use createFriggCommands() instead.
23
+ *
24
+ * @returns {Object} Credential command object with CRUD operations
25
+ */
26
+ function createCredentialCommands() {
27
+ const credRepo = createCredentialRepository();
28
+
29
+ return {
30
+ /**
31
+ * Create a new credential
32
+ * @param {Object} params
33
+ * @param {string} params.userId - User ID who owns this credential
34
+ * @param {string} params.externalId - External identifier from the API module
35
+ * @param {string} params.access_token - OAuth access token
36
+ * @param {string} [params.refresh_token] - OAuth refresh token
37
+ * @param {string} [params.domain] - Domain for the credential
38
+ * @param {boolean} [params.authIsValid=true] - Whether authentication is valid
39
+ * @returns {Promise<Object>} Created credential object
40
+ */
41
+ async createCredential({
42
+ userId,
43
+ externalId,
44
+ access_token,
45
+ refresh_token,
46
+ domain,
47
+ authIsValid = true,
48
+ } = {}) {
49
+ try {
50
+ if (!userId || !externalId || !access_token) {
51
+ const error = new Error(
52
+ 'userId, externalId, and access_token are required'
53
+ );
54
+ error.code = 'INVALID_CREDENTIAL_DATA';
55
+ throw error;
56
+ }
57
+
58
+ const credentialData = {
59
+ identifiers: { user: userId, externalId },
60
+ details: {
61
+ access_token,
62
+ authIsValid,
63
+ },
64
+ };
65
+
66
+ if (refresh_token) {
67
+ credentialData.details.refresh_token = refresh_token;
68
+ }
69
+ if (domain) {
70
+ credentialData.details.domain = domain;
71
+ }
72
+
73
+ const credential = await credRepo.upsertCredential(
74
+ credentialData
75
+ );
76
+
77
+ return {
78
+ id: credential.id,
79
+ userId: credential.userId,
80
+ externalId: credential.externalId,
81
+ access_token: credential.access_token,
82
+ refresh_token: credential.refresh_token,
83
+ authIsValid: credential.authIsValid,
84
+ };
85
+ } catch (error) {
86
+ return mapErrorToResponse(error);
87
+ }
88
+ },
89
+
90
+ /**
91
+ * Find a credential by filter criteria
92
+ * @param {Object} filter
93
+ * @param {string} [filter.userId] - User ID to search for
94
+ * @param {string} [filter.externalId] - External ID to search for
95
+ * @param {string} [filter.credentialId] - Credential ID to search for
96
+ * @returns {Promise<Object|null>} Credential object or null if not found
97
+ */
98
+ async findCredential(filter = {}) {
99
+ try {
100
+ if (
101
+ !filter.userId &&
102
+ !filter.externalId &&
103
+ !filter.credentialId
104
+ ) {
105
+ const error = new Error(
106
+ 'At least one filter criterion is required'
107
+ );
108
+ error.code = 'INVALID_CREDENTIAL_DATA';
109
+ throw error;
110
+ }
111
+
112
+ const credential = await credRepo.findCredential(filter);
113
+
114
+ if (!credential) {
115
+ return null;
116
+ }
117
+
118
+ return {
119
+ id: credential.id,
120
+ userId: credential.userId,
121
+ externalId: credential.externalId,
122
+ access_token: credential.access_token,
123
+ refresh_token: credential.refresh_token,
124
+ authIsValid: credential.authIsValid,
125
+ domain: credential.domain,
126
+ };
127
+ } catch (error) {
128
+ return mapErrorToResponse(error);
129
+ }
130
+ },
131
+
132
+ /**
133
+ * Update a credential by ID
134
+ * @param {string} credentialId - Credential ID to update
135
+ * @param {Object} updates - Fields to update
136
+ * @returns {Promise<Object>} Updated credential object
137
+ */
138
+ async updateCredential(credentialId, updates) {
139
+ try {
140
+ if (!credentialId) {
141
+ const error = new Error('credentialId is required');
142
+ error.code = 'INVALID_CREDENTIAL_DATA';
143
+ throw error;
144
+ }
145
+
146
+ const credential = await credRepo.updateCredential(
147
+ credentialId,
148
+ updates
149
+ );
150
+
151
+ if (!credential) {
152
+ const error = new Error(
153
+ `Credential ${credentialId} not found`
154
+ );
155
+ error.code = 'CREDENTIAL_NOT_FOUND';
156
+ throw error;
157
+ }
158
+
159
+ return {
160
+ id: credential.id,
161
+ userId: credential.userId,
162
+ externalId: credential.externalId,
163
+ access_token: credential.access_token,
164
+ refresh_token: credential.refresh_token,
165
+ authIsValid: credential.authIsValid,
166
+ domain: credential.domain,
167
+ };
168
+ } catch (error) {
169
+ return mapErrorToResponse(error);
170
+ }
171
+ },
172
+
173
+ /**
174
+ * Update authentication status for a credential
175
+ * @param {string} credentialId - Credential ID to update
176
+ * @param {boolean} isValid - Whether authentication is valid
177
+ * @returns {Promise<Object>} Result object with success flag
178
+ */
179
+ async updateAuthenticationStatus(credentialId, isValid) {
180
+ try {
181
+ if (!credentialId) {
182
+ const error = new Error('credentialId is required');
183
+ error.code = 'INVALID_CREDENTIAL_DATA';
184
+ throw error;
185
+ }
186
+
187
+ await credRepo.updateAuthenticationStatus(
188
+ credentialId,
189
+ isValid
190
+ );
191
+
192
+ return { success: true };
193
+ } catch (error) {
194
+ return mapErrorToResponse(error);
195
+ }
196
+ },
197
+
198
+ /**
199
+ * Delete a credential by ID
200
+ * @param {string} credentialId - Credential ID to delete
201
+ * @returns {Promise<Object>} Result object with success flag
202
+ */
203
+ async deleteCredential(credentialId) {
204
+ try {
205
+ if (!credentialId) {
206
+ const error = new Error('credentialId is required');
207
+ error.code = 'INVALID_CREDENTIAL_DATA';
208
+ throw error;
209
+ }
210
+
211
+ await credRepo.deleteCredentialById(credentialId);
212
+
213
+ return { success: true };
214
+ } catch (error) {
215
+ return mapErrorToResponse(error);
216
+ }
217
+ },
218
+ };
219
+ }
220
+
221
+ module.exports = {
222
+ createCredentialCommands,
223
+ ERROR_CODE_MAP,
224
+ };