@friggframework/core 2.0.0-next.40 → 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 @@
1
+ // todo: we need to get rid of this entire models folder
@@ -0,0 +1,162 @@
1
+ const {
2
+ createEncryptionExtension,
3
+ } = require('./encryption/prisma-encryption-extension');
4
+ const { registerCustomSchema } = require('./encryption/encryption-schema-registry');
5
+ const { logger } = require('./encryption/logger');
6
+ const { Cryptor } = require('../encrypt/Cryptor');
7
+ const config = require('./config');
8
+
9
+ function getEncryptionConfig() {
10
+ const STAGE = process.env.STAGE || process.env.NODE_ENV || 'development';
11
+ const shouldBypassEncryption = ['dev', 'test', 'local'].includes(STAGE);
12
+
13
+ if (shouldBypassEncryption) {
14
+ return { enabled: false };
15
+ }
16
+
17
+ const hasKMS =
18
+ process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '';
19
+ const hasAES =
20
+ process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '';
21
+
22
+ if (!hasKMS && !hasAES) {
23
+ logger.warn(
24
+ 'No encryption keys configured (KMS_KEY_ARN or AES_KEY_ID). ' +
25
+ 'Field-level encryption disabled. Set STAGE=production and configure keys to enable.'
26
+ );
27
+ return { enabled: false };
28
+ }
29
+
30
+ return {
31
+ enabled: true,
32
+ method: hasKMS ? 'kms' : 'aes',
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Loads and registers custom encryption schema from appDefinition
38
+ * Gracefully handles cases where appDefinition is not available
39
+ */
40
+ function loadCustomEncryptionSchema() {
41
+ try {
42
+ // Lazy require to avoid circular dependency issues
43
+ const path = require('node:path');
44
+ const { findNearestBackendPackageJson } = require('../utils');
45
+
46
+ const backendPackagePath = findNearestBackendPackageJson();
47
+ if (!backendPackagePath) {
48
+ return; // No backend found, skip custom schema
49
+ }
50
+
51
+ const backendDir = path.dirname(backendPackagePath);
52
+ const backendIndexPath = path.join(backendDir, 'index.js');
53
+
54
+ const backendModule = require(backendIndexPath);
55
+ const appDefinition = backendModule?.Definition;
56
+
57
+ if (!appDefinition) {
58
+ return; // No app definition found
59
+ }
60
+
61
+ const customSchema = appDefinition.encryption?.schema;
62
+
63
+ if (customSchema && Object.keys(customSchema).length > 0) {
64
+ registerCustomSchema(customSchema);
65
+ }
66
+ } catch (error) {
67
+ // Silently ignore errors - custom schema is optional
68
+ // This handles cases like:
69
+ // - Backend package.json not found (tests, standalone usage)
70
+ // - No appDefinition defined
71
+ // - No custom encryption schema specified
72
+ logger.debug('Could not load custom encryption schema:', error.message);
73
+ }
74
+ }
75
+
76
+ const prismaClientSingleton = () => {
77
+ let PrismaClient;
78
+
79
+ if (config.DB_TYPE === 'mongodb') {
80
+ PrismaClient = require('@prisma-mongodb/client').PrismaClient;
81
+ } else if (config.DB_TYPE === 'postgresql') {
82
+ PrismaClient = require('@prisma-postgresql/client').PrismaClient;
83
+ } else {
84
+ throw new Error(
85
+ `Unsupported database type: ${config.DB_TYPE}. Supported values: 'mongodb', 'postgresql'`
86
+ );
87
+ }
88
+
89
+ let client = new PrismaClient({
90
+ log: process.env.PRISMA_LOG_LEVEL
91
+ ? process.env.PRISMA_LOG_LEVEL.split(',')
92
+ : ['error', 'warn'],
93
+ errorFormat: 'pretty',
94
+ });
95
+
96
+ const encryptionConfig = getEncryptionConfig();
97
+
98
+ if (encryptionConfig.enabled) {
99
+ try {
100
+ // Load custom encryption schema from appDefinition before creating extension
101
+ loadCustomEncryptionSchema();
102
+
103
+ const cryptor = new Cryptor({
104
+ shouldUseAws: encryptionConfig.method === 'kms',
105
+ });
106
+
107
+ client = client.$extends(
108
+ createEncryptionExtension({
109
+ cryptor,
110
+ enabled: true,
111
+ })
112
+ );
113
+
114
+ logger.info(
115
+ `Field-level encryption enabled using ${encryptionConfig.method.toUpperCase()}`
116
+ );
117
+ } catch (error) {
118
+ logger.error(
119
+ 'Failed to initialize encryption extension:',
120
+ error
121
+ );
122
+ logger.warn('Continuing without encryption...');
123
+ }
124
+ } else {
125
+ logger.info('Field-level encryption disabled');
126
+ }
127
+
128
+ return client;
129
+ };
130
+
131
+ const globalForPrisma = global;
132
+
133
+ // Lazy initialization - only create singleton when first accessed
134
+ function getPrismaClient() {
135
+ if (!globalForPrisma._prismaInstance) {
136
+ globalForPrisma._prismaInstance = prismaClientSingleton();
137
+ }
138
+ return globalForPrisma._prismaInstance;
139
+ }
140
+
141
+ // Export a getter for lazy initialization
142
+ const prisma = new Proxy({}, {
143
+ get(target, prop) {
144
+ return getPrismaClient()[prop];
145
+ }
146
+ });
147
+
148
+ async function disconnectPrisma() {
149
+ await getPrismaClient().$disconnect();
150
+ }
151
+
152
+ async function connectPrisma() {
153
+ await getPrismaClient().$connect();
154
+ return getPrismaClient();
155
+ }
156
+
157
+ module.exports = {
158
+ prisma,
159
+ connectPrisma,
160
+ disconnectPrisma,
161
+ getEncryptionConfig,
162
+ };
@@ -0,0 +1,38 @@
1
+ const { HealthCheckRepositoryMongoDB } = require('./health-check-repository-mongodb');
2
+ const { HealthCheckRepositoryPostgreSQL } = require('./health-check-repository-postgres');
3
+ const config = require('../config');
4
+
5
+ /**
6
+ * Health Check Repository Factory
7
+ * Creates the appropriate repository adapter based on database type
8
+ *
9
+ * Usage:
10
+ * ```javascript
11
+ * const repository = createHealthCheckRepository();
12
+ * ```
13
+ *
14
+ * @returns {HealthCheckRepositoryInterface} Configured repository adapter
15
+ */
16
+ function createHealthCheckRepository() {
17
+ const dbType = config.DB_TYPE;
18
+
19
+ switch (dbType) {
20
+ case 'mongodb':
21
+ return new HealthCheckRepositoryMongoDB();
22
+
23
+ case 'postgresql':
24
+ return new HealthCheckRepositoryPostgreSQL();
25
+
26
+ default:
27
+ throw new Error(
28
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
29
+ );
30
+ }
31
+ }
32
+
33
+ module.exports = {
34
+ createHealthCheckRepository,
35
+ // Export adapters for direct testing
36
+ HealthCheckRepositoryMongoDB,
37
+ HealthCheckRepositoryPostgreSQL,
38
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Health Check Repository Interface
3
+ * Abstract base class defining the contract for health check 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, HealthCheckRepository has identical structure across MongoDB and PostgreSQL,
11
+ * so HealthCheckRepository serves both. This interface exists for consistency and
12
+ * future-proofing if database-specific implementations become needed.
13
+ *
14
+ * @abstract
15
+ */
16
+ class HealthCheckRepositoryInterface {
17
+ /**
18
+ * Ping database to verify connectivity
19
+ *
20
+ * @param {number} maxTimeMS - Maximum time in milliseconds
21
+ * @returns {Promise<number>} Response time in milliseconds
22
+ * @abstract
23
+ */
24
+ async pingDatabase(maxTimeMS) {
25
+ throw new Error('Method pingDatabase must be implemented by subclass');
26
+ }
27
+
28
+ /**
29
+ * Save a test document
30
+ *
31
+ * @param {Object} TestModel - Prisma model
32
+ * @param {Object} data - Data to save
33
+ * @returns {Promise<Object>} Saved document
34
+ * @abstract
35
+ */
36
+ async saveTestDocument(TestModel, data) {
37
+ throw new Error('Method saveTestDocument must be implemented by subclass');
38
+ }
39
+
40
+ /**
41
+ * Find test document by ID
42
+ *
43
+ * @param {Object} TestModel - Prisma model
44
+ * @param {string|number} id - Document ID
45
+ * @returns {Promise<Object|null>} Document or null
46
+ * @abstract
47
+ */
48
+ async findTestDocumentById(TestModel, id) {
49
+ throw new Error('Method findTestDocumentById must be implemented by subclass');
50
+ }
51
+
52
+ /**
53
+ * Get raw document from collection
54
+ *
55
+ * @param {string} collectionName - Collection name
56
+ * @param {Object} filter - Filter criteria
57
+ * @returns {Promise<Object|null>} Raw document or null
58
+ * @abstract
59
+ */
60
+ async getRawDocumentFromCollection(collectionName, filter) {
61
+ throw new Error('Method getRawDocumentFromCollection must be implemented by subclass');
62
+ }
63
+
64
+ /**
65
+ * Delete test document
66
+ *
67
+ * @param {Object} TestModel - Prisma model
68
+ * @param {string|number} id - Document ID
69
+ * @returns {Promise<Object>} Deletion result
70
+ * @abstract
71
+ */
72
+ async deleteTestDocument(TestModel, id) {
73
+ throw new Error('Method deleteTestDocument must be implemented by subclass');
74
+ }
75
+
76
+ /**
77
+ * Get database connection state
78
+ *
79
+ * @returns {Object} Connection state info
80
+ */
81
+ getDatabaseConnectionState() {
82
+ throw new Error('Method getDatabaseConnectionState must be implemented by subclass');
83
+ }
84
+ }
85
+
86
+ module.exports = { HealthCheckRepositoryInterface };
@@ -0,0 +1,72 @@
1
+ const { prisma } = require('../prisma');
2
+ const { mongoose } = require('../mongoose');
3
+ const {
4
+ HealthCheckRepositoryInterface,
5
+ } = require('./health-check-repository-interface');
6
+
7
+ /**
8
+ * MongoDB-specific Health Check Repository
9
+ *
10
+ * Provides MongoDB-specific database operations for health testing.
11
+ * Uses Mongoose for MongoDB-specific operations (raw access, ping).
12
+ */
13
+ class HealthCheckRepositoryMongoDB extends HealthCheckRepositoryInterface {
14
+ constructor() {
15
+ super();
16
+ }
17
+
18
+ getDatabaseConnectionState() {
19
+ const stateMap = {
20
+ 0: 'disconnected',
21
+ 1: 'connected',
22
+ 2: 'connecting',
23
+ 3: 'disconnecting',
24
+ };
25
+ const readyState = mongoose.connection.readyState;
26
+
27
+ return {
28
+ readyState,
29
+ stateName: stateMap[readyState],
30
+ isConnected: readyState === 1,
31
+ };
32
+ }
33
+
34
+ async pingDatabase(maxTimeMS = 2000) {
35
+ const pingStart = Date.now();
36
+ await mongoose.connection.db.admin().ping({ maxTimeMS });
37
+ return Date.now() - pingStart;
38
+ }
39
+
40
+ async createCredential(credentialData) {
41
+ return await prisma.credential.create({
42
+ data: credentialData,
43
+ });
44
+ }
45
+
46
+ async findCredentialById(id) {
47
+ return await prisma.credential.findUnique({
48
+ where: { id },
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Get raw credential from MongoDB bypassing Prisma encryption extension
54
+ * Uses Mongoose to access raw MongoDB collection
55
+ * @param {string} id - Credential ID
56
+ * @returns {Promise<Object|null>} Raw credential from database
57
+ */
58
+ async getRawCredentialById(id) {
59
+ const { ObjectId } = require('mongodb');
60
+ return await mongoose.connection.db
61
+ .collection('Credential')
62
+ .findOne({ _id: new ObjectId(id) });
63
+ }
64
+
65
+ async deleteCredential(id) {
66
+ await prisma.credential.delete({
67
+ where: { id },
68
+ });
69
+ }
70
+ }
71
+
72
+ module.exports = { HealthCheckRepositoryMongoDB };
@@ -0,0 +1,75 @@
1
+ const { prisma } = require('../prisma');
2
+ const {
3
+ HealthCheckRepositoryInterface,
4
+ } = require('./health-check-repository-interface');
5
+
6
+ /**
7
+ * PostgreSQL-specific Health Check Repository
8
+ *
9
+ * Provides PostgreSQL-specific database operations for health testing.
10
+ * Uses Prisma raw queries for PostgreSQL-specific operations.
11
+ */
12
+ class HealthCheckRepositoryPostgreSQL extends HealthCheckRepositoryInterface {
13
+ constructor() {
14
+ super();
15
+ }
16
+
17
+ getDatabaseConnectionState() {
18
+ // PostgreSQL connection state via Prisma
19
+ // Note: Prisma doesn't expose connection state like Mongoose
20
+ // We check if prisma is connected by attempting a query
21
+ return {
22
+ readyState: 1, // Assume connected if Prisma instance exists
23
+ stateName: 'connected',
24
+ isConnected: true,
25
+ };
26
+ }
27
+
28
+ async pingDatabase(maxTimeMS = 2000) {
29
+ const pingStart = Date.now();
30
+
31
+ // PostgreSQL ping using SELECT 1
32
+ await prisma.$queryRaw`SELECT 1`;
33
+
34
+ return Date.now() - pingStart;
35
+ }
36
+
37
+ async createCredential(credentialData) {
38
+ return await prisma.credential.create({
39
+ data: credentialData,
40
+ });
41
+ }
42
+
43
+ async findCredentialById(id) {
44
+ return await prisma.credential.findUnique({
45
+ where: { id },
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get raw credential from PostgreSQL bypassing Prisma encryption extension
51
+ * Uses $queryRaw to access raw PostgreSQL table
52
+ * @param {string} id - Credential ID
53
+ * @returns {Promise<Object|null>} Raw credential from database
54
+ */
55
+ async getRawCredentialById(id) {
56
+ const results = await prisma.$queryRaw`
57
+ SELECT * FROM "Credential" WHERE id = ${id}
58
+ `;
59
+
60
+ if (!results || results.length === 0) {
61
+ return null;
62
+ }
63
+
64
+ // Return first result
65
+ return results[0];
66
+ }
67
+
68
+ async deleteCredential(id) {
69
+ await prisma.credential.delete({
70
+ where: { id },
71
+ });
72
+ }
73
+ }
74
+
75
+ module.exports = { HealthCheckRepositoryPostgreSQL };
@@ -0,0 +1,108 @@
1
+ const { prisma } = require('../prisma');
2
+ const { mongoose } = require('../mongoose');
3
+ const {
4
+ HealthCheckRepositoryInterface,
5
+ } = require('./health-check-repository-interface');
6
+
7
+ /**
8
+ * Repository for Health Check database operations.
9
+ * Provides atomic database operations for health testing.
10
+ *
11
+ * Follows DDD/Hexagonal Architecture:
12
+ * - Infrastructure Layer (this repository)
13
+ * - Pure database operations only, no business logic
14
+ * - Used by Application Layer (Use Cases)
15
+ *
16
+ * Works identically for both MongoDB and PostgreSQL:
17
+ * - Uses Prisma for database operations
18
+ * - Encryption happens transparently via Prisma extension
19
+ * - Both MongoDB and PostgreSQL use same Prisma API
20
+ *
21
+ * Migration from Mongoose to Prisma:
22
+ * - Replaced Mongoose models with Prisma client
23
+ * - Uses Credential model for encryption testing
24
+ * - Maintains same method signatures for compatibility
25
+ */
26
+ class HealthCheckRepository extends HealthCheckRepositoryInterface {
27
+ constructor() {
28
+ super();
29
+ }
30
+
31
+ /**
32
+ * Get database connection state
33
+ * @returns {Object} Object with readyState, stateName, and isConnected
34
+ */
35
+ getDatabaseConnectionState() {
36
+ const stateMap = {
37
+ 0: 'disconnected',
38
+ 1: 'connected',
39
+ 2: 'connecting',
40
+ 3: 'disconnecting',
41
+ };
42
+ const readyState = mongoose.connection.readyState;
43
+
44
+ return {
45
+ readyState,
46
+ stateName: stateMap[readyState],
47
+ isConnected: readyState === 1,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Ping the database to verify connectivity
53
+ * @param {number} maxTimeMS - Maximum time to wait for ping response
54
+ * @returns {Promise<number>} Response time in milliseconds
55
+ * @throws {Error} If database is not connected or ping fails
56
+ */
57
+ async pingDatabase(maxTimeMS = 2000) {
58
+ const pingStart = Date.now();
59
+ await mongoose.connection.db.admin().ping({ maxTimeMS });
60
+ return Date.now() - pingStart;
61
+ }
62
+
63
+ /**
64
+ * Create a test credential for encryption testing
65
+ * @param {Object} credentialData - Credential data to create
66
+ * @returns {Promise<Object>} Created credential
67
+ */
68
+ async createCredential(credentialData) {
69
+ return await prisma.credential.create({
70
+ data: credentialData,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Find a credential by ID
76
+ * @param {string} id - Credential ID
77
+ * @returns {Promise<Object|null>} Found credential or null
78
+ */
79
+ async findCredentialById(id) {
80
+ return await prisma.credential.findUnique({
81
+ where: { id },
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Get raw credential from database bypassing Prisma encryption extension
87
+ * @param {string} id - Credential ID
88
+ * @returns {Promise<Object|null>} Raw credential from database
89
+ */
90
+ async getRawCredentialById(id) {
91
+ return await mongoose.connection.db
92
+ .collection('credentials')
93
+ .findOne({ _id: id });
94
+ }
95
+
96
+ /**
97
+ * Delete a credential by ID
98
+ * @param {string} id - Credential ID
99
+ * @returns {Promise<void>}
100
+ */
101
+ async deleteCredential(id) {
102
+ await prisma.credential.delete({
103
+ where: { id },
104
+ });
105
+ }
106
+ }
107
+
108
+ module.exports = { HealthCheckRepository };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Use Case for checking database health.
3
+ * Contains business logic for determining database connectivity and health status.
4
+ */
5
+ class CheckDatabaseHealthUseCase {
6
+ /**
7
+ * @param {Object} params
8
+ * @param {import('../health-check-repository-interface').HealthCheckRepositoryInterface} params.healthCheckRepository
9
+ */
10
+ constructor({ healthCheckRepository }) {
11
+ this.repository = healthCheckRepository;
12
+ }
13
+
14
+ /**
15
+ * Execute database health check
16
+ * @returns {Promise<Object>} Health check result with status, state, and response time
17
+ */
18
+ async execute() {
19
+ const { stateName, isConnected } = this.repository.getDatabaseConnectionState();
20
+
21
+ const result = {
22
+ status: isConnected ? 'healthy' : 'unhealthy',
23
+ state: stateName,
24
+ };
25
+
26
+ if (isConnected) {
27
+ result.responseTime = await this.repository.pingDatabase(2000);
28
+ }
29
+
30
+ return result;
31
+ }
32
+ }
33
+
34
+ module.exports = { CheckDatabaseHealthUseCase };
@@ -0,0 +1,82 @@
1
+ class CheckEncryptionHealthUseCase {
2
+ constructor({ testEncryptionUseCase }) {
3
+ this.testEncryptionUseCase = testEncryptionUseCase;
4
+ }
5
+
6
+ async execute() {
7
+ const config = this._getEncryptionConfiguration();
8
+
9
+ if (config.isBypassed || config.mode === 'none') {
10
+ const testResult = config.isBypassed
11
+ ? 'Encryption bypassed for this stage'
12
+ : 'No encryption keys configured';
13
+
14
+ return {
15
+ status: 'disabled',
16
+ mode: config.mode,
17
+ bypassed: config.isBypassed,
18
+ stage: config.stage,
19
+ testResult,
20
+ encryptionWorks: false,
21
+ debug: {
22
+ hasKMS: config.hasKMS,
23
+ hasAES: config.hasAES,
24
+ },
25
+ };
26
+ }
27
+
28
+ try {
29
+ const testResults = await this.testEncryptionUseCase.execute();
30
+
31
+ return {
32
+ ...testResults,
33
+ mode: config.mode,
34
+ bypassed: config.isBypassed,
35
+ stage: config.stage,
36
+ debug: {
37
+ hasKMS: config.hasKMS,
38
+ hasAES: config.hasAES,
39
+ },
40
+ };
41
+ } catch (error) {
42
+ return {
43
+ status: 'unhealthy',
44
+ mode: config.mode,
45
+ bypassed: config.isBypassed,
46
+ stage: config.stage,
47
+ testResult: `Encryption test failed: ${error.message}`,
48
+ encryptionWorks: false,
49
+ debug: {
50
+ hasKMS: config.hasKMS,
51
+ hasAES: config.hasAES,
52
+ },
53
+ };
54
+ }
55
+ }
56
+
57
+ _getEncryptionConfiguration() {
58
+ const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } =
59
+ process.env;
60
+
61
+ const defaultBypassStages = ['dev', 'test', 'local'];
62
+ const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
63
+ const bypassStages = useEnv
64
+ ? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
65
+ : defaultBypassStages;
66
+
67
+ const isBypassed = bypassStages.includes(STAGE);
68
+ const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
69
+ const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '' && !hasAES;
70
+ const mode = hasAES ? 'aes' : hasKMS ? 'kms' : 'none';
71
+
72
+ return {
73
+ stage: STAGE || null,
74
+ isBypassed,
75
+ hasAES,
76
+ hasKMS,
77
+ mode,
78
+ };
79
+ }
80
+ }
81
+
82
+ module.exports = { CheckEncryptionHealthUseCase };