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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/CLAUDE.md +694 -0
  2. package/README.md +959 -50
  3. package/application/commands/README.md +451 -0
  4. package/application/commands/credential-commands.js +245 -0
  5. package/application/commands/entity-commands.js +336 -0
  6. package/application/commands/integration-commands.js +210 -0
  7. package/application/commands/user-commands.js +283 -0
  8. package/application/index.js +69 -0
  9. package/core/CLAUDE.md +690 -0
  10. package/core/Worker.js +8 -21
  11. package/core/create-handler.js +14 -7
  12. package/credential/repositories/credential-repository-documentdb.js +304 -0
  13. package/credential/repositories/credential-repository-factory.js +54 -0
  14. package/credential/repositories/credential-repository-interface.js +98 -0
  15. package/credential/repositories/credential-repository-mongo.js +269 -0
  16. package/credential/repositories/credential-repository-postgres.js +291 -0
  17. package/credential/repositories/credential-repository.js +302 -0
  18. package/credential/use-cases/get-credential-for-user.js +25 -0
  19. package/credential/use-cases/update-authentication-status.js +15 -0
  20. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  21. package/database/adapters/lambda-invoker.js +97 -0
  22. package/database/config.js +154 -0
  23. package/database/documentdb-encryption-service.js +330 -0
  24. package/database/documentdb-utils.js +136 -0
  25. package/database/encryption/README.md +839 -0
  26. package/database/encryption/documentdb-encryption-service.md +3575 -0
  27. package/database/encryption/encryption-schema-registry.js +268 -0
  28. package/database/encryption/field-encryption-service.js +226 -0
  29. package/database/encryption/logger.js +79 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/index.js +61 -21
  32. package/database/models/WebsocketConnection.js +16 -10
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +182 -0
  35. package/database/repositories/health-check-repository-documentdb.js +134 -0
  36. package/database/repositories/health-check-repository-factory.js +48 -0
  37. package/database/repositories/health-check-repository-interface.js +82 -0
  38. package/database/repositories/health-check-repository-mongodb.js +89 -0
  39. package/database/repositories/health-check-repository-postgres.js +82 -0
  40. package/database/repositories/health-check-repository.js +108 -0
  41. package/database/repositories/migration-status-repository-s3.js +137 -0
  42. package/database/use-cases/check-database-health-use-case.js +29 -0
  43. package/database/use-cases/check-database-state-use-case.js +81 -0
  44. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  45. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  46. package/database/use-cases/get-migration-status-use-case.js +93 -0
  47. package/database/use-cases/run-database-migration-use-case.js +139 -0
  48. package/database/use-cases/test-encryption-use-case.js +253 -0
  49. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  50. package/database/utils/mongodb-collection-utils.js +91 -0
  51. package/database/utils/mongodb-schema-init.js +106 -0
  52. package/database/utils/prisma-runner.js +477 -0
  53. package/database/utils/prisma-schema-parser.js +182 -0
  54. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  55. package/encrypt/Cryptor.js +34 -168
  56. package/encrypt/index.js +1 -2
  57. package/encrypt/test-encrypt.js +0 -2
  58. package/errors/client-safe-error.js +26 -0
  59. package/errors/fetch-error.js +2 -1
  60. package/errors/index.js +2 -0
  61. package/generated/prisma-mongodb/client.d.ts +1 -0
  62. package/generated/prisma-mongodb/client.js +4 -0
  63. package/generated/prisma-mongodb/default.d.ts +1 -0
  64. package/generated/prisma-mongodb/default.js +4 -0
  65. package/generated/prisma-mongodb/edge.d.ts +1 -0
  66. package/generated/prisma-mongodb/edge.js +334 -0
  67. package/generated/prisma-mongodb/index-browser.js +316 -0
  68. package/generated/prisma-mongodb/index.d.ts +22903 -0
  69. package/generated/prisma-mongodb/index.js +359 -0
  70. package/generated/prisma-mongodb/package.json +183 -0
  71. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  72. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  73. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  74. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  75. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  76. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  77. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  78. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  79. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  80. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  81. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  82. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  83. package/generated/prisma-mongodb/schema.prisma +360 -0
  84. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  85. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  86. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  87. package/generated/prisma-mongodb/wasm.js +341 -0
  88. package/generated/prisma-postgresql/client.d.ts +1 -0
  89. package/generated/prisma-postgresql/client.js +4 -0
  90. package/generated/prisma-postgresql/default.d.ts +1 -0
  91. package/generated/prisma-postgresql/default.js +4 -0
  92. package/generated/prisma-postgresql/edge.d.ts +1 -0
  93. package/generated/prisma-postgresql/edge.js +356 -0
  94. package/generated/prisma-postgresql/index-browser.js +338 -0
  95. package/generated/prisma-postgresql/index.d.ts +25077 -0
  96. package/generated/prisma-postgresql/index.js +381 -0
  97. package/generated/prisma-postgresql/package.json +183 -0
  98. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  99. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  100. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  101. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  102. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  103. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  104. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  105. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  106. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  107. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  108. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  109. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  110. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  111. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  112. package/generated/prisma-postgresql/schema.prisma +343 -0
  113. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  114. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  115. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  116. package/generated/prisma-postgresql/wasm.js +363 -0
  117. package/handlers/WEBHOOKS.md +653 -0
  118. package/handlers/app-definition-loader.js +38 -0
  119. package/handlers/app-handler-helpers.js +56 -0
  120. package/handlers/backend-utils.js +186 -0
  121. package/handlers/database-migration-handler.js +227 -0
  122. package/handlers/integration-event-dispatcher.js +54 -0
  123. package/handlers/routers/HEALTHCHECK.md +342 -0
  124. package/handlers/routers/auth.js +15 -0
  125. package/handlers/routers/db-migration.handler.js +29 -0
  126. package/handlers/routers/db-migration.js +326 -0
  127. package/handlers/routers/health.js +516 -0
  128. package/handlers/routers/integration-defined-routers.js +45 -0
  129. package/handlers/routers/integration-webhook-routers.js +67 -0
  130. package/handlers/routers/user.js +63 -0
  131. package/handlers/routers/websocket.js +57 -0
  132. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  133. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  134. package/handlers/workers/db-migration.js +352 -0
  135. package/handlers/workers/integration-defined-workers.js +27 -0
  136. package/index.js +77 -22
  137. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  138. package/integrations/index.js +12 -10
  139. package/integrations/integration-base.js +326 -55
  140. package/integrations/integration-router.js +374 -179
  141. package/integrations/options.js +1 -1
  142. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  143. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  144. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  145. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  146. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  147. package/integrations/repositories/integration-mapping-repository.js +156 -0
  148. package/integrations/repositories/integration-repository-documentdb.js +210 -0
  149. package/integrations/repositories/integration-repository-factory.js +51 -0
  150. package/integrations/repositories/integration-repository-interface.js +127 -0
  151. package/integrations/repositories/integration-repository-mongo.js +303 -0
  152. package/integrations/repositories/integration-repository-postgres.js +352 -0
  153. package/integrations/repositories/process-repository-documentdb.js +243 -0
  154. package/integrations/repositories/process-repository-factory.js +53 -0
  155. package/integrations/repositories/process-repository-interface.js +90 -0
  156. package/integrations/repositories/process-repository-mongo.js +190 -0
  157. package/integrations/repositories/process-repository-postgres.js +217 -0
  158. package/integrations/tests/doubles/dummy-integration-class.js +83 -0
  159. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  160. package/integrations/use-cases/create-integration.js +83 -0
  161. package/integrations/use-cases/create-process.js +128 -0
  162. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  163. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  164. package/integrations/use-cases/get-integration-for-user.js +78 -0
  165. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  166. package/integrations/use-cases/get-integration-instance.js +83 -0
  167. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  168. package/integrations/use-cases/get-possible-integrations.js +27 -0
  169. package/integrations/use-cases/get-process.js +87 -0
  170. package/integrations/use-cases/index.js +19 -0
  171. package/integrations/use-cases/load-integration-context.js +71 -0
  172. package/integrations/use-cases/update-integration-messages.js +44 -0
  173. package/integrations/use-cases/update-integration-status.js +32 -0
  174. package/integrations/use-cases/update-integration.js +93 -0
  175. package/integrations/use-cases/update-process-metrics.js +201 -0
  176. package/integrations/use-cases/update-process-state.js +119 -0
  177. package/integrations/utils/map-integration-dto.js +37 -0
  178. package/jest-global-setup-noop.js +3 -0
  179. package/jest-global-teardown-noop.js +3 -0
  180. package/logs/logger.js +0 -4
  181. package/{module-plugin → modules}/entity.js +1 -1
  182. package/{module-plugin → modules}/index.js +0 -8
  183. package/modules/module-factory.js +56 -0
  184. package/modules/module.js +221 -0
  185. package/modules/repositories/module-repository-documentdb.js +307 -0
  186. package/modules/repositories/module-repository-factory.js +40 -0
  187. package/modules/repositories/module-repository-interface.js +129 -0
  188. package/modules/repositories/module-repository-mongo.js +377 -0
  189. package/modules/repositories/module-repository-postgres.js +426 -0
  190. package/modules/repositories/module-repository.js +316 -0
  191. package/modules/requester/api-key.js +52 -0
  192. package/{module-plugin → modules}/requester/requester.js +1 -0
  193. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  194. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  195. package/modules/tests/doubles/test-module-factory.js +16 -0
  196. package/modules/tests/doubles/test-module-repository.js +39 -0
  197. package/modules/use-cases/get-entities-for-user.js +32 -0
  198. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  199. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  200. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  201. package/modules/use-cases/get-module.js +74 -0
  202. package/modules/use-cases/process-authorization-callback.js +133 -0
  203. package/modules/use-cases/refresh-entity-options.js +72 -0
  204. package/modules/use-cases/test-module-auth.js +72 -0
  205. package/modules/utils/map-module-dto.js +18 -0
  206. package/package.json +82 -50
  207. package/prisma-mongodb/schema.prisma +360 -0
  208. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  209. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  210. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  211. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  212. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  213. package/prisma-postgresql/schema.prisma +343 -0
  214. package/queues/queuer-util.js +27 -22
  215. package/syncs/manager.js +468 -443
  216. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  217. package/syncs/repositories/sync-repository-factory.js +43 -0
  218. package/syncs/repositories/sync-repository-interface.js +109 -0
  219. package/syncs/repositories/sync-repository-mongo.js +239 -0
  220. package/syncs/repositories/sync-repository-postgres.js +319 -0
  221. package/syncs/sync.js +0 -1
  222. package/token/repositories/token-repository-documentdb.js +137 -0
  223. package/token/repositories/token-repository-factory.js +40 -0
  224. package/token/repositories/token-repository-interface.js +131 -0
  225. package/token/repositories/token-repository-mongo.js +219 -0
  226. package/token/repositories/token-repository-postgres.js +264 -0
  227. package/token/repositories/token-repository.js +219 -0
  228. package/types/core/index.d.ts +2 -2
  229. package/types/integrations/index.d.ts +2 -6
  230. package/types/module-plugin/index.d.ts +5 -59
  231. package/types/syncs/index.d.ts +0 -2
  232. package/user/repositories/user-repository-documentdb.js +441 -0
  233. package/user/repositories/user-repository-factory.js +52 -0
  234. package/user/repositories/user-repository-interface.js +201 -0
  235. package/user/repositories/user-repository-mongo.js +308 -0
  236. package/user/repositories/user-repository-postgres.js +360 -0
  237. package/user/tests/doubles/test-user-repository.js +72 -0
  238. package/user/use-cases/authenticate-user.js +127 -0
  239. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  240. package/user/use-cases/create-individual-user.js +61 -0
  241. package/user/use-cases/create-organization-user.js +47 -0
  242. package/user/use-cases/create-token-for-user-id.js +30 -0
  243. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  244. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  245. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  246. package/user/use-cases/login-user.js +122 -0
  247. package/user/user.js +125 -0
  248. package/utils/backend-path.js +38 -0
  249. package/utils/index.js +6 -0
  250. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  251. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  252. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  253. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  254. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  255. package/websocket/repositories/websocket-connection-repository.js +161 -0
  256. package/database/models/State.js +0 -9
  257. package/database/models/Token.js +0 -70
  258. package/database/mongo.js +0 -45
  259. package/encrypt/Cryptor.test.js +0 -32
  260. package/encrypt/encrypt.js +0 -132
  261. package/encrypt/encrypt.test.js +0 -1069
  262. package/errors/base-error.test.js +0 -32
  263. package/errors/fetch-error.test.js +0 -79
  264. package/errors/halt-error.test.js +0 -11
  265. package/errors/validation-errors.test.js +0 -120
  266. package/integrations/create-frigg-backend.js +0 -31
  267. package/integrations/integration-factory.js +0 -251
  268. package/integrations/integration-mapping.js +0 -43
  269. package/integrations/integration-model.js +0 -46
  270. package/integrations/integration-user.js +0 -144
  271. package/integrations/test/integration-base.test.js +0 -144
  272. package/lambda/TimeoutCatcher.test.js +0 -68
  273. package/logs/logger.test.js +0 -76
  274. package/module-plugin/auther.js +0 -393
  275. package/module-plugin/credential.js +0 -22
  276. package/module-plugin/entity-manager.js +0 -70
  277. package/module-plugin/manager.js +0 -169
  278. package/module-plugin/module-factory.js +0 -61
  279. package/module-plugin/requester/api-key.js +0 -36
  280. package/module-plugin/requester/requester.test.js +0 -28
  281. package/module-plugin/test/auther.test.js +0 -97
  282. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  283. /package/{module-plugin → modules}/requester/basic.js +0 -0
  284. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  285. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,38 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('node:path');
3
+ const PACKAGE_JSON = 'package.json';
4
+
5
+ function findNearestBackendPackageJson() {
6
+ let currentDir = process.cwd();
7
+
8
+ // First check if we're in production by looking for package.json in the current directory
9
+ const rootPackageJson = path.join(currentDir, PACKAGE_JSON);
10
+ if (fs.existsSync(rootPackageJson)) {
11
+ // In production environment, check for index.js in the same directory
12
+ const indexJs = path.join(currentDir, 'index.js');
13
+ if (fs.existsSync(indexJs)) {
14
+ return rootPackageJson;
15
+ }
16
+ }
17
+
18
+ // If not found at root or not in production, look for it in the backend directory
19
+ while (currentDir !== path.parse(currentDir).root) {
20
+ const packageJsonPath = path.join(currentDir, 'backend', PACKAGE_JSON);
21
+ if (fs.existsSync(packageJsonPath)) {
22
+ return packageJsonPath;
23
+ }
24
+ currentDir = path.dirname(currentDir);
25
+ }
26
+ return null;
27
+ }
28
+
29
+ function validateBackendPath(backendPath) {
30
+ if (!backendPath) {
31
+ throw new Error('Could not find a backend package.json file.');
32
+ }
33
+ }
34
+
35
+ module.exports = {
36
+ findNearestBackendPackageJson,
37
+ validateBackendPath,
38
+ };
package/utils/index.js ADDED
@@ -0,0 +1,6 @@
1
+ const { findNearestBackendPackageJson, validateBackendPath } = require('./backend-path');
2
+
3
+ module.exports = {
4
+ findNearestBackendPackageJson,
5
+ validateBackendPath,
6
+ };
@@ -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
+
@@ -0,0 +1,44 @@
1
+ const {
2
+ WebsocketConnectionRepositoryMongo,
3
+ } = require('./websocket-connection-repository-mongo');
4
+ const {
5
+ WebsocketConnectionRepositoryPostgres,
6
+ } = require('./websocket-connection-repository-postgres');
7
+ const {
8
+ WebsocketConnectionRepositoryDocumentDB,
9
+ } = require('./websocket-connection-repository-documentdb');
10
+ const config = require('../../database/config');
11
+
12
+ /**
13
+ * Websocket Connection Repository Factory
14
+ * Creates the appropriate repository adapter based on database type
15
+ *
16
+ * @returns {WebsocketConnectionRepositoryInterface} Configured repository adapter
17
+ */
18
+ function createWebsocketConnectionRepository() {
19
+ const dbType = config.DB_TYPE;
20
+
21
+ switch (dbType) {
22
+ case 'mongodb':
23
+ return new WebsocketConnectionRepositoryMongo();
24
+
25
+ case 'postgresql':
26
+ return new WebsocketConnectionRepositoryPostgres();
27
+
28
+ case 'documentdb':
29
+ return new WebsocketConnectionRepositoryDocumentDB();
30
+
31
+ default:
32
+ throw new Error(
33
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
34
+ );
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ createWebsocketConnectionRepository,
40
+ // Export adapters for direct testing
41
+ WebsocketConnectionRepositoryMongo,
42
+ WebsocketConnectionRepositoryPostgres,
43
+ WebsocketConnectionRepositoryDocumentDB,
44
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Websocket Connection Repository Interface
3
+ * Abstract base class defining the contract for websocket connection persistence adapters
4
+ *
5
+ * This follows the Port in Hexagonal Architecture:
6
+ * - Domain layer depends on this abstraction
7
+ * - Concrete adapters implement this interface
8
+ * - Use cases receive repositories via dependency injection
9
+ *
10
+ * Note: Currently, WebsocketConnection model has identical structure across MongoDB and PostgreSQL,
11
+ * so WebsocketConnectionRepository serves both. This interface exists for consistency and
12
+ * future-proofing if database-specific implementations become needed.
13
+ *
14
+ * @abstract
15
+ */
16
+ class WebsocketConnectionRepositoryInterface {
17
+ /**
18
+ * Create a new websocket connection
19
+ *
20
+ * @param {string} connectionId - Connection ID
21
+ * @returns {Promise<Object>} Created connection object
22
+ * @abstract
23
+ */
24
+ async createConnection(connectionId) {
25
+ throw new Error(
26
+ 'Method createConnection must be implemented by subclass'
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Delete a websocket connection
32
+ *
33
+ * @param {string} connectionId - Connection ID
34
+ * @returns {Promise<Object>} Deletion result
35
+ * @abstract
36
+ */
37
+ async deleteConnection(connectionId) {
38
+ throw new Error(
39
+ 'Method deleteConnection must be implemented by subclass'
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Get active connections
45
+ *
46
+ * @returns {Promise<Array>} Array of active connection objects
47
+ * @abstract
48
+ */
49
+ async getActiveConnections() {
50
+ throw new Error(
51
+ 'Method getActiveConnections must be implemented by subclass'
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Find connection by connection ID
57
+ *
58
+ * @param {string} connectionId - Connection ID
59
+ * @returns {Promise<Object|null>} Connection object or null
60
+ * @abstract
61
+ */
62
+ async findConnection(connectionId) {
63
+ throw new Error(
64
+ 'Method findConnection must be implemented by subclass'
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Find connection by database ID
70
+ *
71
+ * @param {string|number} id - Database ID
72
+ * @returns {Promise<Object|null>} Connection object or null
73
+ * @abstract
74
+ */
75
+ async findConnectionById(id) {
76
+ throw new Error(
77
+ 'Method findConnectionById must be implemented by subclass'
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Get all connections
83
+ *
84
+ * @returns {Promise<Array>} Array of all connection objects
85
+ * @abstract
86
+ */
87
+ async getAllConnections() {
88
+ throw new Error(
89
+ 'Method getAllConnections must be implemented by subclass'
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Delete all connections
95
+ *
96
+ * @returns {Promise<Object>} Deletion result
97
+ * @abstract
98
+ */
99
+ async deleteAllConnections() {
100
+ throw new Error(
101
+ 'Method deleteAllConnections must be implemented by subclass'
102
+ );
103
+ }
104
+ }
105
+
106
+ module.exports = { WebsocketConnectionRepositoryInterface };
@@ -0,0 +1,156 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ApiGatewayManagementApiClient,
4
+ PostToConnectionCommand,
5
+ } = require('@aws-sdk/client-apigatewaymanagementapi');
6
+ const {
7
+ WebsocketConnectionRepositoryInterface,
8
+ } = require('./websocket-connection-repository-interface');
9
+
10
+ /**
11
+ * MongoDB WebSocket Connection Repository Adapter
12
+ * Handles persistence of active WebSocket connections
13
+ *
14
+ * MongoDB-specific characteristics:
15
+ * - Uses String IDs (ObjectId)
16
+ * - No ID conversion needed (IDs are already strings)
17
+ * - AWS API Gateway Management API integration preserved
18
+ */
19
+ class WebsocketConnectionRepositoryMongo extends WebsocketConnectionRepositoryInterface {
20
+ constructor() {
21
+ super();
22
+ this.prisma = prisma;
23
+ }
24
+
25
+ /**
26
+ * Create a new WebSocket connection record
27
+ * Replaces: WebsocketConnection.create({ connectionId })
28
+ *
29
+ * @param {string} connectionId - The WebSocket connection ID
30
+ * @returns {Promise<Object>} The created connection record with string IDs
31
+ */
32
+ async createConnection(connectionId) {
33
+ return await this.prisma.websocketConnection.create({
34
+ data: { connectionId },
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Delete a WebSocket connection record
40
+ * Replaces: WebsocketConnection.deleteOne({ connectionId })
41
+ *
42
+ * @param {string} connectionId - The WebSocket connection ID to delete
43
+ * @returns {Promise<Object>} The deletion result
44
+ */
45
+ async deleteConnection(connectionId) {
46
+ try {
47
+ await this.prisma.websocketConnection.delete({
48
+ where: { connectionId },
49
+ });
50
+ return { acknowledged: true, deletedCount: 1 };
51
+ } catch (error) {
52
+ if (error.code === 'P2025') {
53
+ // Record not found
54
+ return { acknowledged: true, deletedCount: 0 };
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get all active WebSocket connections with send capability
62
+ * Replaces: WebsocketConnection.getActiveConnections()
63
+ *
64
+ * @returns {Promise<Array>} Array of active connection objects with send capability
65
+ */
66
+ async getActiveConnections() {
67
+ try {
68
+ // Return empty array if websockets are not configured
69
+ if (!process.env.WEBSOCKET_API_ENDPOINT) {
70
+ return [];
71
+ }
72
+
73
+ const connections = await this.prisma.websocketConnection.findMany({
74
+ select: { connectionId: true },
75
+ });
76
+
77
+ return connections.map((conn) => ({
78
+ connectionId: conn.connectionId,
79
+ send: async (data) => {
80
+ const apigwManagementApi = new ApiGatewayManagementApiClient({
81
+ endpoint: process.env.WEBSOCKET_API_ENDPOINT,
82
+ });
83
+
84
+ try {
85
+ const command = new PostToConnectionCommand({
86
+ ConnectionId: conn.connectionId,
87
+ Data: JSON.stringify(data),
88
+ });
89
+ await apigwManagementApi.send(command);
90
+ } catch (error) {
91
+ if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
92
+ console.log(
93
+ `Stale connection ${conn.connectionId}`
94
+ );
95
+ // Delete stale connection
96
+ await this.prisma.websocketConnection.deleteMany({
97
+ where: { connectionId: conn.connectionId },
98
+ });
99
+ } else {
100
+ throw error;
101
+ }
102
+ }
103
+ },
104
+ }));
105
+ } catch (error) {
106
+ console.error('Error getting active connections:', error);
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Find a connection by connection ID
113
+ * Replaces: WebsocketConnection.findOne({ connectionId })
114
+ *
115
+ * @param {string} connectionId - The WebSocket connection ID
116
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
117
+ */
118
+ async findConnection(connectionId) {
119
+ return await this.prisma.websocketConnection.findFirst({
120
+ where: { connectionId },
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Find connection by internal ID
126
+ * @param {string} id - The internal connection ID
127
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
128
+ */
129
+ async findConnectionById(id) {
130
+ return await this.prisma.websocketConnection.findUnique({
131
+ where: { id },
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Get all connections
137
+ * @returns {Promise<Array>} Array of all connection records with string IDs
138
+ */
139
+ async getAllConnections() {
140
+ return await this.prisma.websocketConnection.findMany();
141
+ }
142
+
143
+ /**
144
+ * Delete all connections
145
+ * @returns {Promise<Object>} The deletion result
146
+ */
147
+ async deleteAllConnections() {
148
+ const result = await this.prisma.websocketConnection.deleteMany();
149
+ return {
150
+ acknowledged: true,
151
+ deletedCount: result.count,
152
+ };
153
+ }
154
+ }
155
+
156
+ module.exports = { WebsocketConnectionRepositoryMongo };
@@ -0,0 +1,196 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ApiGatewayManagementApiClient,
4
+ PostToConnectionCommand,
5
+ } = require('@aws-sdk/client-apigatewaymanagementapi');
6
+ const {
7
+ WebsocketConnectionRepositoryInterface,
8
+ } = require('./websocket-connection-repository-interface');
9
+
10
+ /**
11
+ * PostgreSQL WebSocket Connection Repository Adapter
12
+ * Handles persistence of active WebSocket connections
13
+ *
14
+ * PostgreSQL-specific characteristics:
15
+ * - Uses Int IDs with autoincrement
16
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
17
+ * - All returned IDs are converted to strings for application layer consistency
18
+ */
19
+ class WebsocketConnectionRepositoryPostgres extends WebsocketConnectionRepositoryInterface {
20
+ constructor() {
21
+ super();
22
+ this.prisma = prisma;
23
+ }
24
+
25
+ /**
26
+ * Convert string ID to integer for PostgreSQL queries
27
+ * @private
28
+ * @param {string|number|null|undefined} id - ID to convert
29
+ * @returns {number|null|undefined} Integer ID or null/undefined
30
+ * @throws {Error} If ID cannot be converted to integer
31
+ */
32
+ _convertId(id) {
33
+ if (id === null || id === undefined) return id;
34
+ const parsed = parseInt(id, 10);
35
+ if (isNaN(parsed)) {
36
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
37
+ }
38
+ return parsed;
39
+ }
40
+
41
+ /**
42
+ * Convert connection object IDs to strings
43
+ * @private
44
+ * @param {Object|null} connection - Connection object from database
45
+ * @returns {Object|null} Connection with string IDs
46
+ */
47
+ _convertConnectionIds(connection) {
48
+ if (!connection) return connection;
49
+ return {
50
+ ...connection,
51
+ id: connection.id?.toString(),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Create a new WebSocket connection record
57
+ * Replaces: WebsocketConnection.create({ connectionId })
58
+ *
59
+ * @param {string} connectionId - The WebSocket connection ID
60
+ * @returns {Promise<Object>} The created connection record with string IDs
61
+ */
62
+ async createConnection(connectionId) {
63
+ const connection = await this.prisma.websocketConnection.create({
64
+ data: { connectionId },
65
+ });
66
+ return this._convertConnectionIds(connection);
67
+ }
68
+
69
+ /**
70
+ * Delete a WebSocket connection record
71
+ * Replaces: WebsocketConnection.deleteOne({ connectionId })
72
+ *
73
+ * Note: connectionId is a string field in schema, not the primary key,
74
+ * so no conversion needed here.
75
+ *
76
+ * @param {string} connectionId - The WebSocket connection ID to delete
77
+ * @returns {Promise<Object>} The deletion result
78
+ */
79
+ async deleteConnection(connectionId) {
80
+ try {
81
+ await this.prisma.websocketConnection.delete({
82
+ where: { connectionId },
83
+ });
84
+ return { acknowledged: true, deletedCount: 1 };
85
+ } catch (error) {
86
+ if (error.code === 'P2025') {
87
+ // Record not found
88
+ return { acknowledged: true, deletedCount: 0 };
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get all active WebSocket connections with send capability
96
+ * Replaces: WebsocketConnection.getActiveConnections()
97
+ *
98
+ * @returns {Promise<Array>} Array of active connection objects with send capability
99
+ */
100
+ async getActiveConnections() {
101
+ try {
102
+ // Return empty array if websockets are not configured
103
+ if (!process.env.WEBSOCKET_API_ENDPOINT) {
104
+ return [];
105
+ }
106
+
107
+ const connections = await this.prisma.websocketConnection.findMany({
108
+ select: { connectionId: true },
109
+ });
110
+
111
+ return connections.map((conn) => ({
112
+ connectionId: conn.connectionId,
113
+ send: async (data) => {
114
+ const apigwManagementApi = new ApiGatewayManagementApiClient({
115
+ endpoint: process.env.WEBSOCKET_API_ENDPOINT,
116
+ });
117
+
118
+ try {
119
+ const command = new PostToConnectionCommand({
120
+ ConnectionId: conn.connectionId,
121
+ Data: JSON.stringify(data),
122
+ });
123
+ await apigwManagementApi.send(command);
124
+ } catch (error) {
125
+ if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
126
+ console.log(
127
+ `Stale connection ${conn.connectionId}`
128
+ );
129
+ // Delete stale connection
130
+ await this.prisma.websocketConnection.deleteMany({
131
+ where: { connectionId: conn.connectionId },
132
+ });
133
+ } else {
134
+ throw error;
135
+ }
136
+ }
137
+ },
138
+ }));
139
+ } catch (error) {
140
+ console.error('Error getting active connections:', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Find a connection by connection ID
147
+ * Replaces: WebsocketConnection.findOne({ connectionId })
148
+ *
149
+ * Note: connectionId is a string field, not the primary key
150
+ *
151
+ * @param {string} connectionId - The WebSocket connection ID
152
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
153
+ */
154
+ async findConnection(connectionId) {
155
+ const connection = await this.prisma.websocketConnection.findFirst({
156
+ where: { connectionId },
157
+ });
158
+ return this._convertConnectionIds(connection);
159
+ }
160
+
161
+ /**
162
+ * Find connection by internal ID
163
+ * @param {string} id - The internal connection ID (string from application layer)
164
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
165
+ */
166
+ async findConnectionById(id) {
167
+ const intId = this._convertId(id);
168
+ const connection = await this.prisma.websocketConnection.findUnique({
169
+ where: { id: intId },
170
+ });
171
+ return this._convertConnectionIds(connection);
172
+ }
173
+
174
+ /**
175
+ * Get all connections
176
+ * @returns {Promise<Array>} Array of all connection records with string IDs
177
+ */
178
+ async getAllConnections() {
179
+ const connections = await this.prisma.websocketConnection.findMany();
180
+ return connections.map((conn) => this._convertConnectionIds(conn));
181
+ }
182
+
183
+ /**
184
+ * Delete all connections
185
+ * @returns {Promise<Object>} The deletion result
186
+ */
187
+ async deleteAllConnections() {
188
+ const result = await this.prisma.websocketConnection.deleteMany();
189
+ return {
190
+ acknowledged: true,
191
+ deletedCount: result.count,
192
+ };
193
+ }
194
+ }
195
+
196
+ module.exports = { WebsocketConnectionRepositoryPostgres };