@friggframework/core 2.0.0-next.6 → 2.0.0-next.61

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