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

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 (196) 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 +25 -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 +121 -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 +321 -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/migration_lock.toml +3 -0
  134. package/prisma-postgresql/schema.prisma +303 -0
  135. package/syncs/manager.js +468 -443
  136. package/syncs/repositories/sync-repository-factory.js +38 -0
  137. package/syncs/repositories/sync-repository-interface.js +109 -0
  138. package/syncs/repositories/sync-repository-mongo.js +239 -0
  139. package/syncs/repositories/sync-repository-postgres.js +319 -0
  140. package/syncs/sync.js +0 -1
  141. package/token/repositories/token-repository-factory.js +33 -0
  142. package/token/repositories/token-repository-interface.js +131 -0
  143. package/token/repositories/token-repository-mongo.js +212 -0
  144. package/token/repositories/token-repository-postgres.js +257 -0
  145. package/token/repositories/token-repository.js +219 -0
  146. package/types/integrations/index.d.ts +2 -6
  147. package/types/module-plugin/index.d.ts +5 -57
  148. package/types/syncs/index.d.ts +0 -2
  149. package/user/repositories/user-repository-factory.js +46 -0
  150. package/user/repositories/user-repository-interface.js +198 -0
  151. package/user/repositories/user-repository-mongo.js +250 -0
  152. package/user/repositories/user-repository-postgres.js +311 -0
  153. package/user/tests/doubles/test-user-repository.js +72 -0
  154. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  155. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  156. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  157. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  158. package/user/tests/use-cases/login-user.test.js +140 -0
  159. package/user/use-cases/create-individual-user.js +61 -0
  160. package/user/use-cases/create-organization-user.js +47 -0
  161. package/user/use-cases/create-token-for-user-id.js +30 -0
  162. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  163. package/user/use-cases/login-user.js +122 -0
  164. package/user/user.js +77 -0
  165. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  166. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  167. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  168. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  169. package/websocket/repositories/websocket-connection-repository.js +160 -0
  170. package/database/models/State.js +0 -9
  171. package/database/models/Token.js +0 -70
  172. package/database/mongo.js +0 -171
  173. package/encrypt/Cryptor.test.js +0 -32
  174. package/encrypt/encrypt.js +0 -104
  175. package/encrypt/encrypt.test.js +0 -1069
  176. package/handlers/routers/middleware/loadUser.js +0 -15
  177. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  178. package/integrations/create-frigg-backend.js +0 -31
  179. package/integrations/integration-factory.js +0 -251
  180. package/integrations/integration-mapping.js +0 -43
  181. package/integrations/integration-model.js +0 -46
  182. package/integrations/integration-user.js +0 -144
  183. package/integrations/test/integration-base.test.js +0 -144
  184. package/module-plugin/auther.js +0 -393
  185. package/module-plugin/credential.js +0 -22
  186. package/module-plugin/entity-manager.js +0 -70
  187. package/module-plugin/manager.js +0 -169
  188. package/module-plugin/module-factory.js +0 -61
  189. package/module-plugin/test/auther.test.js +0 -97
  190. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  191. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  192. /package/{module-plugin → modules}/requester/basic.js +0 -0
  193. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  194. /package/{module-plugin → modules}/requester/requester.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  196. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -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,155 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const AWS = require('aws-sdk');
3
+ const {
4
+ WebsocketConnectionRepositoryInterface,
5
+ } = require('./websocket-connection-repository-interface');
6
+
7
+ /**
8
+ * MongoDB WebSocket Connection Repository Adapter
9
+ * Handles persistence of active WebSocket connections
10
+ *
11
+ * MongoDB-specific characteristics:
12
+ * - Uses String IDs (ObjectId)
13
+ * - No ID conversion needed (IDs are already strings)
14
+ * - AWS API Gateway Management API integration preserved
15
+ */
16
+ class WebsocketConnectionRepositoryMongo extends WebsocketConnectionRepositoryInterface {
17
+ constructor() {
18
+ super();
19
+ this.prisma = prisma;
20
+ }
21
+
22
+ /**
23
+ * Create a new WebSocket connection record
24
+ * Replaces: WebsocketConnection.create({ connectionId })
25
+ *
26
+ * @param {string} connectionId - The WebSocket connection ID
27
+ * @returns {Promise<Object>} The created connection record with string IDs
28
+ */
29
+ async createConnection(connectionId) {
30
+ return await this.prisma.websocketConnection.create({
31
+ data: { connectionId },
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Delete a WebSocket connection record
37
+ * Replaces: WebsocketConnection.deleteOne({ connectionId })
38
+ *
39
+ * @param {string} connectionId - The WebSocket connection ID to delete
40
+ * @returns {Promise<Object>} The deletion result
41
+ */
42
+ async deleteConnection(connectionId) {
43
+ try {
44
+ await this.prisma.websocketConnection.delete({
45
+ where: { connectionId },
46
+ });
47
+ return { acknowledged: true, deletedCount: 1 };
48
+ } catch (error) {
49
+ if (error.code === 'P2025') {
50
+ // Record not found
51
+ return { acknowledged: true, deletedCount: 0 };
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get all active WebSocket connections with send capability
59
+ * Replaces: WebsocketConnection.getActiveConnections()
60
+ *
61
+ * @returns {Promise<Array>} Array of active connection objects with send capability
62
+ */
63
+ async getActiveConnections() {
64
+ try {
65
+ // Return empty array if websockets are not configured
66
+ if (!process.env.WEBSOCKET_API_ENDPOINT) {
67
+ return [];
68
+ }
69
+
70
+ const connections = await this.prisma.websocketConnection.findMany({
71
+ select: { connectionId: true },
72
+ });
73
+
74
+ return connections.map((conn) => ({
75
+ connectionId: conn.connectionId,
76
+ send: async (data) => {
77
+ const apigwManagementApi = new AWS.ApiGatewayManagementApi({
78
+ apiVersion: '2018-11-29',
79
+ endpoint: process.env.WEBSOCKET_API_ENDPOINT,
80
+ });
81
+
82
+ try {
83
+ await apigwManagementApi
84
+ .postToConnection({
85
+ ConnectionId: conn.connectionId,
86
+ Data: JSON.stringify(data),
87
+ })
88
+ .promise();
89
+ } catch (error) {
90
+ if (error.statusCode === 410) {
91
+ console.log(
92
+ `Stale connection ${conn.connectionId}`
93
+ );
94
+ // Delete stale connection
95
+ await this.prisma.websocketConnection.deleteMany({
96
+ where: { connectionId: conn.connectionId },
97
+ });
98
+ } else {
99
+ throw error;
100
+ }
101
+ }
102
+ },
103
+ }));
104
+ } catch (error) {
105
+ console.error('Error getting active connections:', error);
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Find a connection by connection ID
112
+ * Replaces: WebsocketConnection.findOne({ connectionId })
113
+ *
114
+ * @param {string} connectionId - The WebSocket connection ID
115
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
116
+ */
117
+ async findConnection(connectionId) {
118
+ return await this.prisma.websocketConnection.findFirst({
119
+ where: { connectionId },
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Find connection by internal ID
125
+ * @param {string} id - The internal connection ID
126
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
127
+ */
128
+ async findConnectionById(id) {
129
+ return await this.prisma.websocketConnection.findUnique({
130
+ where: { id },
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Get all connections
136
+ * @returns {Promise<Array>} Array of all connection records with string IDs
137
+ */
138
+ async getAllConnections() {
139
+ return await this.prisma.websocketConnection.findMany();
140
+ }
141
+
142
+ /**
143
+ * Delete all connections
144
+ * @returns {Promise<Object>} The deletion result
145
+ */
146
+ async deleteAllConnections() {
147
+ const result = await this.prisma.websocketConnection.deleteMany();
148
+ return {
149
+ acknowledged: true,
150
+ deletedCount: result.count,
151
+ };
152
+ }
153
+ }
154
+
155
+ module.exports = { WebsocketConnectionRepositoryMongo };
@@ -0,0 +1,195 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const AWS = require('aws-sdk');
3
+ const {
4
+ WebsocketConnectionRepositoryInterface,
5
+ } = require('./websocket-connection-repository-interface');
6
+
7
+ /**
8
+ * PostgreSQL WebSocket Connection Repository Adapter
9
+ * Handles persistence of active WebSocket connections
10
+ *
11
+ * PostgreSQL-specific characteristics:
12
+ * - Uses Int IDs with autoincrement
13
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
14
+ * - All returned IDs are converted to strings for application layer consistency
15
+ */
16
+ class WebsocketConnectionRepositoryPostgres extends WebsocketConnectionRepositoryInterface {
17
+ constructor() {
18
+ super();
19
+ this.prisma = prisma;
20
+ }
21
+
22
+ /**
23
+ * Convert string ID to integer for PostgreSQL queries
24
+ * @private
25
+ * @param {string|number|null|undefined} id - ID to convert
26
+ * @returns {number|null|undefined} Integer ID or null/undefined
27
+ * @throws {Error} If ID cannot be converted to integer
28
+ */
29
+ _convertId(id) {
30
+ if (id === null || id === undefined) return id;
31
+ const parsed = parseInt(id, 10);
32
+ if (isNaN(parsed)) {
33
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
34
+ }
35
+ return parsed;
36
+ }
37
+
38
+ /**
39
+ * Convert connection object IDs to strings
40
+ * @private
41
+ * @param {Object|null} connection - Connection object from database
42
+ * @returns {Object|null} Connection with string IDs
43
+ */
44
+ _convertConnectionIds(connection) {
45
+ if (!connection) return connection;
46
+ return {
47
+ ...connection,
48
+ id: connection.id?.toString(),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Create a new WebSocket connection record
54
+ * Replaces: WebsocketConnection.create({ connectionId })
55
+ *
56
+ * @param {string} connectionId - The WebSocket connection ID
57
+ * @returns {Promise<Object>} The created connection record with string IDs
58
+ */
59
+ async createConnection(connectionId) {
60
+ const connection = await this.prisma.websocketConnection.create({
61
+ data: { connectionId },
62
+ });
63
+ return this._convertConnectionIds(connection);
64
+ }
65
+
66
+ /**
67
+ * Delete a WebSocket connection record
68
+ * Replaces: WebsocketConnection.deleteOne({ connectionId })
69
+ *
70
+ * Note: connectionId is a string field in schema, not the primary key,
71
+ * so no conversion needed here.
72
+ *
73
+ * @param {string} connectionId - The WebSocket connection ID to delete
74
+ * @returns {Promise<Object>} The deletion result
75
+ */
76
+ async deleteConnection(connectionId) {
77
+ try {
78
+ await this.prisma.websocketConnection.delete({
79
+ where: { connectionId },
80
+ });
81
+ return { acknowledged: true, deletedCount: 1 };
82
+ } catch (error) {
83
+ if (error.code === 'P2025') {
84
+ // Record not found
85
+ return { acknowledged: true, deletedCount: 0 };
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get all active WebSocket connections with send capability
93
+ * Replaces: WebsocketConnection.getActiveConnections()
94
+ *
95
+ * @returns {Promise<Array>} Array of active connection objects with send capability
96
+ */
97
+ async getActiveConnections() {
98
+ try {
99
+ // Return empty array if websockets are not configured
100
+ if (!process.env.WEBSOCKET_API_ENDPOINT) {
101
+ return [];
102
+ }
103
+
104
+ const connections = await this.prisma.websocketConnection.findMany({
105
+ select: { connectionId: true },
106
+ });
107
+
108
+ return connections.map((conn) => ({
109
+ connectionId: conn.connectionId,
110
+ send: async (data) => {
111
+ const apigwManagementApi = new AWS.ApiGatewayManagementApi({
112
+ apiVersion: '2018-11-29',
113
+ endpoint: process.env.WEBSOCKET_API_ENDPOINT,
114
+ });
115
+
116
+ try {
117
+ await apigwManagementApi
118
+ .postToConnection({
119
+ ConnectionId: conn.connectionId,
120
+ Data: JSON.stringify(data),
121
+ })
122
+ .promise();
123
+ } catch (error) {
124
+ if (error.statusCode === 410) {
125
+ console.log(
126
+ `Stale connection ${conn.connectionId}`
127
+ );
128
+ // Delete stale connection
129
+ await this.prisma.websocketConnection.deleteMany({
130
+ where: { connectionId: conn.connectionId },
131
+ });
132
+ } else {
133
+ throw error;
134
+ }
135
+ }
136
+ },
137
+ }));
138
+ } catch (error) {
139
+ console.error('Error getting active connections:', error);
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Find a connection by connection ID
146
+ * Replaces: WebsocketConnection.findOne({ connectionId })
147
+ *
148
+ * Note: connectionId is a string field, not the primary key
149
+ *
150
+ * @param {string} connectionId - The WebSocket connection ID
151
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
152
+ */
153
+ async findConnection(connectionId) {
154
+ const connection = await this.prisma.websocketConnection.findFirst({
155
+ where: { connectionId },
156
+ });
157
+ return this._convertConnectionIds(connection);
158
+ }
159
+
160
+ /**
161
+ * Find connection by internal ID
162
+ * @param {string} id - The internal connection ID (string from application layer)
163
+ * @returns {Promise<Object|null>} The connection record with string IDs or null
164
+ */
165
+ async findConnectionById(id) {
166
+ const intId = this._convertId(id);
167
+ const connection = await this.prisma.websocketConnection.findUnique({
168
+ where: { id: intId },
169
+ });
170
+ return this._convertConnectionIds(connection);
171
+ }
172
+
173
+ /**
174
+ * Get all connections
175
+ * @returns {Promise<Array>} Array of all connection records with string IDs
176
+ */
177
+ async getAllConnections() {
178
+ const connections = await this.prisma.websocketConnection.findMany();
179
+ return connections.map((conn) => this._convertConnectionIds(conn));
180
+ }
181
+
182
+ /**
183
+ * Delete all connections
184
+ * @returns {Promise<Object>} The deletion result
185
+ */
186
+ async deleteAllConnections() {
187
+ const result = await this.prisma.websocketConnection.deleteMany();
188
+ return {
189
+ acknowledged: true,
190
+ deletedCount: result.count,
191
+ };
192
+ }
193
+ }
194
+
195
+ module.exports = { WebsocketConnectionRepositoryPostgres };
@@ -0,0 +1,160 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const AWS = require('aws-sdk');
3
+ const {
4
+ WebsocketConnectionRepositoryInterface,
5
+ } = require('./websocket-connection-repository-interface');
6
+
7
+ /**
8
+ * Prisma-based WebSocket Connection Repository
9
+ * Handles persistence of active WebSocket connections
10
+ *
11
+ * Works identically for both MongoDB and PostgreSQL:
12
+ * - MongoDB: String IDs with @db.ObjectId
13
+ * - PostgreSQL: Integer IDs with auto-increment
14
+ * - Both use same query patterns (no many-to-many differences)
15
+ *
16
+ * Migration from Mongoose:
17
+ * - Constructor injection of Prisma client
18
+ * - Static method getActiveConnections() → Instance method
19
+ * - AWS API Gateway Management API integration preserved
20
+ */
21
+ class WebsocketConnectionRepository extends WebsocketConnectionRepositoryInterface {
22
+ constructor(prismaClient = prisma) {
23
+ super();
24
+ this.prisma = prismaClient; // Allow injection for testing
25
+ }
26
+
27
+ /**
28
+ * Create a new WebSocket connection record
29
+ * Replaces: WebsocketConnection.create({ connectionId })
30
+ *
31
+ * @param {string} connectionId - The WebSocket connection ID
32
+ * @returns {Promise<Object>} The created connection record
33
+ */
34
+ async createConnection(connectionId) {
35
+ return await this.prisma.websocketConnection.create({
36
+ data: { connectionId },
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Delete a WebSocket connection record
42
+ * Replaces: WebsocketConnection.deleteOne({ connectionId })
43
+ *
44
+ * @param {string} connectionId - The WebSocket connection ID to delete
45
+ * @returns {Promise<Object>} The deletion result
46
+ */
47
+ async deleteConnection(connectionId) {
48
+ try {
49
+ await this.prisma.websocketConnection.delete({
50
+ where: { connectionId },
51
+ });
52
+ return { acknowledged: true, deletedCount: 1 };
53
+ } catch (error) {
54
+ if (error.code === 'P2025') {
55
+ // Record not found
56
+ return { acknowledged: true, deletedCount: 0 };
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get all active WebSocket connections with send capability
64
+ * Replaces: WebsocketConnection.getActiveConnections()
65
+ *
66
+ * @returns {Promise<Array>} Array of active connection objects with send capability
67
+ */
68
+ async getActiveConnections() {
69
+ try {
70
+ // Return empty array if websockets are not configured
71
+ if (!process.env.WEBSOCKET_API_ENDPOINT) {
72
+ return [];
73
+ }
74
+
75
+ const connections = await this.prisma.websocketConnection.findMany({
76
+ select: { connectionId: true },
77
+ });
78
+
79
+ return connections.map((conn) => ({
80
+ connectionId: conn.connectionId,
81
+ send: async (data) => {
82
+ const apigwManagementApi = new AWS.ApiGatewayManagementApi({
83
+ apiVersion: '2018-11-29',
84
+ endpoint: process.env.WEBSOCKET_API_ENDPOINT,
85
+ });
86
+
87
+ try {
88
+ await apigwManagementApi
89
+ .postToConnection({
90
+ ConnectionId: conn.connectionId,
91
+ Data: JSON.stringify(data),
92
+ })
93
+ .promise();
94
+ } catch (error) {
95
+ if (error.statusCode === 410) {
96
+ console.log(
97
+ `Stale connection ${conn.connectionId}`
98
+ );
99
+ // Delete stale connection
100
+ await this.prisma.websocketConnection.deleteMany({
101
+ where: { connectionId: conn.connectionId },
102
+ });
103
+ } else {
104
+ throw error;
105
+ }
106
+ }
107
+ },
108
+ }));
109
+ } catch (error) {
110
+ console.error('Error getting active connections:', error);
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Find a connection by ID
117
+ * Replaces: WebsocketConnection.findOne({ connectionId })
118
+ *
119
+ * @param {string} connectionId - The WebSocket connection ID
120
+ * @returns {Promise<Object|null>} The connection record or null
121
+ */
122
+ async findConnection(connectionId) {
123
+ return await this.prisma.websocketConnection.findFirst({
124
+ where: { connectionId },
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Find connection by internal ID
130
+ * @param {string} id - The internal connection ID
131
+ * @returns {Promise<Object|null>} The connection record or null
132
+ */
133
+ async findConnectionById(id) {
134
+ return await this.prisma.websocketConnection.findUnique({
135
+ where: { id },
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Get all connections
141
+ * @returns {Promise<Array>} Array of all connection records
142
+ */
143
+ async getAllConnections() {
144
+ return await this.prisma.websocketConnection.findMany();
145
+ }
146
+
147
+ /**
148
+ * Delete all connections
149
+ * @returns {Promise<Object>} The deletion result
150
+ */
151
+ async deleteAllConnections() {
152
+ const result = await this.prisma.websocketConnection.deleteMany();
153
+ return {
154
+ acknowledged: true,
155
+ deletedCount: result.count,
156
+ };
157
+ }
158
+ }
159
+
160
+ module.exports = { WebsocketConnectionRepository };
@@ -1,9 +0,0 @@
1
- const { mongoose } = require('../mongoose');
2
-
3
- const schema = new mongoose.Schema({
4
- state: { type: mongoose.Schema.Types.Mixed }
5
- });
6
-
7
- const State = mongoose.models.State || mongoose.model('State', schema);
8
-
9
- module.exports = { State };
@@ -1,70 +0,0 @@
1
- const { mongoose } = require('../mongoose');
2
- const bcrypt = require('bcryptjs');
3
-
4
- const collectionName = 'Token';
5
- const decimals = 10;
6
-
7
- const schema = new mongoose.Schema({
8
- token: { type: String, required: true },
9
- created: { type: Date, default: Date.now },
10
- expires: { type: Date },
11
- user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
12
- });
13
-
14
- schema.static({
15
- createTokenWithExpire: async function (userId, rawToken, minutes) {
16
- // Create user token
17
- let tokenHash = await bcrypt.hashSync(rawToken, parseInt(decimals));
18
-
19
- let session = {
20
- token: tokenHash,
21
- expires: new Date(Date.now() + minutes * 60000).toISOString(),
22
- user: userId,
23
- };
24
-
25
- return this.create(session);
26
- },
27
- // Takes in a token object and that has been created in the database and the raw token value.
28
- // Returns a json of just the token and id to return to the browser
29
- createJSONToken: function (token, rawToken) {
30
- let returnArr = {
31
- id: token.id,
32
- token: rawToken,
33
- };
34
- return JSON.stringify(returnArr);
35
- },
36
- // Takes in a token object and that has been created in the database and the raw token value.
37
- // Returns a base64 buffer of just the token and id to return to the browser
38
- createBase64BufferToken: function (token, rawToken) {
39
- let jsonVal = Token.createJSONToken(token, rawToken);
40
- return Buffer.from(jsonVal).toString('base64');
41
- },
42
- getJSONTokenFromBase64BufferToken: function (buffer) {
43
- let tokenStr = Buffer.from(buffer.trim(), 'base64').toString('ascii');
44
- return JSON.parse(tokenStr);
45
- },
46
-
47
- // Takes in a JSON Token with id and token in it and verifies the token
48
- // is valid from the database. If it is not va
49
- validateAndGetTokenFromJSONToken: async function (tokenObj) {
50
- let sessionToken = await this.findById(tokenObj.id);
51
- if (sessionToken) {
52
- if (
53
- !(await bcrypt.compareSync(tokenObj.token, sessionToken.token))
54
- ) {
55
- throw new Error('Invalid Token: Token does not match');
56
- }
57
- if (new Date(sessionToken.expires) < new Date()) {
58
- throw new Error('Invalid Token: Token is expired');
59
- }
60
-
61
- return sessionToken;
62
- } else {
63
- throw new Error('Invalid Token: Token does not exist');
64
- }
65
- }
66
- })
67
-
68
- const Token = mongoose.models.Token || mongoose.model(collectionName, schema);
69
-
70
- module.exports = { Token };