@friggframework/core 2.0.0-next.5 → 2.0.0-next.51

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 (267) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +959 -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 +179 -0
  7. package/application/commands/user-commands.js +213 -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 +2 -7
  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 +307 -0
  15. package/credential/repositories/credential-repository-postgres.js +313 -0
  16. package/credential/repositories/credential-repository.js +302 -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/MONGODB_TRANSACTION_FIX.md +198 -0
  20. package/database/adapters/lambda-invoker.js +97 -0
  21. package/database/config.js +154 -0
  22. package/database/encryption/README.md +684 -0
  23. package/database/encryption/encryption-schema-registry.js +141 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/logger.js +79 -0
  26. package/database/encryption/prisma-encryption-extension.js +222 -0
  27. package/database/index.js +25 -12
  28. package/database/models/WebsocketConnection.js +16 -10
  29. package/database/models/readme.md +1 -0
  30. package/database/prisma.js +222 -0
  31. package/database/repositories/health-check-repository-factory.js +43 -0
  32. package/database/repositories/health-check-repository-interface.js +87 -0
  33. package/database/repositories/health-check-repository-mongodb.js +91 -0
  34. package/database/repositories/health-check-repository-postgres.js +82 -0
  35. package/database/repositories/health-check-repository.js +108 -0
  36. package/database/repositories/migration-status-repository-s3.js +137 -0
  37. package/database/use-cases/check-database-health-use-case.js +29 -0
  38. package/database/use-cases/check-database-state-use-case.js +81 -0
  39. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  40. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  41. package/database/use-cases/get-migration-status-use-case.js +93 -0
  42. package/database/use-cases/run-database-migration-use-case.js +137 -0
  43. package/database/use-cases/test-encryption-use-case.js +253 -0
  44. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  45. package/database/utils/mongodb-collection-utils.js +91 -0
  46. package/database/utils/mongodb-schema-init.js +106 -0
  47. package/database/utils/prisma-runner.js +400 -0
  48. package/database/utils/prisma-schema-parser.js +182 -0
  49. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  50. package/encrypt/Cryptor.js +34 -168
  51. package/encrypt/index.js +1 -2
  52. package/encrypt/test-encrypt.js +0 -2
  53. package/generated/prisma-mongodb/client.d.ts +1 -0
  54. package/generated/prisma-mongodb/client.js +4 -0
  55. package/generated/prisma-mongodb/default.d.ts +1 -0
  56. package/generated/prisma-mongodb/default.js +4 -0
  57. package/generated/prisma-mongodb/edge.d.ts +1 -0
  58. package/generated/prisma-mongodb/edge.js +334 -0
  59. package/generated/prisma-mongodb/index-browser.js +316 -0
  60. package/generated/prisma-mongodb/index.d.ts +22898 -0
  61. package/generated/prisma-mongodb/index.js +359 -0
  62. package/generated/prisma-mongodb/package.json +183 -0
  63. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  64. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  65. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  66. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  67. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  68. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  69. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  70. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  71. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  72. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  73. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  74. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  75. package/generated/prisma-mongodb/schema.prisma +362 -0
  76. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  77. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  78. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  79. package/generated/prisma-mongodb/wasm.js +341 -0
  80. package/generated/prisma-postgresql/client.d.ts +1 -0
  81. package/generated/prisma-postgresql/client.js +4 -0
  82. package/generated/prisma-postgresql/default.d.ts +1 -0
  83. package/generated/prisma-postgresql/default.js +4 -0
  84. package/generated/prisma-postgresql/edge.d.ts +1 -0
  85. package/generated/prisma-postgresql/edge.js +356 -0
  86. package/generated/prisma-postgresql/index-browser.js +338 -0
  87. package/generated/prisma-postgresql/index.d.ts +25072 -0
  88. package/generated/prisma-postgresql/index.js +381 -0
  89. package/generated/prisma-postgresql/package.json +183 -0
  90. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  91. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  92. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  93. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  94. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  95. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  96. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  97. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  98. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  99. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  100. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  101. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  102. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  103. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  104. package/generated/prisma-postgresql/schema.prisma +345 -0
  105. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  106. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  107. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  108. package/generated/prisma-postgresql/wasm.js +363 -0
  109. package/handlers/WEBHOOKS.md +653 -0
  110. package/handlers/app-definition-loader.js +38 -0
  111. package/handlers/app-handler-helpers.js +56 -0
  112. package/handlers/backend-utils.js +180 -0
  113. package/handlers/database-migration-handler.js +227 -0
  114. package/handlers/integration-event-dispatcher.js +54 -0
  115. package/handlers/routers/HEALTHCHECK.md +342 -0
  116. package/handlers/routers/auth.js +15 -0
  117. package/handlers/routers/db-migration.handler.js +29 -0
  118. package/handlers/routers/db-migration.js +256 -0
  119. package/handlers/routers/health.js +519 -0
  120. package/handlers/routers/integration-defined-routers.js +45 -0
  121. package/handlers/routers/integration-webhook-routers.js +67 -0
  122. package/handlers/routers/user.js +63 -0
  123. package/handlers/routers/websocket.js +57 -0
  124. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  125. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  126. package/handlers/workers/db-migration.js +352 -0
  127. package/handlers/workers/integration-defined-workers.js +27 -0
  128. package/index.js +77 -22
  129. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  130. package/integrations/index.js +12 -10
  131. package/integrations/integration-base.js +296 -54
  132. package/integrations/integration-router.js +381 -182
  133. package/integrations/options.js +1 -1
  134. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  135. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  136. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  137. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  138. package/integrations/repositories/integration-mapping-repository.js +156 -0
  139. package/integrations/repositories/integration-repository-factory.js +44 -0
  140. package/integrations/repositories/integration-repository-interface.js +127 -0
  141. package/integrations/repositories/integration-repository-mongo.js +303 -0
  142. package/integrations/repositories/integration-repository-postgres.js +352 -0
  143. package/integrations/repositories/process-repository-factory.js +46 -0
  144. package/integrations/repositories/process-repository-interface.js +90 -0
  145. package/integrations/repositories/process-repository-mongo.js +190 -0
  146. package/integrations/repositories/process-repository-postgres.js +217 -0
  147. package/integrations/tests/doubles/dummy-integration-class.js +83 -0
  148. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  149. package/integrations/use-cases/create-integration.js +83 -0
  150. package/integrations/use-cases/create-process.js +128 -0
  151. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  152. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  153. package/integrations/use-cases/get-integration-for-user.js +78 -0
  154. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  155. package/integrations/use-cases/get-integration-instance.js +83 -0
  156. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  157. package/integrations/use-cases/get-possible-integrations.js +27 -0
  158. package/integrations/use-cases/get-process.js +87 -0
  159. package/integrations/use-cases/index.js +19 -0
  160. package/integrations/use-cases/load-integration-context.js +71 -0
  161. package/integrations/use-cases/update-integration-messages.js +44 -0
  162. package/integrations/use-cases/update-integration-status.js +32 -0
  163. package/integrations/use-cases/update-integration.js +93 -0
  164. package/integrations/use-cases/update-process-metrics.js +201 -0
  165. package/integrations/use-cases/update-process-state.js +119 -0
  166. package/integrations/utils/map-integration-dto.js +37 -0
  167. package/jest-global-setup-noop.js +3 -0
  168. package/jest-global-teardown-noop.js +3 -0
  169. package/logs/logger.js +0 -4
  170. package/{module-plugin → modules}/entity.js +1 -1
  171. package/{module-plugin → modules}/index.js +0 -8
  172. package/modules/module-factory.js +56 -0
  173. package/modules/module.js +221 -0
  174. package/modules/repositories/module-repository-factory.js +33 -0
  175. package/modules/repositories/module-repository-interface.js +129 -0
  176. package/modules/repositories/module-repository-mongo.js +377 -0
  177. package/modules/repositories/module-repository-postgres.js +426 -0
  178. package/modules/repositories/module-repository.js +316 -0
  179. package/{module-plugin → modules}/requester/requester.js +1 -0
  180. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  181. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  182. package/modules/tests/doubles/test-module-factory.js +16 -0
  183. package/modules/tests/doubles/test-module-repository.js +39 -0
  184. package/modules/use-cases/get-entities-for-user.js +32 -0
  185. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  186. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  187. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  188. package/modules/use-cases/get-module.js +55 -0
  189. package/modules/use-cases/process-authorization-callback.js +122 -0
  190. package/modules/use-cases/refresh-entity-options.js +59 -0
  191. package/modules/use-cases/test-module-auth.js +55 -0
  192. package/modules/utils/map-module-dto.js +18 -0
  193. package/package.json +82 -50
  194. package/prisma-mongodb/schema.prisma +362 -0
  195. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  196. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  197. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  198. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  199. package/prisma-postgresql/schema.prisma +345 -0
  200. package/queues/queuer-util.js +28 -15
  201. package/syncs/manager.js +468 -443
  202. package/syncs/repositories/sync-repository-factory.js +38 -0
  203. package/syncs/repositories/sync-repository-interface.js +109 -0
  204. package/syncs/repositories/sync-repository-mongo.js +239 -0
  205. package/syncs/repositories/sync-repository-postgres.js +319 -0
  206. package/syncs/sync.js +0 -1
  207. package/token/repositories/token-repository-factory.js +33 -0
  208. package/token/repositories/token-repository-interface.js +131 -0
  209. package/token/repositories/token-repository-mongo.js +212 -0
  210. package/token/repositories/token-repository-postgres.js +257 -0
  211. package/token/repositories/token-repository.js +219 -0
  212. package/types/core/index.d.ts +2 -2
  213. package/types/integrations/index.d.ts +2 -6
  214. package/types/module-plugin/index.d.ts +5 -59
  215. package/types/syncs/index.d.ts +0 -2
  216. package/user/repositories/user-repository-factory.js +46 -0
  217. package/user/repositories/user-repository-interface.js +198 -0
  218. package/user/repositories/user-repository-mongo.js +291 -0
  219. package/user/repositories/user-repository-postgres.js +350 -0
  220. package/user/tests/doubles/test-user-repository.js +72 -0
  221. package/user/use-cases/authenticate-user.js +127 -0
  222. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  223. package/user/use-cases/create-individual-user.js +61 -0
  224. package/user/use-cases/create-organization-user.js +47 -0
  225. package/user/use-cases/create-token-for-user-id.js +30 -0
  226. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  227. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  228. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  229. package/user/use-cases/login-user.js +122 -0
  230. package/user/user.js +93 -0
  231. package/utils/backend-path.js +38 -0
  232. package/utils/index.js +6 -0
  233. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  234. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  235. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  236. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  237. package/websocket/repositories/websocket-connection-repository.js +161 -0
  238. package/database/models/State.js +0 -9
  239. package/database/models/Token.js +0 -70
  240. package/database/mongo.js +0 -45
  241. package/encrypt/Cryptor.test.js +0 -32
  242. package/encrypt/encrypt.js +0 -132
  243. package/encrypt/encrypt.test.js +0 -1069
  244. package/errors/base-error.test.js +0 -32
  245. package/errors/fetch-error.test.js +0 -79
  246. package/errors/halt-error.test.js +0 -11
  247. package/errors/validation-errors.test.js +0 -120
  248. package/integrations/create-frigg-backend.js +0 -31
  249. package/integrations/integration-factory.js +0 -251
  250. package/integrations/integration-mapping.js +0 -43
  251. package/integrations/integration-model.js +0 -46
  252. package/integrations/integration-user.js +0 -144
  253. package/integrations/test/integration-base.test.js +0 -144
  254. package/lambda/TimeoutCatcher.test.js +0 -68
  255. package/logs/logger.test.js +0 -76
  256. package/module-plugin/auther.js +0 -393
  257. package/module-plugin/credential.js +0 -22
  258. package/module-plugin/entity-manager.js +0 -70
  259. package/module-plugin/manager.js +0 -169
  260. package/module-plugin/module-factory.js +0 -61
  261. package/module-plugin/requester/requester.test.js +0 -28
  262. package/module-plugin/test/auther.test.js +0 -97
  263. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  264. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  265. /package/{module-plugin → modules}/requester/basic.js +0 -0
  266. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  267. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,313 @@
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
+ * Replaces: Credential.findById(id)
40
+ *
41
+ * @param {string} id - Credential ID (string from application layer)
42
+ * @returns {Promise<Object|null>} Credential object with string IDs or null
43
+ */
44
+ async findCredentialById(id) {
45
+ const intId = this._convertId(id);
46
+ const credential = await this.prisma.credential.findUnique({
47
+ where: { id: intId },
48
+ });
49
+
50
+ if (!credential) {
51
+ return null;
52
+ }
53
+
54
+ // Extract data from JSON field
55
+ const data = credential.data || {};
56
+
57
+ return {
58
+ _id: credential.id.toString(),
59
+ id: credential.id.toString(),
60
+ user: credential.userId?.toString(),
61
+ userId: credential.userId?.toString(),
62
+ externalId: credential.externalId,
63
+ authIsValid: credential.authIsValid,
64
+ ...data, // Spread OAuth tokens from JSON field
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Update authentication status
70
+ * Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
71
+ *
72
+ * @param {string} credentialId - Credential ID (string from application layer)
73
+ * @param {boolean} authIsValid - Authentication validity status
74
+ * @returns {Promise<Object>} Update result
75
+ */
76
+ async updateAuthenticationStatus(credentialId, authIsValid) {
77
+ const intId = this._convertId(credentialId);
78
+ await this.prisma.credential.update({
79
+ where: { id: intId },
80
+ data: { authIsValid },
81
+ });
82
+
83
+ return { acknowledged: true, modifiedCount: 1 };
84
+ }
85
+
86
+ /**
87
+ * Permanently remove a credential document
88
+ * Replaces: Credential.deleteOne({ _id: credentialId })
89
+ *
90
+ * @param {string} credentialId - Credential ID (string from application layer)
91
+ * @returns {Promise<Object>} Deletion result
92
+ */
93
+ async deleteCredentialById(credentialId) {
94
+ try {
95
+ const intId = this._convertId(credentialId);
96
+ await this.prisma.credential.delete({
97
+ where: { id: intId },
98
+ });
99
+ return { acknowledged: true, deletedCount: 1 };
100
+ } catch (error) {
101
+ if (error.code === 'P2025') {
102
+ // Record not found
103
+ return { acknowledged: true, deletedCount: 0 };
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Create or update credential matching identifiers
111
+ * Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
112
+ *
113
+ * @param {{identifiers: Object, details: Object}} credentialDetails
114
+ * @returns {Promise<Object>} The persisted credential with string IDs
115
+ */
116
+ async upsertCredential(credentialDetails) {
117
+ const { identifiers, details } = credentialDetails;
118
+ if (!identifiers)
119
+ throw new Error('identifiers required to upsert credential');
120
+
121
+ if (!identifiers.user && !identifiers.userId) {
122
+ throw new Error('user or userId required in identifiers');
123
+ }
124
+ if (!identifiers.externalId) {
125
+ throw new Error(
126
+ 'externalId required in identifiers to prevent credential collision. ' +
127
+ 'When multiple credentials exist for the same user, both userId and externalId ' +
128
+ 'are needed to uniquely identify which credential to update.'
129
+ );
130
+ }
131
+
132
+ const where = this._convertIdentifiersToWhere(identifiers);
133
+
134
+ const { user, externalId } = identifiers;
135
+
136
+ // Separate schema fields from dynamic OAuth data
137
+ const { authIsValid, ...oauthData } = details;
138
+
139
+ const existing = await this.prisma.credential.findFirst({ where });
140
+
141
+ if (existing) {
142
+ const mergedData = { ...(existing.data || {}), ...oauthData };
143
+
144
+ const updated = await this.prisma.credential.update({
145
+ where: { id: existing.id },
146
+ data: {
147
+ userId: this._convertId(user || existing.userId),
148
+ externalId:
149
+ externalId !== undefined
150
+ ? externalId
151
+ : existing.externalId,
152
+ authIsValid:
153
+ authIsValid !== undefined
154
+ ? authIsValid
155
+ : existing.authIsValid,
156
+ data: mergedData,
157
+ },
158
+ });
159
+
160
+ return {
161
+ id: updated.id.toString(),
162
+ externalId: updated.externalId,
163
+ userId: updated.userId?.toString(),
164
+ authIsValid: updated.authIsValid,
165
+ ...(updated.data || {}),
166
+ };
167
+ }
168
+
169
+ const created = await this.prisma.credential.create({
170
+ data: {
171
+ userId: this._convertId(user),
172
+ externalId,
173
+ authIsValid: authIsValid,
174
+
175
+ data: oauthData,
176
+ },
177
+ });
178
+
179
+ return {
180
+ id: created.id.toString(),
181
+ externalId: created.externalId,
182
+ userId: created.userId?.toString(),
183
+ authIsValid: created.authIsValid,
184
+ ...(created.data || {}),
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Find a credential by filter criteria
190
+ * Replaces: Credential.findOne(query)
191
+ *
192
+ * @param {Object} filter
193
+ * @param {string} [filter.userId] - User ID (string from application layer)
194
+ * @param {string} [filter.externalId] - External ID
195
+ * @param {string} [filter.credentialId] - Credential ID (string from application layer)
196
+ * @returns {Promise<Object|null>} Credential object with string IDs or null if not found
197
+ */
198
+ async findCredential(filter) {
199
+ const where = this._convertFilterToWhere(filter);
200
+
201
+ const credential = await this.prisma.credential.findFirst({
202
+ where,
203
+ });
204
+
205
+ if (!credential) {
206
+ return null;
207
+ }
208
+
209
+ const data = credential.data || {};
210
+
211
+ return {
212
+ id: credential.id.toString(),
213
+ userId: credential.userId?.toString(),
214
+ externalId: credential.externalId,
215
+ authIsValid: credential.authIsValid,
216
+ access_token: data.access_token,
217
+ refresh_token: data.refresh_token,
218
+ domain: data.domain,
219
+ ...data,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Update a credential by ID
225
+ * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
226
+ *
227
+ * @param {string} credentialId - Credential ID (string from application layer)
228
+ * @param {Object} updates - Fields to update
229
+ * @returns {Promise<Object|null>} Updated credential object with string IDs or null if not found
230
+ */
231
+ async updateCredential(credentialId, updates) {
232
+ // Get existing credential to merge OAuth data
233
+ const intId = this._convertId(credentialId);
234
+ const existing = await this.prisma.credential.findUnique({
235
+ where: { id: intId },
236
+ });
237
+
238
+ if (!existing) {
239
+ return null;
240
+ }
241
+
242
+ // Separate schema fields from OAuth data
243
+ const { user, authIsValid, ...oauthData } =
244
+ updates;
245
+
246
+ // Merge OAuth data with existing
247
+ const mergedData = { ...(existing.data || {}), ...oauthData };
248
+
249
+ const updated = await this.prisma.credential.update({
250
+ where: { id: intId },
251
+ data: {
252
+ userId: this._convertId(userId || user || existing.userId),
253
+ externalId:
254
+ externalId !== undefined ? externalId : existing.externalId,
255
+ authIsValid:
256
+ authIsValid !== undefined ? authIsValid : existing.authIsValid,
257
+ data: mergedData,
258
+ },
259
+ });
260
+
261
+ const data = updated.data || {};
262
+
263
+ return {
264
+ id: updated.id.toString(),
265
+ userId: updated.userId?.toString(),
266
+ externalId: updated.externalId,
267
+ authIsValid: updated.authIsValid,
268
+ access_token: data.access_token,
269
+ refresh_token: data.refresh_token,
270
+ domain: data.domain,
271
+ ...data,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Convert identifiers to Prisma where clause (converting IDs to Int)
277
+ * @private
278
+ * @param {Object} identifiers - Identifier fields
279
+ * @returns {Object} Prisma where clause with Int IDs
280
+ */
281
+ _convertIdentifiersToWhere(identifiers) {
282
+ const where = {};
283
+
284
+ if (identifiers.id) where.id = this._convertId(identifiers.id);
285
+ if (identifiers.user) where.userId = this._convertId(identifiers.user);
286
+ if (identifiers.userId)
287
+ where.userId = this._convertId(identifiers.userId);
288
+ if (identifiers.externalId) where.externalId = identifiers.externalId;
289
+
290
+ return where;
291
+ }
292
+
293
+ /**
294
+ * Convert filter to Prisma where clause (converting IDs to Int)
295
+ * @private
296
+ * @param {Object} filter - Filter criteria
297
+ * @returns {Object} Prisma where clause with Int IDs
298
+ */
299
+ _convertFilterToWhere(filter) {
300
+ const where = {};
301
+
302
+ if (filter.credentialId)
303
+ where.id = this._convertId(filter.credentialId);
304
+ if (filter.id) where.id = this._convertId(filter.id);
305
+ if (filter.user) where.userId = this._convertId(filter.user);
306
+ if (filter.userId) where.userId = this._convertId(filter.userId);
307
+ if (filter.externalId) where.externalId = filter.externalId;
308
+
309
+ return where;
310
+ }
311
+ }
312
+
313
+ module.exports = { CredentialRepositoryPostgres };
@@ -0,0 +1,302 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ CredentialRepositoryInterface,
4
+ } = require('./credential-repository-interface');
5
+
6
+ /**
7
+ * Prisma-based Credential Repository
8
+ * Handles OAuth credentials and API tokens persistence
9
+ *
10
+ * Works identically for both MongoDB and PostgreSQL:
11
+ * - MongoDB: String IDs with @db.ObjectId
12
+ * - PostgreSQL: Integer IDs with auto-increment
13
+ * - Both use same query patterns (no many-to-many differences)
14
+ *
15
+ * Migration from Mongoose:
16
+ * - Constructor injection of Prisma client
17
+ * - Dynamic schema (strict: false) → JSON field (data)
18
+ * - All OAuth tokens stored in data JSON field
19
+ * - Mongoose field names → Prisma field names (user → userId)
20
+ */
21
+ class CredentialRepository extends CredentialRepositoryInterface {
22
+ constructor(prismaClient = prisma) {
23
+ super();
24
+ this.prisma = prismaClient; // Allow injection for testing
25
+ }
26
+
27
+ /**
28
+ * Find credential by ID
29
+ * Replaces: Credential.findById(id)
30
+ *
31
+ * @param {string} id - Credential ID
32
+ * @returns {Promise<Object|null>} Credential object or null
33
+ */
34
+ async findCredentialById(id) {
35
+ const credential = await this.prisma.credential.findUnique({
36
+ where: { id },
37
+ });
38
+
39
+ if (!credential) {
40
+ return null;
41
+ }
42
+
43
+ // Extract data from JSON field
44
+ const data = credential.data || {};
45
+
46
+ return {
47
+ _id: credential.id,
48
+ id: credential.id,
49
+ user: credential.userId,
50
+ userId: credential.userId,
51
+ externalId: credential.externalId,
52
+ authIsValid: credential.authIsValid,
53
+ ...data, // Spread OAuth tokens from JSON field
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Update authentication status
59
+ * Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
60
+ *
61
+ * @param {string} credentialId - Credential ID
62
+ * @param {boolean} authIsValid - Authentication validity status
63
+ * @returns {Promise<Object>} Update result
64
+ */
65
+ async updateAuthenticationStatus(credentialId, authIsValid) {
66
+ await this.prisma.credential.update({
67
+ where: { id: credentialId },
68
+ data: { authIsValid },
69
+ });
70
+
71
+ return { acknowledged: true, modifiedCount: 1 };
72
+ }
73
+
74
+ /**
75
+ * Permanently remove a credential document
76
+ * Replaces: Credential.deleteOne({ _id: credentialId })
77
+ *
78
+ * @param {string} credentialId - Credential ID
79
+ * @returns {Promise<Object>} Deletion result
80
+ */
81
+ async deleteCredentialById(credentialId) {
82
+ try {
83
+ await this.prisma.credential.delete({
84
+ where: { id: credentialId },
85
+ });
86
+ return { acknowledged: true, deletedCount: 1 };
87
+ } catch (error) {
88
+ if (error.code === 'P2025') {
89
+ // Record not found
90
+ return { acknowledged: true, deletedCount: 0 };
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Create or update credential matching identifiers
98
+ * Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
99
+ *
100
+ * @param {{identifiers: Object, details: Object}} credentialDetails
101
+ * @returns {Promise<Object>} The persisted credential
102
+ */
103
+ async upsertCredential(credentialDetails) {
104
+ const { identifiers, details } = credentialDetails;
105
+ if (!identifiers)
106
+ throw new Error('identifiers required to upsert credential');
107
+
108
+ // Build where clause from identifiers
109
+ const where = this._convertIdentifiersToWhere(identifiers);
110
+
111
+ // Separate schema fields from dynamic OAuth data
112
+ const {
113
+ user,
114
+ userId,
115
+ externalId,
116
+ authIsValid,
117
+
118
+ ...oauthData
119
+ } = details;
120
+
121
+ // Find existing credential
122
+ const existing = await this.prisma.credential.findFirst({ where });
123
+
124
+ if (existing) {
125
+ // Update existing - merge OAuth data into existing data JSON
126
+ const mergedData = { ...(existing.data || {}), ...oauthData };
127
+
128
+ const updated = await this.prisma.credential.update({
129
+ where: { id: existing.id },
130
+ data: {
131
+ userId: userId || user || existing.userId,
132
+ externalId:
133
+ externalId !== undefined
134
+ ? externalId
135
+ : existing.externalId,
136
+ authIsValid:
137
+ authIsValid !== undefined
138
+ ? authIsValid
139
+ : existing.authIsValid,
140
+ data: mergedData,
141
+ },
142
+ });
143
+
144
+ return {
145
+ id: updated.id,
146
+ externalId: updated.externalId,
147
+ userId: updated.userId,
148
+ authIsValid: updated.authIsValid,
149
+ ...(updated.data || {}),
150
+ };
151
+ }
152
+
153
+ // Create new credential
154
+ const created = await this.prisma.credential.create({
155
+ data: {
156
+ userId: userId || user,
157
+ externalId,
158
+ authIsValid: authIsValid,
159
+
160
+ data: oauthData,
161
+ },
162
+ });
163
+
164
+ return {
165
+ id: created.id,
166
+ externalId: created.externalId,
167
+ userId: created.userId,
168
+ authIsValid: created.authIsValid,
169
+ ...(created.data || {}),
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Find a credential by filter criteria
175
+ * Replaces: Credential.findOne(query)
176
+ *
177
+ * @param {Object} filter
178
+ * @param {string} [filter.userId] - User ID
179
+ * @param {string} [filter.externalId] - External ID
180
+ * @param {string} [filter.credentialId] - Credential ID
181
+ * @returns {Promise<Object|null>} Credential object or null if not found
182
+ */
183
+ async findCredential(filter) {
184
+ const where = this._convertFilterToWhere(filter);
185
+
186
+ const credential = await this.prisma.credential.findFirst({
187
+ where,
188
+ });
189
+
190
+ if (!credential) {
191
+ return null;
192
+ }
193
+
194
+ const data = credential.data || {};
195
+
196
+ return {
197
+ id: credential.id,
198
+ userId: credential.userId,
199
+ externalId: credential.externalId,
200
+ authIsValid: credential.authIsValid,
201
+ access_token: data.access_token,
202
+ refresh_token: data.refresh_token,
203
+ domain: data.domain,
204
+ ...data,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Update a credential by ID
210
+ * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
211
+ *
212
+ * @param {string} credentialId - Credential ID
213
+ * @param {Object} updates - Fields to update
214
+ * @returns {Promise<Object|null>} Updated credential object or null if not found
215
+ */
216
+ async updateCredential(credentialId, updates) {
217
+ // Get existing credential to merge OAuth data
218
+ const existing = await this.prisma.credential.findUnique({
219
+ where: { id: credentialId },
220
+ });
221
+
222
+ if (!existing) {
223
+ return null;
224
+ }
225
+
226
+ // Separate schema fields from OAuth data
227
+ const {
228
+ user,
229
+ userId,
230
+ externalId,
231
+ authIsValid,
232
+
233
+ ...oauthData
234
+ } = updates;
235
+
236
+ // Merge OAuth data with existing
237
+ const mergedData = { ...(existing.data || {}), ...oauthData };
238
+
239
+ const updated = await this.prisma.credential.update({
240
+ where: { id: credentialId },
241
+ data: {
242
+ userId: userId || user || existing.userId,
243
+ externalId:
244
+ externalId !== undefined ? externalId : existing.externalId,
245
+ authIsValid:
246
+ authIsValid !== undefined ? authIsValid : existing.authIsValid,
247
+ data: mergedData,
248
+ },
249
+ });
250
+
251
+ const data = updated.data || {};
252
+
253
+ return {
254
+ id: updated.id,
255
+ userId: updated.userId,
256
+ externalId: updated.externalId,
257
+ authIsValid: updated.authIsValid,
258
+ access_token: data.access_token,
259
+ refresh_token: data.refresh_token,
260
+ domain: data.domain,
261
+ ...data,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Convert identifiers to Prisma where clause
267
+ * @private
268
+ * @param {Object} identifiers - Identifier fields
269
+ * @returns {Object} Prisma where clause
270
+ */
271
+ _convertIdentifiersToWhere(identifiers) {
272
+ const where = {};
273
+
274
+ if (identifiers._id) where.id = identifiers._id;
275
+ if (identifiers.id) where.id = identifiers.id;
276
+ if (identifiers.user) where.userId = identifiers.user;
277
+ if (identifiers.userId) where.userId = identifiers.userId;
278
+ if (identifiers.externalId) where.externalId = identifiers.externalId;
279
+
280
+ return where;
281
+ }
282
+
283
+ /**
284
+ * Convert filter to Prisma where clause
285
+ * @private
286
+ * @param {Object} filter - Filter criteria
287
+ * @returns {Object} Prisma where clause
288
+ */
289
+ _convertFilterToWhere(filter) {
290
+ const where = {};
291
+
292
+ if (filter.credentialId) where.id = filter.credentialId;
293
+ if (filter.id) where.id = filter.id;
294
+ if (filter.user) where.userId = filter.user;
295
+ if (filter.userId) where.userId = filter.userId;
296
+ if (filter.externalId) where.externalId = filter.externalId;
297
+
298
+ return where;
299
+ }
300
+ }
301
+
302
+ module.exports = { CredentialRepository };
@@ -0,0 +1,21 @@
1
+ class GetCredentialForUser {
2
+ constructor({ credentialRepository }) {
3
+ this.credentialRepository = credentialRepository;
4
+ }
5
+
6
+ async execute(credentialId, userId) {
7
+ const credential = await this.credentialRepository.findCredentialById(credentialId);
8
+
9
+ if (!credential) {
10
+ throw new Error(`Credential with id ${credentialId} not found`);
11
+ }
12
+
13
+ if (credential.user.toString() !== userId.toString()) {
14
+ throw new Error(`Credential ${credentialId} does not belong to user ${userId}`);
15
+ }
16
+
17
+ return credential;
18
+ }
19
+ }
20
+
21
+ module.exports = { GetCredentialForUser };
@@ -0,0 +1,15 @@
1
+ class UpdateAuthenticationStatus {
2
+ constructor({ credentialRepository }) {
3
+ this.credentialRepository = credentialRepository;
4
+ }
5
+
6
+ /**
7
+ * @param {string} credentialId
8
+ * @param {boolean} authIsValid
9
+ */
10
+ async execute(credentialId, authIsValid) {
11
+ await this.credentialRepository.updateAuthenticationStatus(credentialId, authIsValid);
12
+ }
13
+ }
14
+
15
+ module.exports = { UpdateAuthenticationStatus };