@friggframework/core 2.0.0-next.41 → 2.0.0-next.43

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 (197) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +27 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +122 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +318 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  134. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  135. package/prisma-postgresql/schema.prisma +300 -0
  136. package/syncs/manager.js +468 -443
  137. package/syncs/repositories/sync-repository-factory.js +38 -0
  138. package/syncs/repositories/sync-repository-interface.js +109 -0
  139. package/syncs/repositories/sync-repository-mongo.js +239 -0
  140. package/syncs/repositories/sync-repository-postgres.js +319 -0
  141. package/syncs/sync.js +0 -1
  142. package/token/repositories/token-repository-factory.js +33 -0
  143. package/token/repositories/token-repository-interface.js +131 -0
  144. package/token/repositories/token-repository-mongo.js +212 -0
  145. package/token/repositories/token-repository-postgres.js +257 -0
  146. package/token/repositories/token-repository.js +219 -0
  147. package/types/integrations/index.d.ts +2 -6
  148. package/types/module-plugin/index.d.ts +5 -57
  149. package/types/syncs/index.d.ts +0 -2
  150. package/user/repositories/user-repository-factory.js +46 -0
  151. package/user/repositories/user-repository-interface.js +198 -0
  152. package/user/repositories/user-repository-mongo.js +250 -0
  153. package/user/repositories/user-repository-postgres.js +311 -0
  154. package/user/tests/doubles/test-user-repository.js +72 -0
  155. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  156. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  157. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  158. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  159. package/user/tests/use-cases/login-user.test.js +140 -0
  160. package/user/use-cases/create-individual-user.js +61 -0
  161. package/user/use-cases/create-organization-user.js +47 -0
  162. package/user/use-cases/create-token-for-user-id.js +30 -0
  163. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  164. package/user/use-cases/login-user.js +122 -0
  165. package/user/user.js +77 -0
  166. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  167. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  168. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  169. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  170. package/websocket/repositories/websocket-connection-repository.js +160 -0
  171. package/database/models/State.js +0 -9
  172. package/database/models/Token.js +0 -70
  173. package/database/mongo.js +0 -171
  174. package/encrypt/Cryptor.test.js +0 -32
  175. package/encrypt/encrypt.js +0 -104
  176. package/encrypt/encrypt.test.js +0 -1069
  177. package/handlers/routers/middleware/loadUser.js +0 -15
  178. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  179. package/integrations/create-frigg-backend.js +0 -31
  180. package/integrations/integration-factory.js +0 -251
  181. package/integrations/integration-mapping.js +0 -43
  182. package/integrations/integration-model.js +0 -46
  183. package/integrations/integration-user.js +0 -144
  184. package/integrations/test/integration-base.test.js +0 -144
  185. package/module-plugin/auther.js +0 -393
  186. package/module-plugin/credential.js +0 -22
  187. package/module-plugin/entity-manager.js +0 -70
  188. package/module-plugin/manager.js +0 -169
  189. package/module-plugin/module-factory.js +0 -61
  190. package/module-plugin/test/auther.test.js +0 -97
  191. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  192. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  193. /package/{module-plugin → modules}/requester/basic.js +0 -0
  194. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.js +0 -0
  196. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  197. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,307 @@
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
+ subType: credential.subType,
65
+ ...data, // Spread OAuth tokens from JSON field
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Update authentication status
71
+ * Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
72
+ *
73
+ * @param {string} credentialId - Credential ID (string from application layer)
74
+ * @param {boolean} authIsValid - Authentication validity status
75
+ * @returns {Promise<Object>} Update result
76
+ */
77
+ async updateAuthenticationStatus(credentialId, authIsValid) {
78
+ const intId = this._convertId(credentialId);
79
+ await this.prisma.credential.update({
80
+ where: { id: intId },
81
+ data: { authIsValid },
82
+ });
83
+
84
+ return { acknowledged: true, modifiedCount: 1 };
85
+ }
86
+
87
+ /**
88
+ * Permanently remove a credential document
89
+ * Replaces: Credential.deleteOne({ _id: credentialId })
90
+ *
91
+ * @param {string} credentialId - Credential ID (string from application layer)
92
+ * @returns {Promise<Object>} Deletion result
93
+ */
94
+ async deleteCredentialById(credentialId) {
95
+ try {
96
+ const intId = this._convertId(credentialId);
97
+ await this.prisma.credential.delete({
98
+ where: { id: intId },
99
+ });
100
+ return { acknowledged: true, deletedCount: 1 };
101
+ } catch (error) {
102
+ if (error.code === 'P2025') {
103
+ // Record not found
104
+ return { acknowledged: true, deletedCount: 0 };
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Create or update credential matching identifiers
112
+ * Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
113
+ *
114
+ * @param {{identifiers: Object, details: Object}} credentialDetails
115
+ * @returns {Promise<Object>} The persisted credential with string IDs
116
+ */
117
+ async upsertCredential(credentialDetails) {
118
+ const { identifiers, details } = credentialDetails;
119
+ if (!identifiers)
120
+ throw new Error('identifiers required to upsert credential');
121
+
122
+ const where = this._convertIdentifiersToWhere(identifiers);
123
+
124
+ const { user, externalId } = identifiers;
125
+
126
+ // Separate schema fields from dynamic OAuth data
127
+ const { authIsValid, subType, ...oauthData } = details;
128
+
129
+ const existing = await this.prisma.credential.findFirst({ where });
130
+
131
+ if (existing) {
132
+ const mergedData = { ...(existing.data || {}), ...oauthData };
133
+
134
+ const updated = await this.prisma.credential.update({
135
+ where: { id: existing.id },
136
+ data: {
137
+ userId: this._convertId(user || existing.userId),
138
+ externalId:
139
+ externalId !== undefined
140
+ ? externalId
141
+ : existing.externalId,
142
+ authIsValid:
143
+ authIsValid !== undefined
144
+ ? authIsValid
145
+ : existing.authIsValid,
146
+ subType: subType !== undefined ? subType : existing.subType,
147
+ data: mergedData,
148
+ },
149
+ });
150
+
151
+ return {
152
+ id: updated.id.toString(),
153
+ externalId: updated.externalId,
154
+ userId: updated.userId?.toString(),
155
+ authIsValid: updated.authIsValid,
156
+ ...(updated.data || {}),
157
+ };
158
+ }
159
+
160
+ const created = await this.prisma.credential.create({
161
+ data: {
162
+ userId: this._convertId(user),
163
+ externalId,
164
+ authIsValid: authIsValid,
165
+ subType,
166
+ data: oauthData,
167
+ },
168
+ });
169
+
170
+ return {
171
+ id: created.id.toString(),
172
+ externalId: created.externalId,
173
+ userId: created.userId?.toString(),
174
+ authIsValid: created.authIsValid,
175
+ ...(created.data || {}),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Find a credential by filter criteria
181
+ * Replaces: Credential.findOne(query)
182
+ *
183
+ * @param {Object} filter
184
+ * @param {string} [filter.userId] - User ID (string from application layer)
185
+ * @param {string} [filter.externalId] - External ID
186
+ * @param {string} [filter.credentialId] - Credential ID (string from application layer)
187
+ * @returns {Promise<Object|null>} Credential object with string IDs or null if not found
188
+ */
189
+ async findCredential(filter) {
190
+ const where = this._convertFilterToWhere(filter);
191
+
192
+ const credential = await this.prisma.credential.findFirst({
193
+ where,
194
+ });
195
+
196
+ if (!credential) {
197
+ return null;
198
+ }
199
+
200
+ const data = credential.data || {};
201
+
202
+ return {
203
+ id: credential.id.toString(),
204
+ userId: credential.userId?.toString(),
205
+ externalId: credential.externalId,
206
+ authIsValid: credential.authIsValid,
207
+ access_token: data.access_token,
208
+ refresh_token: data.refresh_token,
209
+ domain: data.domain,
210
+ ...data,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Update a credential by ID
216
+ * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
217
+ *
218
+ * @param {string} credentialId - Credential ID (string from application layer)
219
+ * @param {Object} updates - Fields to update
220
+ * @returns {Promise<Object|null>} Updated credential object with string IDs or null if not found
221
+ */
222
+ async updateCredential(credentialId, updates) {
223
+ // Get existing credential to merge OAuth data
224
+ const intId = this._convertId(credentialId);
225
+ const existing = await this.prisma.credential.findUnique({
226
+ where: { id: intId },
227
+ });
228
+
229
+ if (!existing) {
230
+ return null;
231
+ }
232
+
233
+ // Separate schema fields from OAuth data
234
+ const { user, authIsValid, subType, ...oauthData } =
235
+ updates;
236
+
237
+ // Merge OAuth data with existing
238
+ const mergedData = { ...(existing.data || {}), ...oauthData };
239
+
240
+ const updated = await this.prisma.credential.update({
241
+ where: { id: intId },
242
+ data: {
243
+ userId: this._convertId(userId || user || existing.userId),
244
+ externalId:
245
+ externalId !== undefined ? externalId : existing.externalId,
246
+ authIsValid:
247
+ authIsValid !== undefined ? authIsValid : existing.authIsValid,
248
+ subType: subType !== undefined ? subType : existing.subType,
249
+ data: mergedData,
250
+ },
251
+ });
252
+
253
+ const data = updated.data || {};
254
+
255
+ return {
256
+ id: updated.id.toString(),
257
+ userId: updated.userId?.toString(),
258
+ externalId: updated.externalId,
259
+ authIsValid: updated.authIsValid,
260
+ access_token: data.access_token,
261
+ refresh_token: data.refresh_token,
262
+ domain: data.domain,
263
+ ...data,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Convert identifiers to Prisma where clause (converting IDs to Int)
269
+ * @private
270
+ * @param {Object} identifiers - Identifier fields
271
+ * @returns {Object} Prisma where clause with Int IDs
272
+ */
273
+ _convertIdentifiersToWhere(identifiers) {
274
+ const where = {};
275
+
276
+ if (identifiers.id) where.id = this._convertId(identifiers.id);
277
+ if (identifiers.user) where.userId = this._convertId(identifiers.user);
278
+ if (identifiers.userId)
279
+ where.userId = this._convertId(identifiers.userId);
280
+ if (identifiers.externalId) where.externalId = identifiers.externalId;
281
+ if (identifiers.subType) where.subType = identifiers.subType;
282
+
283
+ return where;
284
+ }
285
+
286
+ /**
287
+ * Convert filter to Prisma where clause (converting IDs to Int)
288
+ * @private
289
+ * @param {Object} filter - Filter criteria
290
+ * @returns {Object} Prisma where clause with Int IDs
291
+ */
292
+ _convertFilterToWhere(filter) {
293
+ const where = {};
294
+
295
+ if (filter.credentialId)
296
+ where.id = this._convertId(filter.credentialId);
297
+ if (filter.id) where.id = this._convertId(filter.id);
298
+ if (filter.user) where.userId = this._convertId(filter.user);
299
+ if (filter.userId) where.userId = this._convertId(filter.userId);
300
+ if (filter.externalId) where.externalId = filter.externalId;
301
+ if (filter.subType) where.subType = filter.subType;
302
+
303
+ return where;
304
+ }
305
+ }
306
+
307
+ module.exports = { CredentialRepositoryPostgres };
@@ -0,0 +1,307 @@
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
+ subType: credential.subType,
54
+ ...data, // Spread OAuth tokens from JSON field
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Update authentication status
60
+ * Replaces: Credential.updateOne({ _id: credentialId }, { $set: { authIsValid } })
61
+ *
62
+ * @param {string} credentialId - Credential ID
63
+ * @param {boolean} authIsValid - Authentication validity status
64
+ * @returns {Promise<Object>} Update result
65
+ */
66
+ async updateAuthenticationStatus(credentialId, authIsValid) {
67
+ await this.prisma.credential.update({
68
+ where: { id: credentialId },
69
+ data: { authIsValid },
70
+ });
71
+
72
+ return { acknowledged: true, modifiedCount: 1 };
73
+ }
74
+
75
+ /**
76
+ * Permanently remove a credential document
77
+ * Replaces: Credential.deleteOne({ _id: credentialId })
78
+ *
79
+ * @param {string} credentialId - Credential ID
80
+ * @returns {Promise<Object>} Deletion result
81
+ */
82
+ async deleteCredentialById(credentialId) {
83
+ try {
84
+ await this.prisma.credential.delete({
85
+ where: { id: credentialId },
86
+ });
87
+ return { acknowledged: true, deletedCount: 1 };
88
+ } catch (error) {
89
+ if (error.code === 'P2025') {
90
+ // Record not found
91
+ return { acknowledged: true, deletedCount: 0 };
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Create or update credential matching identifiers
99
+ * Replaces: Credential.findOneAndUpdate(query, update, { upsert: true })
100
+ *
101
+ * @param {{identifiers: Object, details: Object}} credentialDetails
102
+ * @returns {Promise<Object>} The persisted credential
103
+ */
104
+ async upsertCredential(credentialDetails) {
105
+ const { identifiers, details } = credentialDetails;
106
+ if (!identifiers)
107
+ throw new Error('identifiers required to upsert credential');
108
+
109
+ // Build where clause from identifiers
110
+ const where = this._convertIdentifiersToWhere(identifiers);
111
+
112
+ // Separate schema fields from dynamic OAuth data
113
+ const {
114
+ user,
115
+ userId,
116
+ externalId,
117
+ authIsValid,
118
+ subType,
119
+ ...oauthData
120
+ } = details;
121
+
122
+ // Find existing credential
123
+ const existing = await this.prisma.credential.findFirst({ where });
124
+
125
+ if (existing) {
126
+ // Update existing - merge OAuth data into existing data JSON
127
+ const mergedData = { ...(existing.data || {}), ...oauthData };
128
+
129
+ const updated = await this.prisma.credential.update({
130
+ where: { id: existing.id },
131
+ data: {
132
+ userId: userId || user || existing.userId,
133
+ externalId:
134
+ externalId !== undefined
135
+ ? externalId
136
+ : existing.externalId,
137
+ authIsValid:
138
+ authIsValid !== undefined
139
+ ? authIsValid
140
+ : existing.authIsValid,
141
+ subType: subType !== undefined ? subType : existing.subType,
142
+ data: mergedData,
143
+ },
144
+ });
145
+
146
+ return {
147
+ id: updated.id,
148
+ externalId: updated.externalId,
149
+ userId: updated.userId,
150
+ authIsValid: updated.authIsValid,
151
+ ...(updated.data || {}),
152
+ };
153
+ }
154
+
155
+ // Create new credential
156
+ const created = await this.prisma.credential.create({
157
+ data: {
158
+ userId: userId || user,
159
+ externalId,
160
+ authIsValid: authIsValid,
161
+ subType,
162
+ data: oauthData,
163
+ },
164
+ });
165
+
166
+ return {
167
+ id: created.id,
168
+ externalId: created.externalId,
169
+ userId: created.userId,
170
+ authIsValid: created.authIsValid,
171
+ ...(created.data || {}),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Find a credential by filter criteria
177
+ * Replaces: Credential.findOne(query)
178
+ *
179
+ * @param {Object} filter
180
+ * @param {string} [filter.userId] - User ID
181
+ * @param {string} [filter.externalId] - External ID
182
+ * @param {string} [filter.credentialId] - Credential ID
183
+ * @returns {Promise<Object|null>} Credential object or null if not found
184
+ */
185
+ async findCredential(filter) {
186
+ const where = this._convertFilterToWhere(filter);
187
+
188
+ const credential = await this.prisma.credential.findFirst({
189
+ where,
190
+ });
191
+
192
+ if (!credential) {
193
+ return null;
194
+ }
195
+
196
+ const data = credential.data || {};
197
+
198
+ return {
199
+ id: credential.id,
200
+ userId: credential.userId,
201
+ externalId: credential.externalId,
202
+ authIsValid: credential.authIsValid,
203
+ access_token: data.access_token,
204
+ refresh_token: data.refresh_token,
205
+ domain: data.domain,
206
+ ...data,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Update a credential by ID
212
+ * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
213
+ *
214
+ * @param {string} credentialId - Credential ID
215
+ * @param {Object} updates - Fields to update
216
+ * @returns {Promise<Object|null>} Updated credential object or null if not found
217
+ */
218
+ async updateCredential(credentialId, updates) {
219
+ // Get existing credential to merge OAuth data
220
+ const existing = await this.prisma.credential.findUnique({
221
+ where: { id: credentialId },
222
+ });
223
+
224
+ if (!existing) {
225
+ return null;
226
+ }
227
+
228
+ // Separate schema fields from OAuth data
229
+ const {
230
+ user,
231
+ userId,
232
+ externalId,
233
+ authIsValid,
234
+ subType,
235
+ ...oauthData
236
+ } = updates;
237
+
238
+ // Merge OAuth data with existing
239
+ const mergedData = { ...(existing.data || {}), ...oauthData };
240
+
241
+ const updated = await this.prisma.credential.update({
242
+ where: { id: credentialId },
243
+ data: {
244
+ userId: userId || user || existing.userId,
245
+ externalId:
246
+ externalId !== undefined ? externalId : existing.externalId,
247
+ authIsValid:
248
+ authIsValid !== undefined ? authIsValid : existing.authIsValid,
249
+ subType: subType !== undefined ? subType : existing.subType,
250
+ data: mergedData,
251
+ },
252
+ });
253
+
254
+ const data = updated.data || {};
255
+
256
+ return {
257
+ id: updated.id,
258
+ userId: updated.userId,
259
+ externalId: updated.externalId,
260
+ authIsValid: updated.authIsValid,
261
+ access_token: data.access_token,
262
+ refresh_token: data.refresh_token,
263
+ domain: data.domain,
264
+ ...data,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Convert identifiers to Prisma where clause
270
+ * @private
271
+ * @param {Object} identifiers - Identifier fields
272
+ * @returns {Object} Prisma where clause
273
+ */
274
+ _convertIdentifiersToWhere(identifiers) {
275
+ const where = {};
276
+
277
+ if (identifiers._id) where.id = identifiers._id;
278
+ if (identifiers.id) where.id = identifiers.id;
279
+ if (identifiers.user) where.userId = identifiers.user;
280
+ if (identifiers.userId) where.userId = identifiers.userId;
281
+ if (identifiers.externalId) where.externalId = identifiers.externalId;
282
+ if (identifiers.subType) where.subType = identifiers.subType;
283
+
284
+ return where;
285
+ }
286
+
287
+ /**
288
+ * Convert filter to Prisma where clause
289
+ * @private
290
+ * @param {Object} filter - Filter criteria
291
+ * @returns {Object} Prisma where clause
292
+ */
293
+ _convertFilterToWhere(filter) {
294
+ const where = {};
295
+
296
+ if (filter.credentialId) where.id = filter.credentialId;
297
+ if (filter.id) where.id = filter.id;
298
+ if (filter.user) where.userId = filter.user;
299
+ if (filter.userId) where.userId = filter.userId;
300
+ if (filter.externalId) where.externalId = filter.externalId;
301
+ if (filter.subType) where.subType = filter.subType;
302
+
303
+ return where;
304
+ }
305
+ }
306
+
307
+ 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 };