@friggframework/core 2.0.0--canary.395.0e9373c.0 → 2.0.0--canary.395.2718e67.0
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/database/health-check-repository.js +117 -0
- package/database/token-repository.js +105 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/test-encryption-use-case.js +193 -0
- package/database/websocket-connection-repository.js +44 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/health.js +26 -193
- package/handlers/routers/websocket.js +5 -3
- package/integrations/integration-base.js +4 -3
- package/integrations/integration-mapping-repository.js +63 -0
- package/package.json +5 -5
- package/syncs/manager.js +468 -443
- package/syncs/sync-repository.js +87 -0
- package/user/user-repository.js +6 -6
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const { mongoose } = require('./mongoose');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repository for Health Check database operations.
|
|
5
|
+
* Provides atomic database operations for health testing.
|
|
6
|
+
*
|
|
7
|
+
* Follows DDD/Hexagonal Architecture:
|
|
8
|
+
* - Infrastructure Layer (this repository)
|
|
9
|
+
* - Pure database operations only, no business logic
|
|
10
|
+
* - Used by Application Layer (Use Cases)
|
|
11
|
+
*/
|
|
12
|
+
class HealthCheckRepository {
|
|
13
|
+
/**
|
|
14
|
+
* Get database connection state
|
|
15
|
+
* @returns {Object} Object with readyState, stateName, and isConnected
|
|
16
|
+
*/
|
|
17
|
+
getDatabaseConnectionState() {
|
|
18
|
+
const stateMap = {
|
|
19
|
+
0: 'disconnected',
|
|
20
|
+
1: 'connected',
|
|
21
|
+
2: 'connecting',
|
|
22
|
+
3: 'disconnecting',
|
|
23
|
+
};
|
|
24
|
+
const readyState = mongoose.connection.readyState;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
readyState,
|
|
28
|
+
stateName: stateMap[readyState],
|
|
29
|
+
isConnected: readyState === 1,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ping the database to verify connectivity
|
|
35
|
+
* @param {number} maxTimeMS - Maximum time to wait for ping response
|
|
36
|
+
* @returns {Promise<number>} Response time in milliseconds
|
|
37
|
+
* @throws {Error} If database is not connected or ping fails
|
|
38
|
+
*/
|
|
39
|
+
async pingDatabase(maxTimeMS = 2000) {
|
|
40
|
+
const pingStart = Date.now();
|
|
41
|
+
await mongoose.connection.db.admin().ping({ maxTimeMS });
|
|
42
|
+
return Date.now() - pingStart;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a test encryption model
|
|
47
|
+
* @returns {Model} Mongoose model with encryption plugin
|
|
48
|
+
*/
|
|
49
|
+
createEncryptionTestModel() {
|
|
50
|
+
const { Encrypt } = require('../encrypt');
|
|
51
|
+
|
|
52
|
+
const testSchema = new mongoose.Schema(
|
|
53
|
+
{
|
|
54
|
+
testSecret: { type: String, lhEncrypt: true },
|
|
55
|
+
normalField: { type: String },
|
|
56
|
+
nestedSecret: {
|
|
57
|
+
value: { type: String, lhEncrypt: true },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{ timestamps: false }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
testSchema.plugin(Encrypt);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
mongoose.models.TestEncryption ||
|
|
67
|
+
mongoose.model('TestEncryption', testSchema)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Save a test document to the database
|
|
73
|
+
* @param {Model} TestModel - Mongoose model
|
|
74
|
+
* @param {Object} data - Data to save
|
|
75
|
+
* @returns {Promise<Document>} Saved document
|
|
76
|
+
*/
|
|
77
|
+
async saveTestDocument(TestModel, data) {
|
|
78
|
+
const testDoc = new TestModel(data);
|
|
79
|
+
await testDoc.save();
|
|
80
|
+
return testDoc;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find a test document by ID
|
|
85
|
+
* @param {Model} TestModel - Mongoose model
|
|
86
|
+
* @param {string} id - Document ID
|
|
87
|
+
* @returns {Promise<Document>} Found document
|
|
88
|
+
*/
|
|
89
|
+
async findTestDocumentById(TestModel, id) {
|
|
90
|
+
return await TestModel.findById(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get raw document from collection (bypassing Mongoose)
|
|
95
|
+
* Used for verifying encryption at the database level
|
|
96
|
+
* @param {string} collectionName - Collection name
|
|
97
|
+
* @param {Object} filter - Query filter
|
|
98
|
+
* @returns {Promise<Object>} Raw document from database
|
|
99
|
+
*/
|
|
100
|
+
async getRawDocumentFromCollection(collectionName, filter) {
|
|
101
|
+
return await mongoose.connection.db
|
|
102
|
+
.collection(collectionName)
|
|
103
|
+
.findOne(filter);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Delete a test document
|
|
108
|
+
* @param {Model} TestModel - Mongoose model
|
|
109
|
+
* @param {string} id - Document ID
|
|
110
|
+
* @returns {Promise<Object>} Deletion result
|
|
111
|
+
*/
|
|
112
|
+
async deleteTestDocument(TestModel, id) {
|
|
113
|
+
return await TestModel.deleteOne({ _id: id });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { HealthCheckRepository };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const { Token } = require('./models/Token');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repository for Token operations.
|
|
5
|
+
* Handles persistence of authentication tokens.
|
|
6
|
+
*/
|
|
7
|
+
class TokenRepository {
|
|
8
|
+
/**
|
|
9
|
+
* Create a token with expiration
|
|
10
|
+
* @param {string} userId - The user ID
|
|
11
|
+
* @param {string} rawToken - The raw (unhashed) token string
|
|
12
|
+
* @param {number} minutes - Minutes until expiration
|
|
13
|
+
* @returns {Promise<Object>} The created token record
|
|
14
|
+
*/
|
|
15
|
+
async createTokenWithExpire(userId, rawToken, minutes) {
|
|
16
|
+
return await Token.createTokenWithExpire(userId, rawToken, minutes);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate and retrieve token from JSON token object
|
|
21
|
+
* @param {Object} tokenObj - Object with id and token properties
|
|
22
|
+
* @returns {Promise<Object>} The validated token record
|
|
23
|
+
* @throws {Error} If token is invalid, expired, or doesn't exist
|
|
24
|
+
*/
|
|
25
|
+
async validateAndGetToken(tokenObj) {
|
|
26
|
+
return await Token.validateAndGetTokenFromJSONToken(tokenObj);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find a token by ID
|
|
31
|
+
* @param {string} tokenId - The token ID
|
|
32
|
+
* @returns {Promise<Object|null>} The token record or null
|
|
33
|
+
*/
|
|
34
|
+
async findTokenById(tokenId) {
|
|
35
|
+
return await Token.findById(tokenId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find tokens by user ID
|
|
40
|
+
* @param {string} userId - The user ID
|
|
41
|
+
* @returns {Promise<Array>} Array of token records
|
|
42
|
+
*/
|
|
43
|
+
async findTokensByUserId(userId) {
|
|
44
|
+
return await Token.find({ user: userId });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delete a token by ID
|
|
49
|
+
* @param {string} tokenId - The token ID
|
|
50
|
+
* @returns {Promise<Object>} The deletion result
|
|
51
|
+
*/
|
|
52
|
+
async deleteToken(tokenId) {
|
|
53
|
+
return await Token.deleteOne({ _id: tokenId });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Delete expired tokens
|
|
58
|
+
* @returns {Promise<Object>} The deletion result with count
|
|
59
|
+
*/
|
|
60
|
+
async deleteExpiredTokens() {
|
|
61
|
+
return await Token.deleteMany({
|
|
62
|
+
expires: { $lt: new Date() },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Delete all tokens for a user
|
|
68
|
+
* @param {string} userId - The user ID
|
|
69
|
+
* @returns {Promise<Object>} The deletion result
|
|
70
|
+
*/
|
|
71
|
+
async deleteTokensByUserId(userId) {
|
|
72
|
+
return await Token.deleteMany({ user: userId });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create JSON token string from token object and raw token
|
|
77
|
+
* @param {Object} token - The token record
|
|
78
|
+
* @param {string} rawToken - The raw token string
|
|
79
|
+
* @returns {string} JSON string with id and token
|
|
80
|
+
*/
|
|
81
|
+
createJSONToken(token, rawToken) {
|
|
82
|
+
return Token.createJSONToken(token, rawToken);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create base64 encoded buffer token
|
|
87
|
+
* @param {Object} token - The token record
|
|
88
|
+
* @param {string} rawToken - The raw token string
|
|
89
|
+
* @returns {string} Base64 encoded token
|
|
90
|
+
*/
|
|
91
|
+
createBase64BufferToken(token, rawToken) {
|
|
92
|
+
return Token.createBase64BufferToken(token, rawToken);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse JSON token from base64 buffer
|
|
97
|
+
* @param {string} buffer - Base64 encoded token string
|
|
98
|
+
* @returns {Object} Parsed token object with id and token
|
|
99
|
+
*/
|
|
100
|
+
getJSONTokenFromBase64BufferToken(buffer) {
|
|
101
|
+
return Token.getJSONTokenFromBase64BufferToken(buffer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { TokenRepository };
|
|
@@ -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').HealthCheckRepository} 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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use Case for testing encryption functionality.
|
|
3
|
+
* Contains business logic for verifying that encryption and decryption work correctly.
|
|
4
|
+
*
|
|
5
|
+
* Follows DDD/Hexagonal Architecture:
|
|
6
|
+
* - Application Layer (this use case)
|
|
7
|
+
* - Depends on Infrastructure Layer (HealthCheckRepository)
|
|
8
|
+
*/
|
|
9
|
+
class TestEncryptionUseCase {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} params
|
|
12
|
+
* @param {import('../health-check-repository').HealthCheckRepository} params.healthCheckRepository
|
|
13
|
+
*/
|
|
14
|
+
constructor({ healthCheckRepository }) {
|
|
15
|
+
this.repository = healthCheckRepository;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute encryption test
|
|
20
|
+
* Orchestrates the full encryption test workflow
|
|
21
|
+
* @returns {Promise<Object>} Test results with status and details
|
|
22
|
+
*/
|
|
23
|
+
async execute() {
|
|
24
|
+
const TestModel = this.repository.createEncryptionTestModel();
|
|
25
|
+
|
|
26
|
+
const testData = {
|
|
27
|
+
testSecret: 'This is a secret value that should be encrypted',
|
|
28
|
+
normalField: 'This is a normal field that should not be encrypted',
|
|
29
|
+
nestedSecret: {
|
|
30
|
+
value: 'This is a nested secret that should be encrypted',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const testDoc = await this._withTimeout(
|
|
35
|
+
this.repository.saveTestDocument(TestModel, testData),
|
|
36
|
+
5000,
|
|
37
|
+
'Save operation timed out'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const retrievedDoc = await this._withTimeout(
|
|
42
|
+
this.repository.findTestDocumentById(TestModel, testDoc._id),
|
|
43
|
+
5000,
|
|
44
|
+
'Find operation timed out'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const decryptionWorks = this._verifyDecryption(
|
|
48
|
+
retrievedDoc,
|
|
49
|
+
testData
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const rawDoc = await this._withTimeout(
|
|
53
|
+
this.repository.getRawDocumentFromCollection(
|
|
54
|
+
TestModel.collection.name,
|
|
55
|
+
{ _id: testDoc._id }
|
|
56
|
+
),
|
|
57
|
+
5000,
|
|
58
|
+
'Database verification timed out'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const encryptionResults = this._verifyEncryptionInDatabase(
|
|
62
|
+
rawDoc,
|
|
63
|
+
testData
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return this._evaluateEncryptionResults(
|
|
67
|
+
decryptionWorks,
|
|
68
|
+
encryptionResults
|
|
69
|
+
);
|
|
70
|
+
} finally {
|
|
71
|
+
await this._withTimeout(
|
|
72
|
+
this.repository.deleteTestDocument(TestModel, testDoc._id),
|
|
73
|
+
5000,
|
|
74
|
+
'Delete operation timed out'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Verify that a document was decrypted correctly
|
|
81
|
+
* @param {Object} retrievedDoc - Document retrieved from database
|
|
82
|
+
* @param {Object} originalData - Original unencrypted data
|
|
83
|
+
* @returns {boolean} True if decryption worked correctly
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
_verifyDecryption(retrievedDoc, originalData) {
|
|
87
|
+
return (
|
|
88
|
+
retrievedDoc &&
|
|
89
|
+
retrievedDoc.testSecret === originalData.testSecret &&
|
|
90
|
+
retrievedDoc.normalField === originalData.normalField &&
|
|
91
|
+
retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Verify that data was encrypted in the database
|
|
97
|
+
* Business rule: Encrypted fields should contain ':' and differ from original
|
|
98
|
+
* @param {Object} rawDoc - Raw document from database
|
|
99
|
+
* @param {Object} originalData - Original unencrypted data
|
|
100
|
+
* @returns {Object} Encryption verification results
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
_verifyEncryptionInDatabase(rawDoc, originalData) {
|
|
104
|
+
const secretIsEncrypted =
|
|
105
|
+
rawDoc &&
|
|
106
|
+
typeof rawDoc.testSecret === 'string' &&
|
|
107
|
+
rawDoc.testSecret.includes(':') &&
|
|
108
|
+
rawDoc.testSecret !== originalData.testSecret;
|
|
109
|
+
|
|
110
|
+
const nestedIsEncrypted =
|
|
111
|
+
rawDoc?.nestedSecret?.value &&
|
|
112
|
+
typeof rawDoc.nestedSecret.value === 'string' &&
|
|
113
|
+
rawDoc.nestedSecret.value.includes(':') &&
|
|
114
|
+
rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
|
|
115
|
+
|
|
116
|
+
const normalNotEncrypted =
|
|
117
|
+
rawDoc && rawDoc.normalField === originalData.normalField;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
secretIsEncrypted,
|
|
121
|
+
nestedIsEncrypted,
|
|
122
|
+
normalNotEncrypted,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Evaluate encryption test results
|
|
128
|
+
* Business logic for determining if encryption is healthy
|
|
129
|
+
* @param {boolean} decryptionWorks - Whether decryption succeeded
|
|
130
|
+
* @param {Object} encryptionResults - Encryption verification results
|
|
131
|
+
* @returns {Object} Test status and result message
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
_evaluateEncryptionResults(decryptionWorks, encryptionResults) {
|
|
135
|
+
const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } =
|
|
136
|
+
encryptionResults;
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
decryptionWorks &&
|
|
140
|
+
secretIsEncrypted &&
|
|
141
|
+
nestedIsEncrypted &&
|
|
142
|
+
normalNotEncrypted
|
|
143
|
+
) {
|
|
144
|
+
return {
|
|
145
|
+
status: 'enabled',
|
|
146
|
+
testResult:
|
|
147
|
+
'Encryption and decryption verified successfully',
|
|
148
|
+
encryptionWorks: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
|
|
153
|
+
return {
|
|
154
|
+
status: 'unhealthy',
|
|
155
|
+
testResult: 'Fields are not being encrypted in database',
|
|
156
|
+
encryptionWorks: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (decryptionWorks && !normalNotEncrypted) {
|
|
161
|
+
return {
|
|
162
|
+
status: 'unhealthy',
|
|
163
|
+
testResult: 'Normal fields are being incorrectly encrypted',
|
|
164
|
+
encryptionWorks: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
status: 'unhealthy',
|
|
170
|
+
testResult: 'Decryption failed or data mismatch',
|
|
171
|
+
encryptionWorks: false,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Execute promise with timeout
|
|
177
|
+
* @param {Promise} promise - Promise to execute
|
|
178
|
+
* @param {number} ms - Timeout in milliseconds
|
|
179
|
+
* @param {string} errorMessage - Error message for timeout
|
|
180
|
+
* @returns {Promise} Promise that rejects on timeout
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
_withTimeout(promise, ms, errorMessage) {
|
|
184
|
+
return Promise.race([
|
|
185
|
+
promise,
|
|
186
|
+
new Promise((_, reject) =>
|
|
187
|
+
setTimeout(() => reject(new Error(errorMessage)), ms)
|
|
188
|
+
),
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { TestEncryptionUseCase };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { WebsocketConnection } = require('./models/WebsocketConnection');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repository for WebSocket connection operations.
|
|
5
|
+
* Handles persistence of active WebSocket connections.
|
|
6
|
+
*/
|
|
7
|
+
class WebsocketConnectionRepository {
|
|
8
|
+
/**
|
|
9
|
+
* Create a new WebSocket connection record
|
|
10
|
+
* @param {string} connectionId - The WebSocket connection ID
|
|
11
|
+
* @returns {Promise<Object>} The created connection record
|
|
12
|
+
*/
|
|
13
|
+
async createConnection(connectionId) {
|
|
14
|
+
return await WebsocketConnection.create({ connectionId });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Delete a WebSocket connection record
|
|
19
|
+
* @param {string} connectionId - The WebSocket connection ID to delete
|
|
20
|
+
* @returns {Promise<Object>} The deletion result
|
|
21
|
+
*/
|
|
22
|
+
async deleteConnection(connectionId) {
|
|
23
|
+
return await WebsocketConnection.deleteOne({ connectionId });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get all active WebSocket connections
|
|
28
|
+
* @returns {Promise<Array>} Array of active connection objects with send capability
|
|
29
|
+
*/
|
|
30
|
+
async getActiveConnections() {
|
|
31
|
+
return await WebsocketConnection.getActiveConnections();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find a connection by ID
|
|
36
|
+
* @param {string} connectionId - The WebSocket connection ID
|
|
37
|
+
* @returns {Promise<Object|null>} The connection record or null
|
|
38
|
+
*/
|
|
39
|
+
async findConnection(connectionId) {
|
|
40
|
+
return await WebsocketConnection.findOne({ connectionId });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { WebsocketConnectionRepository };
|
|
@@ -237,4 +237,106 @@ await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
|
|
|
237
237
|
|
|
238
238
|
## Environment Variables
|
|
239
239
|
|
|
240
|
-
- `HEALTH_API_KEY`: Required API key for accessing detailed health endpoints
|
|
240
|
+
- `HEALTH_API_KEY`: Required API key for accessing detailed health endpoints
|
|
241
|
+
|
|
242
|
+
## TODO: DDD/Hexagonal Architecture Refactoring
|
|
243
|
+
|
|
244
|
+
### Current Architecture Issues
|
|
245
|
+
|
|
246
|
+
The health router (health.js, 677 lines) currently violates DDD/Hexagonal Architecture principles:
|
|
247
|
+
|
|
248
|
+
**✅ What's Good:**
|
|
249
|
+
- Database access properly abstracted through `HealthCheckRepository`
|
|
250
|
+
- `CheckDatabaseHealthUseCase` and `TestEncryptionUseCase` correctly implement use case pattern
|
|
251
|
+
- All tests passing, no breaking changes
|
|
252
|
+
|
|
253
|
+
**❌ Architecture Violations:**
|
|
254
|
+
1. **Handler contains significant business logic** - Functions like `getEncryptionConfiguration()`, `checkEncryptionHealth()`, `checkKmsDecryptCapability()`, `detectVpcConfiguration()`, `checkExternalAPIs()`, and `checkIntegrations()` contain business logic that should be in use cases
|
|
255
|
+
2. **Direct infrastructure dependencies** - Handler directly uses `https`, `http`, Node.js `dns`, and factory modules instead of accessing through repositories
|
|
256
|
+
3. **Mixed concerns** - Single file handles HTTP routing, business logic, infrastructure detection, and response formatting
|
|
257
|
+
4. **Violates dependency rule** - Handler should only call use cases, never repositories or contain business logic
|
|
258
|
+
|
|
259
|
+
### Proposed Refactoring Plan
|
|
260
|
+
|
|
261
|
+
#### Priority 1: Extract Core Health Check Use Cases (Immediate)
|
|
262
|
+
|
|
263
|
+
**New Use Cases:**
|
|
264
|
+
1. `CheckEncryptionHealthUseCase` - Orchestrate encryption testing with configuration checks (from health.js:122-181)
|
|
265
|
+
2. `CheckKmsConnectivityUseCase` - Test KMS decrypt capability (from health.js:339-490)
|
|
266
|
+
3. `DetectNetworkConfigurationUseCase` - VPC and network detection (from health.js:244-336)
|
|
267
|
+
|
|
268
|
+
**New Repositories:**
|
|
269
|
+
1. `EncryptionConfigRepository` - Get encryption mode, bypass rules (from health.js:98-120)
|
|
270
|
+
2. `KmsRepository` - KMS connectivity testing, decrypt capability checks
|
|
271
|
+
3. `NetworkRepository` - DNS resolution, VPC detection, TCP connectivity tests
|
|
272
|
+
|
|
273
|
+
#### Priority 2: Extract External Service Checks
|
|
274
|
+
|
|
275
|
+
**New Use Cases:**
|
|
276
|
+
4. `CheckExternalServicesUseCase` - Check external API availability (from health.js:183-209)
|
|
277
|
+
|
|
278
|
+
**New Repositories:**
|
|
279
|
+
4. `ExternalServiceRepository` - HTTP-based service health checking with timeout handling
|
|
280
|
+
|
|
281
|
+
#### Priority 3: Extract Integration Checks
|
|
282
|
+
|
|
283
|
+
**New Use Cases:**
|
|
284
|
+
5. `CheckIntegrationAvailabilityUseCase` - Verify integrations and modules loaded (from health.js:211-231)
|
|
285
|
+
|
|
286
|
+
**Extend Existing:**
|
|
287
|
+
- Add `getAvailableIntegrations()` and `getAvailableModules()` methods to existing `IntegrationRepository`
|
|
288
|
+
|
|
289
|
+
### Architectural Principles to Follow
|
|
290
|
+
|
|
291
|
+
**The Handler Should Only:**
|
|
292
|
+
- Define routes
|
|
293
|
+
- Call use cases
|
|
294
|
+
- Map use case results to HTTP responses
|
|
295
|
+
- Handle HTTP-specific concerns (status codes, headers)
|
|
296
|
+
|
|
297
|
+
**The Rule:**
|
|
298
|
+
> "Handlers (adapters) should only call use cases, never repositories or business logic directly"
|
|
299
|
+
|
|
300
|
+
**Dependency Direction:**
|
|
301
|
+
```
|
|
302
|
+
Handler (Adapter Layer)
|
|
303
|
+
↓ calls
|
|
304
|
+
Use Cases (Application Layer)
|
|
305
|
+
↓ calls
|
|
306
|
+
Repositories (Infrastructure Layer)
|
|
307
|
+
↓ calls
|
|
308
|
+
External Systems (Database, APIs, AWS Services)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Expected Outcome
|
|
312
|
+
|
|
313
|
+
- Reduce health.js from **677 lines to ~100-150 lines**
|
|
314
|
+
- All business logic moved to use cases
|
|
315
|
+
- All infrastructure access moved to repositories
|
|
316
|
+
- Handler becomes thin HTTP adapter
|
|
317
|
+
- Improved testability (use cases testable without HTTP context)
|
|
318
|
+
- Better reusability (use cases usable in CLI tools, background jobs, etc.)
|
|
319
|
+
|
|
320
|
+
### Implementation Status
|
|
321
|
+
|
|
322
|
+
- [ ] P1: Extract `CheckEncryptionHealthUseCase`
|
|
323
|
+
- [ ] P1: Create `EncryptionConfigRepository`
|
|
324
|
+
- [ ] P1: Extract `CheckKmsConnectivityUseCase`
|
|
325
|
+
- [ ] P1: Create `KmsRepository`
|
|
326
|
+
- [ ] P1: Extract `DetectNetworkConfigurationUseCase`
|
|
327
|
+
- [ ] P1: Create `NetworkRepository`
|
|
328
|
+
- [ ] P2: Extract `CheckExternalServicesUseCase`
|
|
329
|
+
- [ ] P2: Create `ExternalServiceRepository`
|
|
330
|
+
- [ ] P3: Extract `CheckIntegrationAvailabilityUseCase`
|
|
331
|
+
- [ ] P3: Extend existing `IntegrationRepository`
|
|
332
|
+
|
|
333
|
+
### Future Considerations (Optional)
|
|
334
|
+
|
|
335
|
+
**Domain Models (Value Objects):**
|
|
336
|
+
- `HealthCheckResult` - Overall health check result with status, checks, timestamp
|
|
337
|
+
- `DatabaseHealth` - Database-specific health information
|
|
338
|
+
- `EncryptionHealth` - Encryption-specific health information
|
|
339
|
+
- `ServiceHealth` - Generic external service health
|
|
340
|
+
- `NetworkConfiguration` - VPC and network detection results
|
|
341
|
+
|
|
342
|
+
These would replace plain objects and provide type safety and business logic encapsulation.
|