@friggframework/core 2.0.0-next.53 → 2.0.0-next.55

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 (70) hide show
  1. package/CLAUDE.md +2 -1
  2. package/application/commands/credential-commands.js +1 -1
  3. package/application/commands/integration-commands.js +1 -1
  4. package/application/index.js +1 -1
  5. package/core/create-handler.js +12 -0
  6. package/credential/repositories/credential-repository-documentdb.js +304 -0
  7. package/credential/repositories/credential-repository-factory.js +8 -1
  8. package/credential/repositories/credential-repository-mongo.js +16 -54
  9. package/credential/repositories/credential-repository-postgres.js +14 -41
  10. package/credential/use-cases/get-credential-for-user.js +7 -3
  11. package/database/config.js +4 -4
  12. package/database/documentdb-encryption-service.js +330 -0
  13. package/database/documentdb-utils.js +136 -0
  14. package/database/encryption/README.md +50 -1
  15. package/database/encryption/documentdb-encryption-service.md +3270 -0
  16. package/database/encryption/encryption-schema-registry.js +46 -0
  17. package/database/prisma.js +7 -47
  18. package/database/repositories/health-check-repository-documentdb.js +134 -0
  19. package/database/repositories/health-check-repository-factory.js +6 -1
  20. package/database/repositories/health-check-repository-interface.js +29 -34
  21. package/database/repositories/health-check-repository-mongodb.js +1 -3
  22. package/database/use-cases/check-database-state-use-case.js +3 -3
  23. package/database/use-cases/run-database-migration-use-case.js +6 -4
  24. package/database/use-cases/trigger-database-migration-use-case.js +2 -2
  25. package/database/utils/mongodb-schema-init.js +5 -5
  26. package/database/utils/prisma-runner.js +15 -9
  27. package/errors/client-safe-error.js +26 -0
  28. package/errors/fetch-error.js +2 -1
  29. package/errors/index.js +2 -0
  30. package/generated/prisma-mongodb/edge.js +3 -3
  31. package/generated/prisma-mongodb/index.d.ts +10 -4
  32. package/generated/prisma-mongodb/index.js +3 -3
  33. package/generated/prisma-mongodb/package.json +1 -1
  34. package/generated/prisma-mongodb/schema.prisma +1 -3
  35. package/generated/prisma-mongodb/wasm.js +2 -2
  36. package/generated/prisma-postgresql/edge.js +3 -3
  37. package/generated/prisma-postgresql/index.d.ts +10 -4
  38. package/generated/prisma-postgresql/index.js +3 -3
  39. package/generated/prisma-postgresql/package.json +1 -1
  40. package/generated/prisma-postgresql/schema.prisma +1 -3
  41. package/generated/prisma-postgresql/wasm.js +2 -2
  42. package/handlers/routers/db-migration.js +2 -3
  43. package/handlers/routers/health.js +0 -3
  44. package/handlers/workers/db-migration.js +8 -8
  45. package/integrations/integration-router.js +6 -6
  46. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  47. package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
  48. package/integrations/repositories/integration-repository-documentdb.js +210 -0
  49. package/integrations/repositories/integration-repository-factory.js +8 -1
  50. package/integrations/repositories/process-repository-documentdb.js +243 -0
  51. package/integrations/repositories/process-repository-factory.js +8 -1
  52. package/modules/repositories/module-repository-documentdb.js +307 -0
  53. package/modules/repositories/module-repository-factory.js +8 -1
  54. package/package.json +5 -5
  55. package/prisma-mongodb/schema.prisma +1 -3
  56. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +69 -0
  57. package/prisma-postgresql/schema.prisma +1 -3
  58. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  59. package/syncs/repositories/sync-repository-factory.js +6 -1
  60. package/token/repositories/token-repository-documentdb.js +137 -0
  61. package/token/repositories/token-repository-factory.js +8 -1
  62. package/token/repositories/token-repository-mongo.js +10 -3
  63. package/token/repositories/token-repository-postgres.js +10 -3
  64. package/user/repositories/user-repository-documentdb.js +432 -0
  65. package/user/repositories/user-repository-factory.js +6 -1
  66. package/user/repositories/user-repository-mongo.js +3 -2
  67. package/user/repositories/user-repository-postgres.js +3 -2
  68. package/user/use-cases/login-user.js +1 -1
  69. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  70. package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
@@ -0,0 +1,432 @@
1
+ const bcrypt = require('bcryptjs');
2
+ const { prisma } = require('../../database/prisma');
3
+ const {
4
+ toObjectId,
5
+ fromObjectId,
6
+ findOne,
7
+ insertOne,
8
+ updateOne,
9
+ deleteOne,
10
+ } = require('../../database/documentdb-utils');
11
+ const {
12
+ createTokenRepository,
13
+ } = require('../../token/repositories/token-repository-factory');
14
+ const { UserRepositoryInterface } = require('./user-repository-interface');
15
+ const { ClientSafeError } = require('../../errors');
16
+ const {
17
+ DocumentDBEncryptionService,
18
+ } = require('../../database/documentdb-encryption-service');
19
+
20
+ /**
21
+ * User repository for DocumentDB.
22
+ * Uses DocumentDBEncryptionService for field-level encryption.
23
+ *
24
+ * Encrypted fields: User.hashword
25
+ *
26
+ * @see DocumentDBEncryptionService
27
+ * @see encryption-schema-registry.js
28
+ */
29
+ class UserRepositoryDocumentDB extends UserRepositoryInterface {
30
+ constructor() {
31
+ super();
32
+ this.prisma = prisma;
33
+ this.tokenRepository = createTokenRepository();
34
+ this.encryptionService = new DocumentDBEncryptionService();
35
+ }
36
+
37
+ async getSessionToken(token) {
38
+ const jsonToken =
39
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
40
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
41
+ jsonToken
42
+ );
43
+ return sessionToken;
44
+ }
45
+
46
+ async findOrganizationUserById(userId) {
47
+ const doc = await findOne(this.prisma, 'User', {
48
+ _id: toObjectId(userId),
49
+ type: 'ORGANIZATION',
50
+ });
51
+ const decrypted = await this.encryptionService.decryptFields(
52
+ 'User',
53
+ doc
54
+ );
55
+ return this._mapUser(decrypted);
56
+ }
57
+
58
+ async findIndividualUserById(userId) {
59
+ const doc = await findOne(this.prisma, 'User', {
60
+ _id: toObjectId(userId),
61
+ type: 'INDIVIDUAL',
62
+ });
63
+ const decrypted = await this.encryptionService.decryptFields(
64
+ 'User',
65
+ doc
66
+ );
67
+ return this._mapUser(decrypted);
68
+ }
69
+
70
+ async createToken(userId, rawToken, minutes = 120) {
71
+ const createdToken = await this.tokenRepository.createTokenWithExpire(
72
+ fromObjectId(toObjectId(userId)),
73
+ rawToken,
74
+ minutes
75
+ );
76
+ return this.tokenRepository.createBase64BufferToken(
77
+ createdToken,
78
+ rawToken
79
+ );
80
+ }
81
+
82
+ async createIndividualUser(params) {
83
+ const now = new Date();
84
+ const document = {
85
+ type: 'INDIVIDUAL',
86
+ email: params.email ?? null,
87
+ username: params.username ?? null,
88
+ appUserId: params.appUserId ?? null,
89
+ organizationId: params.organization
90
+ ? toObjectId(params.organization)
91
+ : params.organizationId
92
+ ? toObjectId(params.organizationId)
93
+ : null,
94
+ createdAt: now,
95
+ updatedAt: now,
96
+ };
97
+
98
+ if (
99
+ params.hashword !== undefined &&
100
+ params.hashword !== null &&
101
+ params.hashword !== ''
102
+ ) {
103
+ if (typeof params.hashword !== 'string') {
104
+ throw new ClientSafeError('Password must be a string', 400);
105
+ }
106
+
107
+ if (params.hashword.startsWith('$2')) {
108
+ throw new Error(
109
+ 'Password appears to be already hashed. Pass plain text password only.'
110
+ );
111
+ }
112
+
113
+ // Bcrypt hash the password
114
+ document.hashword = await bcrypt.hash(params.hashword, 10);
115
+ }
116
+
117
+ // Encrypt sensitive fields before insert
118
+ const encryptedDocument = await this.encryptionService.encryptFields(
119
+ 'User',
120
+ document
121
+ );
122
+ const insertedId = await insertOne(
123
+ this.prisma,
124
+ 'User',
125
+ encryptedDocument
126
+ );
127
+ const created = await findOne(this.prisma, 'User', { _id: insertedId });
128
+
129
+ // Defensive check: verify document was found after insert
130
+ if (!created) {
131
+ console.error(
132
+ '[UserRepositoryDocumentDB] User not found after insert',
133
+ {
134
+ insertedId: fromObjectId(insertedId),
135
+ params: {
136
+ username: params.username,
137
+ appUserId: params.appUserId,
138
+ email: params.email,
139
+ },
140
+ }
141
+ );
142
+ throw new Error(
143
+ 'Failed to create individual user: Document not found after insert. ' +
144
+ 'This indicates a database consistency issue.'
145
+ );
146
+ }
147
+
148
+ // Decrypt sensitive fields after read
149
+ const decrypted = await this.encryptionService.decryptFields(
150
+ 'User',
151
+ created
152
+ );
153
+
154
+ return this._mapUser(decrypted);
155
+ }
156
+
157
+ async createOrganizationUser(params) {
158
+ const now = new Date();
159
+ const document = {
160
+ type: 'ORGANIZATION',
161
+ appOrgId: params.appOrgId ?? null,
162
+ name: params.name ?? null,
163
+ createdAt: now,
164
+ updatedAt: now,
165
+ };
166
+
167
+ // Encrypt sensitive fields before insert (consistency with individual user)
168
+ const encryptedDocument = await this.encryptionService.encryptFields(
169
+ 'User',
170
+ document
171
+ );
172
+ const insertedId = await insertOne(
173
+ this.prisma,
174
+ 'User',
175
+ encryptedDocument
176
+ );
177
+ const created = await findOne(this.prisma, 'User', { _id: insertedId });
178
+
179
+ // Defensive check: verify document was found after insert
180
+ if (!created) {
181
+ console.error(
182
+ '[UserRepositoryDocumentDB] Organization user not found after insert',
183
+ {
184
+ insertedId: fromObjectId(insertedId),
185
+ params: {
186
+ appOrgId: params.appOrgId,
187
+ name: params.name,
188
+ },
189
+ }
190
+ );
191
+ throw new Error(
192
+ 'Failed to create organization user: Document not found after insert. ' +
193
+ 'This indicates a database consistency issue.'
194
+ );
195
+ }
196
+
197
+ // Decrypt sensitive fields after read
198
+ const decrypted = await this.encryptionService.decryptFields(
199
+ 'User',
200
+ created
201
+ );
202
+ return this._mapUser(decrypted);
203
+ }
204
+
205
+ async findIndividualUserByUsername(username) {
206
+ const doc = await findOne(this.prisma, 'User', {
207
+ type: 'INDIVIDUAL',
208
+ username,
209
+ });
210
+ const decrypted = await this.encryptionService.decryptFields(
211
+ 'User',
212
+ doc
213
+ );
214
+ return this._mapUser(decrypted);
215
+ }
216
+
217
+ async findIndividualUserByAppUserId(appUserId) {
218
+ const doc = await findOne(this.prisma, 'User', {
219
+ type: 'INDIVIDUAL',
220
+ appUserId,
221
+ });
222
+ const decrypted = await this.encryptionService.decryptFields(
223
+ 'User',
224
+ doc
225
+ );
226
+ return this._mapUser(decrypted);
227
+ }
228
+
229
+ async findOrganizationUserByAppOrgId(appOrgId) {
230
+ const doc = await findOne(this.prisma, 'User', {
231
+ type: 'ORGANIZATION',
232
+ appOrgId,
233
+ });
234
+ const decrypted = await this.encryptionService.decryptFields(
235
+ 'User',
236
+ doc
237
+ );
238
+ return this._mapUser(decrypted);
239
+ }
240
+
241
+ async findUserById(userId) {
242
+ const doc = await findOne(this.prisma, 'User', {
243
+ _id: toObjectId(userId),
244
+ });
245
+ const decrypted = await this.encryptionService.decryptFields(
246
+ 'User',
247
+ doc
248
+ );
249
+ return this._mapUser(decrypted);
250
+ }
251
+
252
+ async findIndividualUserByEmail(email) {
253
+ const doc = await findOne(this.prisma, 'User', {
254
+ type: 'INDIVIDUAL',
255
+ email,
256
+ });
257
+ const decrypted = await this.encryptionService.decryptFields(
258
+ 'User',
259
+ doc
260
+ );
261
+ return this._mapUser(decrypted);
262
+ }
263
+
264
+ async updateIndividualUser(userId, updates) {
265
+ const objectId = toObjectId(userId);
266
+ if (!objectId) return null;
267
+
268
+ const payload = await this._prepareUpdatePayload(updates);
269
+ payload.updatedAt = new Date();
270
+
271
+ // Encrypt sensitive fields before update
272
+ const encryptedPayload = await this.encryptionService.encryptFields(
273
+ 'User',
274
+ payload
275
+ );
276
+
277
+ await updateOne(
278
+ this.prisma,
279
+ 'User',
280
+ { _id: objectId, type: 'INDIVIDUAL' },
281
+ { $set: encryptedPayload }
282
+ );
283
+
284
+ const updated = await findOne(this.prisma, 'User', { _id: objectId });
285
+
286
+ // Defensive check: verify document was found after update
287
+ if (!updated) {
288
+ console.error(
289
+ '[UserRepositoryDocumentDB] Individual user not found after update',
290
+ {
291
+ userId: fromObjectId(objectId),
292
+ updates,
293
+ }
294
+ );
295
+ throw new Error(
296
+ 'Failed to update individual user: Document not found after update. ' +
297
+ 'This indicates a database consistency issue.'
298
+ );
299
+ }
300
+
301
+ const decrypted = await this.encryptionService.decryptFields(
302
+ 'User',
303
+ updated
304
+ );
305
+ return this._mapUser(decrypted);
306
+ }
307
+
308
+ async updateOrganizationUser(userId, updates) {
309
+ const objectId = toObjectId(userId);
310
+ if (!objectId) return null;
311
+
312
+ const payload = { ...updates, updatedAt: new Date() };
313
+
314
+ const encryptedPayload = await this.encryptionService.encryptFields(
315
+ 'User',
316
+ payload
317
+ );
318
+
319
+ await updateOne(
320
+ this.prisma,
321
+ 'User',
322
+ { _id: objectId, type: 'ORGANIZATION' },
323
+ { $set: encryptedPayload }
324
+ );
325
+
326
+ const updated = await findOne(this.prisma, 'User', { _id: objectId });
327
+
328
+ if (!updated) {
329
+ console.error(
330
+ '[UserRepositoryDocumentDB] Organization user not found after update',
331
+ {
332
+ userId: fromObjectId(objectId),
333
+ updates,
334
+ }
335
+ );
336
+ throw new Error(
337
+ 'Failed to update organization user: Document not found after update. ' +
338
+ 'This indicates a database consistency issue.'
339
+ );
340
+ }
341
+
342
+ const decrypted = await this.encryptionService.decryptFields(
343
+ 'User',
344
+ updated
345
+ );
346
+ return this._mapUser(decrypted);
347
+ }
348
+
349
+ async deleteUser(userId) {
350
+ const objectId = toObjectId(userId);
351
+ if (!objectId) return false;
352
+
353
+ const result = await deleteOne(this.prisma, 'User', { _id: objectId });
354
+ const deleted = result?.n ?? 0;
355
+ return deleted > 0;
356
+ }
357
+
358
+ _mapUser(doc) {
359
+ if (!doc) {
360
+ console.warn(
361
+ '[UserRepositoryDocumentDB] _mapUser received null/undefined document'
362
+ );
363
+ return null;
364
+ }
365
+
366
+ // Use optional chaining for robustness
367
+ return {
368
+ id: fromObjectId(doc?._id),
369
+ type: doc?.type ?? null,
370
+ email: doc?.email ?? null,
371
+ username: doc?.username ?? null,
372
+ hashword: doc?.hashword ?? null,
373
+ appUserId: doc?.appUserId ?? null,
374
+ organizationId: doc?.organizationId
375
+ ? fromObjectId(doc.organizationId)
376
+ : null,
377
+ appOrgId: doc?.appOrgId ?? null,
378
+ name: doc?.name ?? null,
379
+ createdAt: this._parseDate(doc?.createdAt),
380
+ updatedAt: this._parseDate(doc?.updatedAt),
381
+ };
382
+ }
383
+
384
+ async _prepareUpdatePayload(updates = {}) {
385
+ const payload = { ...updates };
386
+
387
+ if (
388
+ payload.hashword !== undefined &&
389
+ payload.hashword !== null &&
390
+ payload.hashword !== ''
391
+ ) {
392
+ if (typeof payload.hashword !== 'string') {
393
+ throw new ClientSafeError('Password must be a string', 400);
394
+ }
395
+
396
+ if (payload.hashword.startsWith('$2')) {
397
+ throw new Error(
398
+ 'Password appears to be already hashed. Pass plain text password only.'
399
+ );
400
+ }
401
+
402
+ payload.hashword = await bcrypt.hash(payload.hashword, 10);
403
+ }
404
+
405
+ if (payload.organization !== undefined) {
406
+ payload.organizationId = toObjectId(payload.organization);
407
+ delete payload.organization;
408
+ }
409
+
410
+ if (payload.organizationId !== undefined) {
411
+ payload.organizationId = payload.organizationId
412
+ ? toObjectId(payload.organizationId)
413
+ : null;
414
+ }
415
+
416
+ return payload;
417
+ }
418
+
419
+ /**
420
+ * Parse date value safely, returning undefined for invalid dates
421
+ * @private
422
+ * @param {*} value - Date value from database
423
+ * @returns {Date|undefined} Valid Date object or undefined
424
+ */
425
+ _parseDate(value) {
426
+ if (!value) return undefined;
427
+ const date = new Date(value);
428
+ return isNaN(date.getTime()) ? undefined : date;
429
+ }
430
+ }
431
+
432
+ module.exports = { UserRepositoryDocumentDB };
@@ -1,5 +1,6 @@
1
1
  const { UserRepositoryMongo } = require('./user-repository-mongo');
2
2
  const { UserRepositoryPostgres } = require('./user-repository-postgres');
3
+ const { UserRepositoryDocumentDB } = require('./user-repository-documentdb');
3
4
  const databaseConfig = require('../../database/config');
4
5
 
5
6
  /**
@@ -31,9 +32,12 @@ function createUserRepository() {
31
32
  case 'postgresql':
32
33
  return new UserRepositoryPostgres();
33
34
 
35
+ case 'documentdb':
36
+ return new UserRepositoryDocumentDB();
37
+
34
38
  default:
35
39
  throw new Error(
36
- `Unsupported DB_TYPE: ${dbType}. Supported values: 'mongodb', 'postgresql'`
40
+ `Unsupported DB_TYPE: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
37
41
  );
38
42
  }
39
43
  }
@@ -43,4 +47,5 @@ module.exports = {
43
47
  // Export adapters for direct testing
44
48
  UserRepositoryMongo,
45
49
  UserRepositoryPostgres,
50
+ UserRepositoryDocumentDB,
46
51
  };
@@ -4,6 +4,7 @@ const {
4
4
  createTokenRepository,
5
5
  } = require('../../token/repositories/token-repository-factory');
6
6
  const { UserRepositoryInterface } = require('./user-repository-interface');
7
+ const { ClientSafeError } = require('../../errors');
7
8
 
8
9
  /**
9
10
  * MongoDB User Repository Adapter
@@ -113,7 +114,7 @@ class UserRepositoryMongo extends UserRepositoryInterface {
113
114
  params.hashword !== ''
114
115
  ) {
115
116
  if (typeof params.hashword !== 'string') {
116
- throw new Error('Password must be a string');
117
+ throw new ClientSafeError('Password must be a string', 400);
117
118
  }
118
119
 
119
120
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -235,7 +236,7 @@ class UserRepositoryMongo extends UserRepositoryInterface {
235
236
  data.hashword !== ''
236
237
  ) {
237
238
  if (typeof data.hashword !== 'string') {
238
- throw new Error('Password must be a string');
239
+ throw new ClientSafeError('Password must be a string', 400);
239
240
  }
240
241
 
241
242
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -4,6 +4,7 @@ const {
4
4
  createTokenRepository,
5
5
  } = require('../../token/repositories/token-repository-factory');
6
6
  const { UserRepositoryInterface } = require('./user-repository-interface');
7
+ const { ClientSafeError } = require('../../errors');
7
8
 
8
9
  /**
9
10
  * PostgreSQL User Repository Adapter
@@ -150,7 +151,7 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
150
151
  params.hashword !== ''
151
152
  ) {
152
153
  if (typeof params.hashword !== 'string') {
153
- throw new Error('Password must be a string');
154
+ throw new ClientSafeError('Password must be a string', 400);
154
155
  }
155
156
 
156
157
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -290,7 +291,7 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
290
291
  data.hashword !== ''
291
292
  ) {
292
293
  if (typeof data.hashword !== 'string') {
293
- throw new Error('Password must be a string');
294
+ throw new ClientSafeError('Password must be a string', 400);
294
295
  }
295
296
 
296
297
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -115,7 +115,7 @@ class LoginUser {
115
115
  return organizationUser;
116
116
  }
117
117
 
118
- return null;
118
+ throw new Error('User configuration must require either individualUserRequired or organizationUserRequired');
119
119
  }
120
120
  }
121
121
 
@@ -0,0 +1,119 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ApiGatewayManagementApiClient,
4
+ PostToConnectionCommand,
5
+ } = require('@aws-sdk/client-apigatewaymanagementapi');
6
+ const {
7
+ toObjectId,
8
+ fromObjectId,
9
+ findMany,
10
+ findOne,
11
+ insertOne,
12
+ deleteOne,
13
+ deleteMany,
14
+ } = require('../../database/documentdb-utils');
15
+ const {
16
+ WebsocketConnectionRepositoryInterface,
17
+ } = require('./websocket-connection-repository-interface');
18
+
19
+ class WebsocketConnectionRepositoryDocumentDB extends WebsocketConnectionRepositoryInterface {
20
+ constructor() {
21
+ super();
22
+ this.prisma = prisma;
23
+ }
24
+
25
+ async createConnection(connectionId) {
26
+ const now = new Date();
27
+ const document = {
28
+ connectionId,
29
+ createdAt: now,
30
+ updatedAt: now,
31
+ };
32
+ const insertedId = await insertOne(this.prisma, 'WebsocketConnection', document);
33
+ const created = await findOne(this.prisma, 'WebsocketConnection', { _id: insertedId });
34
+ return this._mapConnection(created);
35
+ }
36
+
37
+ async deleteConnection(connectionId) {
38
+ const result = await deleteOne(this.prisma, 'WebsocketConnection', { connectionId });
39
+ const deleted = result?.n ?? 0;
40
+ return { acknowledged: true, deletedCount: deleted };
41
+ }
42
+
43
+ async getActiveConnections() {
44
+ if (!process.env.WEBSOCKET_API_ENDPOINT) {
45
+ return [];
46
+ }
47
+
48
+ const connections = await findMany(
49
+ this.prisma,
50
+ 'WebsocketConnection',
51
+ {},
52
+ { projection: { connectionId: 1 } }
53
+ );
54
+
55
+ return connections.map((conn) => ({
56
+ connectionId: conn.connectionId,
57
+ send: async (data) => {
58
+ const apigwManagementApi = new ApiGatewayManagementApiClient({
59
+ endpoint: process.env.WEBSOCKET_API_ENDPOINT,
60
+ });
61
+
62
+ try {
63
+ const command = new PostToConnectionCommand({
64
+ ConnectionId: conn.connectionId,
65
+ Data: JSON.stringify(data),
66
+ });
67
+ await apigwManagementApi.send(command);
68
+ } catch (error) {
69
+ if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
70
+ console.log(`Stale connection ${conn.connectionId}`);
71
+ await deleteMany(this.prisma, 'WebsocketConnection', {
72
+ connectionId: conn.connectionId,
73
+ });
74
+ } else {
75
+ throw error;
76
+ }
77
+ }
78
+ },
79
+ }));
80
+ }
81
+
82
+ async findConnection(connectionId) {
83
+ const doc = await findOne(this.prisma, 'WebsocketConnection', { connectionId });
84
+ return doc ? this._mapConnection(doc) : null;
85
+ }
86
+
87
+ async findConnectionById(id) {
88
+ const objectId = toObjectId(id);
89
+ if (!objectId) return null;
90
+ const doc = await findOne(this.prisma, 'WebsocketConnection', { _id: objectId });
91
+ return doc ? this._mapConnection(doc) : null;
92
+ }
93
+
94
+ async getAllConnections() {
95
+ const docs = await findMany(this.prisma, 'WebsocketConnection');
96
+ return docs.map((doc) => this._mapConnection(doc));
97
+ }
98
+
99
+ async deleteAllConnections() {
100
+ const result = await deleteMany(this.prisma, 'WebsocketConnection', {});
101
+ const deleted = result?.n ?? 0;
102
+ return {
103
+ acknowledged: true,
104
+ deletedCount: deleted,
105
+ };
106
+ }
107
+
108
+ _mapConnection(doc) {
109
+ if (!doc) return null;
110
+ return {
111
+ id: fromObjectId(doc._id),
112
+ connectionId: doc.connectionId,
113
+ };
114
+ }
115
+ }
116
+
117
+ module.exports = { WebsocketConnectionRepositoryDocumentDB };
118
+
119
+
@@ -4,6 +4,9 @@ const {
4
4
  const {
5
5
  WebsocketConnectionRepositoryPostgres,
6
6
  } = require('./websocket-connection-repository-postgres');
7
+ const {
8
+ WebsocketConnectionRepositoryDocumentDB,
9
+ } = require('./websocket-connection-repository-documentdb');
7
10
  const config = require('../../database/config');
8
11
 
9
12
  /**
@@ -22,9 +25,12 @@ function createWebsocketConnectionRepository() {
22
25
  case 'postgresql':
23
26
  return new WebsocketConnectionRepositoryPostgres();
24
27
 
28
+ case 'documentdb':
29
+ return new WebsocketConnectionRepositoryDocumentDB();
30
+
25
31
  default:
26
32
  throw new Error(
27
- `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
33
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
28
34
  );
29
35
  }
30
36
  }
@@ -34,4 +40,5 @@ module.exports = {
34
40
  // Export adapters for direct testing
35
41
  WebsocketConnectionRepositoryMongo,
36
42
  WebsocketConnectionRepositoryPostgres,
43
+ WebsocketConnectionRepositoryDocumentDB,
37
44
  };