@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.
- package/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +321 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /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 };
|