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

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 (285) 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/dummy-integration-class.js +83 -0
  159. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  160. package/integrations/use-cases/create-integration.js +83 -0
  161. package/integrations/use-cases/create-process.js +128 -0
  162. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  163. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  164. package/integrations/use-cases/get-integration-for-user.js +78 -0
  165. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  166. package/integrations/use-cases/get-integration-instance.js +83 -0
  167. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  168. package/integrations/use-cases/get-possible-integrations.js +27 -0
  169. package/integrations/use-cases/get-process.js +87 -0
  170. package/integrations/use-cases/index.js +19 -0
  171. package/integrations/use-cases/load-integration-context.js +71 -0
  172. package/integrations/use-cases/update-integration-messages.js +44 -0
  173. package/integrations/use-cases/update-integration-status.js +32 -0
  174. package/integrations/use-cases/update-integration.js +93 -0
  175. package/integrations/use-cases/update-process-metrics.js +201 -0
  176. package/integrations/use-cases/update-process-state.js +119 -0
  177. package/integrations/utils/map-integration-dto.js +37 -0
  178. package/jest-global-setup-noop.js +3 -0
  179. package/jest-global-teardown-noop.js +3 -0
  180. package/logs/logger.js +0 -4
  181. package/{module-plugin → modules}/entity.js +1 -1
  182. package/{module-plugin → modules}/index.js +0 -8
  183. package/modules/module-factory.js +56 -0
  184. package/modules/module.js +221 -0
  185. package/modules/repositories/module-repository-documentdb.js +307 -0
  186. package/modules/repositories/module-repository-factory.js +40 -0
  187. package/modules/repositories/module-repository-interface.js +129 -0
  188. package/modules/repositories/module-repository-mongo.js +377 -0
  189. package/modules/repositories/module-repository-postgres.js +426 -0
  190. package/modules/repositories/module-repository.js +316 -0
  191. package/modules/requester/api-key.js +52 -0
  192. package/{module-plugin → modules}/requester/requester.js +1 -0
  193. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  194. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  195. package/modules/tests/doubles/test-module-factory.js +16 -0
  196. package/modules/tests/doubles/test-module-repository.js +39 -0
  197. package/modules/use-cases/get-entities-for-user.js +32 -0
  198. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  199. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  200. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  201. package/modules/use-cases/get-module.js +74 -0
  202. package/modules/use-cases/process-authorization-callback.js +133 -0
  203. package/modules/use-cases/refresh-entity-options.js +72 -0
  204. package/modules/use-cases/test-module-auth.js +72 -0
  205. package/modules/utils/map-module-dto.js +18 -0
  206. package/package.json +82 -50
  207. package/prisma-mongodb/schema.prisma +360 -0
  208. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  209. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  210. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  211. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  212. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  213. package/prisma-postgresql/schema.prisma +343 -0
  214. package/queues/queuer-util.js +27 -22
  215. package/syncs/manager.js +468 -443
  216. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  217. package/syncs/repositories/sync-repository-factory.js +43 -0
  218. package/syncs/repositories/sync-repository-interface.js +109 -0
  219. package/syncs/repositories/sync-repository-mongo.js +239 -0
  220. package/syncs/repositories/sync-repository-postgres.js +319 -0
  221. package/syncs/sync.js +0 -1
  222. package/token/repositories/token-repository-documentdb.js +137 -0
  223. package/token/repositories/token-repository-factory.js +40 -0
  224. package/token/repositories/token-repository-interface.js +131 -0
  225. package/token/repositories/token-repository-mongo.js +219 -0
  226. package/token/repositories/token-repository-postgres.js +264 -0
  227. package/token/repositories/token-repository.js +219 -0
  228. package/types/core/index.d.ts +2 -2
  229. package/types/integrations/index.d.ts +2 -6
  230. package/types/module-plugin/index.d.ts +5 -59
  231. package/types/syncs/index.d.ts +0 -2
  232. package/user/repositories/user-repository-documentdb.js +441 -0
  233. package/user/repositories/user-repository-factory.js +52 -0
  234. package/user/repositories/user-repository-interface.js +201 -0
  235. package/user/repositories/user-repository-mongo.js +308 -0
  236. package/user/repositories/user-repository-postgres.js +360 -0
  237. package/user/tests/doubles/test-user-repository.js +72 -0
  238. package/user/use-cases/authenticate-user.js +127 -0
  239. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  240. package/user/use-cases/create-individual-user.js +61 -0
  241. package/user/use-cases/create-organization-user.js +47 -0
  242. package/user/use-cases/create-token-for-user-id.js +30 -0
  243. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  244. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  245. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  246. package/user/use-cases/login-user.js +122 -0
  247. package/user/user.js +125 -0
  248. package/utils/backend-path.js +38 -0
  249. package/utils/index.js +6 -0
  250. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  251. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  252. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  253. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  254. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  255. package/websocket/repositories/websocket-connection-repository.js +161 -0
  256. package/database/models/State.js +0 -9
  257. package/database/models/Token.js +0 -70
  258. package/database/mongo.js +0 -45
  259. package/encrypt/Cryptor.test.js +0 -32
  260. package/encrypt/encrypt.js +0 -132
  261. package/encrypt/encrypt.test.js +0 -1069
  262. package/errors/base-error.test.js +0 -32
  263. package/errors/fetch-error.test.js +0 -79
  264. package/errors/halt-error.test.js +0 -11
  265. package/errors/validation-errors.test.js +0 -120
  266. package/integrations/create-frigg-backend.js +0 -31
  267. package/integrations/integration-factory.js +0 -251
  268. package/integrations/integration-mapping.js +0 -43
  269. package/integrations/integration-model.js +0 -46
  270. package/integrations/integration-user.js +0 -144
  271. package/integrations/test/integration-base.test.js +0 -144
  272. package/lambda/TimeoutCatcher.test.js +0 -68
  273. package/logs/logger.test.js +0 -76
  274. package/module-plugin/auther.js +0 -393
  275. package/module-plugin/credential.js +0 -22
  276. package/module-plugin/entity-manager.js +0 -70
  277. package/module-plugin/manager.js +0 -169
  278. package/module-plugin/module-factory.js +0 -61
  279. package/module-plugin/requester/api-key.js +0 -36
  280. package/module-plugin/requester/requester.test.js +0 -28
  281. package/module-plugin/test/auther.test.js +0 -97
  282. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  283. /package/{module-plugin → modules}/requester/basic.js +0 -0
  284. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  285. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,269 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ CredentialRepositoryInterface,
4
+ } = require('./credential-repository-interface');
5
+
6
+ /**
7
+ * MongoDB Credential Repository Adapter
8
+ * Handles OAuth credentials and API tokens persistence with MongoDB
9
+ *
10
+ * MongoDB-specific characteristics:
11
+ * - Uses String IDs (ObjectId)
12
+ * - No ID conversion needed (IDs are already strings)
13
+ * - Dynamic schema support via JSON field
14
+ */
15
+ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
16
+ constructor() {
17
+ super();
18
+ this.prisma = prisma;
19
+ }
20
+
21
+ /**
22
+ * Find credential by ID
23
+ * Replaces: Credential.findById(id)
24
+ *
25
+ * @param {string} id - Credential ID
26
+ * @returns {Promise<Object|null>} Credential object or null
27
+ */
28
+ async findCredentialById(id) {
29
+ const credential = await this.prisma.credential.findUnique({
30
+ where: { id },
31
+ });
32
+
33
+ if (!credential) {
34
+ return null;
35
+ }
36
+
37
+ // Extract data from JSON field
38
+ const data = credential.data || {};
39
+
40
+ return {
41
+ id: credential.id,
42
+ userId: credential.userId,
43
+ externalId: credential.externalId,
44
+ authIsValid: credential.authIsValid,
45
+ ...data, // Spread OAuth tokens from JSON field
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Update authentication status
51
+ * Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
52
+ *
53
+ * @param {string} credentialId - Credential ID
54
+ * @param {boolean} authIsValid - Authentication validity status
55
+ * @returns {Promise<Object>} Update result
56
+ */
57
+ async updateAuthenticationStatus(credentialId, authIsValid) {
58
+ await this.prisma.credential.update({
59
+ where: { id: credentialId },
60
+ data: { authIsValid },
61
+ });
62
+
63
+ return { acknowledged: true, modifiedCount: 1 };
64
+ }
65
+
66
+ /**
67
+ * Permanently remove a credential document
68
+ * Replaces: Credential.deleteOne({ _id: credentialId })
69
+ *
70
+ * @param {string} credentialId - Credential ID
71
+ * @returns {Promise<Object>} Deletion result
72
+ */
73
+ async deleteCredentialById(credentialId) {
74
+ try {
75
+ await this.prisma.credential.delete({
76
+ where: { id: credentialId },
77
+ });
78
+ return { acknowledged: true, deletedCount: 1 };
79
+ } catch (error) {
80
+ if (error.code === 'P2025') {
81
+ return { acknowledged: true, deletedCount: 0 };
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Create or update credential matching identifiers
89
+ * Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
90
+ *
91
+ * @param {{identifiers: Object, details: Object}} credentialDetails
92
+ * @returns {Promise<Object>} The persisted credential
93
+ */
94
+ async upsertCredential(credentialDetails) {
95
+ const { identifiers, details } = credentialDetails;
96
+ if (!identifiers)
97
+ throw new Error('identifiers required to upsert credential');
98
+
99
+ if (!identifiers.userId) {
100
+ throw new Error('userId required in identifiers');
101
+ }
102
+ if (!identifiers.externalId) {
103
+ throw new Error(
104
+ 'externalId required in identifiers to prevent credential collision. ' +
105
+ 'When multiple credentials exist for the same user, both userId and externalId ' +
106
+ 'are needed to uniquely identify which credential to update.'
107
+ );
108
+ }
109
+
110
+ const where = this._convertIdentifiersToWhere(identifiers);
111
+
112
+ const { authIsValid, ...oauthData } = details;
113
+
114
+ const existing = await this.prisma.credential.findFirst({ where });
115
+
116
+ if (existing) {
117
+ const mergedData = { ...(existing.data || {}), ...oauthData };
118
+
119
+ const updated = await this.prisma.credential.update({
120
+ where: { id: existing.id },
121
+ data: {
122
+ userId: existing.userId,
123
+ externalId: existing.externalId,
124
+ authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
125
+ data: mergedData,
126
+ },
127
+ });
128
+
129
+ return {
130
+ id: updated.id,
131
+ externalId: updated.externalId,
132
+ userId: updated.userId,
133
+ authIsValid: updated.authIsValid,
134
+ ...(updated.data || {}),
135
+ };
136
+ }
137
+
138
+ const created = await this.prisma.credential.create({
139
+ data: {
140
+ userId: identifiers.userId,
141
+ externalId: identifiers.externalId,
142
+ authIsValid: authIsValid,
143
+ data: oauthData,
144
+ },
145
+ });
146
+
147
+ return {
148
+ id: created.id,
149
+ externalId: created.externalId,
150
+ userId: created.userId,
151
+ authIsValid: created.authIsValid,
152
+ ...(created.data || {}),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Find a credential by filter criteria
158
+ * Replaces: Credential.findOne(query)
159
+ *
160
+ * @param {Object} filter
161
+ * @param {string} [filter.userId] - User ID
162
+ * @param {string} [filter.externalId] - External ID
163
+ * @param {string} [filter.credentialId] - Credential ID
164
+ * @returns {Promise<Object|null>} Credential object or null if not found
165
+ */
166
+ async findCredential(filter) {
167
+ const where = this._convertFilterToWhere(filter);
168
+
169
+ const credential = await this.prisma.credential.findFirst({
170
+ where,
171
+ });
172
+
173
+ if (!credential) {
174
+ return null;
175
+ }
176
+
177
+ const data = credential.data || {};
178
+
179
+ return {
180
+ id: credential.id,
181
+ userId: credential.userId,
182
+ externalId: credential.externalId,
183
+ authIsValid: credential.authIsValid,
184
+ access_token: data.access_token,
185
+ refresh_token: data.refresh_token,
186
+ ...data,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Update a credential by ID
192
+ * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
193
+ *
194
+ * @param {string} credentialId - Credential ID
195
+ * @param {Object} updates - Fields to update
196
+ * @returns {Promise<Object|null>} Updated credential object or null if not found
197
+ */
198
+ async updateCredential(credentialId, updates) {
199
+ const existing = await this.prisma.credential.findUnique({
200
+ where: { id: credentialId },
201
+ });
202
+
203
+ if (!existing) {
204
+ return null;
205
+ }
206
+
207
+ const { authIsValid, ...oauthData } = updates;
208
+
209
+ const mergedData = { ...(existing.data || {}), ...oauthData };
210
+
211
+ const updated = await this.prisma.credential.update({
212
+ where: { id: credentialId },
213
+ data: {
214
+ userId: existing.userId,
215
+ externalId: existing.externalId,
216
+ authIsValid: authIsValid,
217
+ data: mergedData,
218
+ },
219
+ });
220
+
221
+ const data = updated.data || {};
222
+
223
+ return {
224
+ id: updated.id,
225
+ userId: updated.userId,
226
+ externalId: updated.externalId,
227
+ authIsValid: updated.authIsValid,
228
+ access_token: data.access_token,
229
+ refresh_token: data.refresh_token,
230
+ ...data,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Convert identifiers to Prisma where clause
236
+ * @private
237
+ * @param {Object} identifiers - Identifier fields
238
+ * @returns {Object} Prisma where clause
239
+ */
240
+ _convertIdentifiersToWhere(identifiers) {
241
+ const where = {};
242
+
243
+ if (identifiers._id) where.id = identifiers._id;
244
+ if (identifiers.id) where.id = identifiers.id;
245
+ if (identifiers.userId) where.userId = identifiers.userId;
246
+ if (identifiers.externalId) where.externalId = identifiers.externalId;
247
+
248
+ return where;
249
+ }
250
+
251
+ /**
252
+ * Convert filter to Prisma where clause
253
+ * @private
254
+ * @param {Object} filter - Filter criteria
255
+ * @returns {Object} Prisma where clause
256
+ */
257
+ _convertFilterToWhere(filter) {
258
+ const where = {};
259
+
260
+ if (filter.credentialId) where.id = filter.credentialId;
261
+ if (filter.id) where.id = filter.id;
262
+ if (filter.userId) where.userId = filter.userId;
263
+ if (filter.externalId) where.externalId = filter.externalId;
264
+
265
+ return where;
266
+ }
267
+ }
268
+
269
+ module.exports = { CredentialRepositoryMongo };
@@ -0,0 +1,291 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ CredentialRepositoryInterface,
4
+ } = require('./credential-repository-interface');
5
+
6
+ /**
7
+ * PostgreSQL Credential Repository Adapter
8
+ * Handles OAuth credentials and API tokens persistence with PostgreSQL
9
+ *
10
+ * PostgreSQL-specific characteristics:
11
+ * - Uses Int IDs with autoincrement
12
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
13
+ * - All returned IDs are converted to strings for application layer consistency
14
+ */
15
+ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
16
+ constructor() {
17
+ super();
18
+ this.prisma = prisma;
19
+ }
20
+
21
+ /**
22
+ * Convert string ID to integer for PostgreSQL queries
23
+ * @private
24
+ * @param {string|number|null|undefined} id - ID to convert
25
+ * @returns {number|null|undefined} Integer ID or null/undefined
26
+ * @throws {Error} If ID cannot be converted to integer
27
+ */
28
+ _convertId(id) {
29
+ if (id === null || id === undefined) return id;
30
+ const parsed = parseInt(id, 10);
31
+ if (isNaN(parsed)) {
32
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
33
+ }
34
+ return parsed;
35
+ }
36
+
37
+ /**
38
+ * Find credential by ID
39
+ *
40
+ * @param {string} id - Credential ID (string from application layer)
41
+ * @returns {Promise<Object|null>} Credential object with string IDs or null
42
+ */
43
+ async findCredentialById(id) {
44
+ const intId = this._convertId(id);
45
+ const credential = await this.prisma.credential.findUnique({
46
+ where: { id: intId },
47
+ });
48
+
49
+ if (!credential) {
50
+ return null;
51
+ }
52
+
53
+ const data = credential.data || {};
54
+
55
+ return {
56
+ id: credential.id.toString(),
57
+ userId: credential.userId.toString(),
58
+ externalId: credential.externalId,
59
+ authIsValid: credential.authIsValid,
60
+ ...data, // Spread OAuth tokens from JSON field
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Update authentication status
66
+ *
67
+ * @param {string} credentialId - Credential ID (string from application layer)
68
+ * @param {boolean} authIsValid - Authentication validity status
69
+ * @returns {Promise<Object>} Update result
70
+ */
71
+ async updateAuthenticationStatus(credentialId, authIsValid) {
72
+ const intId = this._convertId(credentialId);
73
+ await this.prisma.credential.update({
74
+ where: { id: intId },
75
+ data: { authIsValid },
76
+ });
77
+
78
+ return { acknowledged: true, modifiedCount: 1 };
79
+ }
80
+
81
+ /**
82
+ * Permanently remove a credential document
83
+ *
84
+ * @param {string} credentialId - Credential ID (string from application layer)
85
+ * @returns {Promise<Object>} Deletion result
86
+ */
87
+ async deleteCredentialById(credentialId) {
88
+ try {
89
+ const intId = this._convertId(credentialId);
90
+ await this.prisma.credential.delete({
91
+ where: { id: intId },
92
+ });
93
+ return { acknowledged: true, deletedCount: 1 };
94
+ } catch (error) {
95
+ if (error.code === 'P2025') {
96
+ // Record not found
97
+ return { acknowledged: true, deletedCount: 0 };
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Create or update credential matching identifiers
105
+ *
106
+ * @param {{identifiers: Object, details: Object}} credentialDetails
107
+ * @returns {Promise<Object>} The persisted credential with string IDs
108
+ */
109
+ async upsertCredential(credentialDetails) {
110
+ const { identifiers, details } = credentialDetails;
111
+ if (!identifiers)
112
+ throw new Error('identifiers required to upsert credential');
113
+
114
+ // Support both userId (preferred) and user (legacy) for backward compatibility
115
+ if (!identifiers.userId && !identifiers.user) {
116
+ throw new Error('userId required in identifiers');
117
+ }
118
+ if (!identifiers.externalId) {
119
+ throw new Error(
120
+ 'externalId required in identifiers to prevent credential collision. ' +
121
+ 'When multiple credentials exist for the same user, both userId and externalId ' +
122
+ 'are needed to uniquely identify which credential to update.'
123
+ );
124
+ }
125
+
126
+ const where = this._convertIdentifiersToWhere(identifiers);
127
+
128
+ const { externalId } = identifiers;
129
+
130
+ const { authIsValid, ...oauthData } = details;
131
+
132
+ const existing = await this.prisma.credential.findFirst({ where });
133
+
134
+ if (existing) {
135
+ const mergedData = { ...(existing.data || {}), ...oauthData };
136
+
137
+ const updated = await this.prisma.credential.update({
138
+ where: { id: existing.id },
139
+ data: {
140
+ userId: this._convertId(existing.userId),
141
+ externalId: existing.externalId,
142
+ authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
143
+ data: mergedData,
144
+ },
145
+ });
146
+
147
+ return {
148
+ id: updated.id.toString(),
149
+ externalId: updated.externalId,
150
+ userId: updated.userId?.toString(),
151
+ authIsValid: updated.authIsValid,
152
+ ...(updated.data || {}),
153
+ };
154
+ }
155
+
156
+ const created = await this.prisma.credential.create({
157
+ data: {
158
+ // Use userId from where clause (supports both userId and user fields)
159
+ userId: where.userId,
160
+ externalId,
161
+ authIsValid: authIsValid,
162
+ data: oauthData,
163
+ },
164
+ });
165
+
166
+ return {
167
+ id: created.id.toString(),
168
+ externalId: created.externalId,
169
+ userId: created.userId?.toString(),
170
+ authIsValid: created.authIsValid,
171
+ ...(created.data || {}),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Find a credential by filter criteria
177
+ *
178
+ * @param {Object} filter
179
+ * @param {string} [filter.userId] - User ID (string from application layer)
180
+ * @param {string} [filter.externalId] - External ID
181
+ * @param {string} [filter.credentialId] - Credential ID (string from application layer)
182
+ * @returns {Promise<Object|null>} Credential object with string IDs or null if not found
183
+ */
184
+ async findCredential(filter) {
185
+ const where = this._convertFilterToWhere(filter);
186
+
187
+ const credential = await this.prisma.credential.findFirst({
188
+ where,
189
+ });
190
+
191
+ if (!credential) {
192
+ return null;
193
+ }
194
+
195
+ const data = credential.data || {};
196
+
197
+ return {
198
+ id: credential.id.toString(),
199
+ userId: credential.userId?.toString(),
200
+ externalId: credential.externalId,
201
+ authIsValid: credential.authIsValid,
202
+ access_token: data.access_token,
203
+ refresh_token: data.refresh_token,
204
+ ...data,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Update a credential by ID
210
+ *
211
+ * @param {string} credentialId - Credential ID (string from application layer)
212
+ * @param {Object} updates - Fields to update
213
+ * @returns {Promise<Object|null>} Updated credential object with string IDs or null if not found
214
+ */
215
+ async updateCredential(credentialId, updates) {
216
+ const intId = this._convertId(credentialId);
217
+ const existing = await this.prisma.credential.findUnique({
218
+ where: { id: intId },
219
+ });
220
+
221
+ if (!existing) {
222
+ return null;
223
+ }
224
+
225
+ const { authIsValid, ...oauthData } = updates;
226
+
227
+ const mergedData = { ...(existing.data || {}), ...oauthData };
228
+
229
+ const updated = await this.prisma.credential.update({
230
+ where: { id: intId },
231
+ data: {
232
+ userId: this._convertId(existing.userId),
233
+ externalId: existing.externalId,
234
+ authIsValid: authIsValid,
235
+ data: mergedData,
236
+ },
237
+ });
238
+
239
+ const data = updated.data || {};
240
+
241
+ return {
242
+ id: updated.id.toString(),
243
+ userId: updated.userId?.toString(),
244
+ externalId: updated.externalId,
245
+ authIsValid: updated.authIsValid,
246
+ access_token: data.access_token,
247
+ refresh_token: data.refresh_token,
248
+ ...data,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Convert identifiers to Prisma where clause (converting IDs to Int)
254
+ * @private
255
+ * @param {Object} identifiers - Identifier fields
256
+ * @returns {Object} Prisma where clause with Int IDs
257
+ */
258
+ _convertIdentifiersToWhere(identifiers) {
259
+ const where = {};
260
+
261
+ if (identifiers.id) where.id = this._convertId(identifiers.id);
262
+ // Support both userId (preferred) and user (legacy) for backward compatibility
263
+ if (identifiers.userId)
264
+ where.userId = this._convertId(identifiers.userId);
265
+ else if (identifiers.user)
266
+ where.userId = this._convertId(identifiers.user);
267
+ if (identifiers.externalId) where.externalId = identifiers.externalId;
268
+
269
+ return where;
270
+ }
271
+
272
+ /**
273
+ * Convert filter to Prisma where clause (converting IDs to Int)
274
+ * @private
275
+ * @param {Object} filter - Filter criteria
276
+ * @returns {Object} Prisma where clause with Int IDs
277
+ */
278
+ _convertFilterToWhere(filter) {
279
+ const where = {};
280
+
281
+ if (filter.credentialId)
282
+ where.id = this._convertId(filter.credentialId);
283
+ if (filter.id) where.id = this._convertId(filter.id);
284
+ if (filter.userId) where.userId = this._convertId(filter.userId);
285
+ if (filter.externalId) where.externalId = filter.externalId;
286
+
287
+ return where;
288
+ }
289
+ }
290
+
291
+ module.exports = { CredentialRepositoryPostgres };