@friggframework/core 2.0.0--canary.490.883b991.0 → 2.0.0--canary.497.a3f25f9.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/associations/model.js +54 -0
- package/core/create-handler.js +12 -63
- package/database/index.js +29 -7
- package/database/models/UserModel.js +7 -0
- package/database/models/WebsocketConnection.js +55 -0
- package/database/prisma.js +19 -51
- package/database/repositories/health-check-repository-mongodb.js +23 -23
- package/database/repositories/health-check-repository.js +108 -0
- package/database/utils/mongodb-collection-utils.js +18 -26
- package/database/utils/mongodb-schema-init.js +12 -7
- package/generated/prisma-mongodb/edge.js +6 -6
- package/generated/prisma-mongodb/index-browser.js +4 -4
- package/generated/prisma-mongodb/index.d.ts +2 -3
- package/generated/prisma-mongodb/index.js +6 -6
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.js +2 -2
- package/generated/prisma-mongodb/runtime/edge-esm.js +3 -3
- package/generated/prisma-mongodb/runtime/edge.js +3 -3
- package/generated/prisma-mongodb/runtime/library.d.ts +0 -5
- package/generated/prisma-mongodb/runtime/react-native.js +14 -14
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +18 -18
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +11 -11
- package/generated/prisma-mongodb/wasm.js +6 -6
- package/generated/prisma-postgresql/edge.js +6 -6
- package/generated/prisma-postgresql/index-browser.js +4 -4
- package/generated/prisma-postgresql/index.d.ts +2 -3
- package/generated/prisma-postgresql/index.js +6 -6
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -2
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.js +2 -2
- package/generated/prisma-postgresql/runtime/edge-esm.js +3 -3
- package/generated/prisma-postgresql/runtime/edge.js +3 -3
- package/generated/prisma-postgresql/runtime/library.d.ts +0 -5
- package/generated/prisma-postgresql/runtime/react-native.js +14 -14
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +18 -18
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +11 -11
- package/generated/prisma-postgresql/wasm.js +6 -6
- package/handlers/backend-utils.js +9 -8
- package/integrations/integration-router.js +1 -0
- package/integrations/use-cases/delete-integration-for-user.js +31 -3
- package/integrations/use-cases/get-integrations-for-user.js +5 -4
- package/integrations/utils/map-integration-dto.js +2 -1
- package/modules/entity.js +46 -0
- package/modules/index.js +2 -0
- package/package.json +5 -5
- package/syncs/model.js +62 -0
- package/database/PRISMA_BEST_PRACTICES.md +0 -654
- package/database/utils/documentdb-compatibility.js +0 -63
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
|
|
3
|
+
const schema = new mongoose.Schema({
|
|
4
|
+
integration: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: "Integration",
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
name: { type: String, required: true },
|
|
10
|
+
type: {
|
|
11
|
+
type: String,
|
|
12
|
+
enum: ["ONE_TO_MANY", "ONE_TO_ONE", "MANY_TO_ONE"],
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
primaryObject: { type: String, required: true },
|
|
16
|
+
objects: [
|
|
17
|
+
{
|
|
18
|
+
entity: {
|
|
19
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
20
|
+
ref: "Entity",
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
objectType: { type: String, required: true },
|
|
24
|
+
objId: { type: String, required: true },
|
|
25
|
+
metadata: { type: Object, required: false },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
schema.statics({
|
|
31
|
+
addAssociation: async function (id, object) {
|
|
32
|
+
return this.update({ _id: id }, { $push: { objects: object } });
|
|
33
|
+
},
|
|
34
|
+
findAssociation: async function (name, dataIdentifierHash) {
|
|
35
|
+
const syncList = await this.list({
|
|
36
|
+
name: name,
|
|
37
|
+
"dataIdentifiers.hash": dataIdentifierHash,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (syncList.length === 1) {
|
|
41
|
+
return syncList[0];
|
|
42
|
+
} else if (syncList.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`there are multiple sync objects with the name ${name}, for entities [${entities}]`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const Association =
|
|
53
|
+
mongoose.models.Association || mongoose.model("Association", schema);
|
|
54
|
+
module.exports = { Association };
|
package/core/create-handler.js
CHANGED
|
@@ -1,76 +1,21 @@
|
|
|
1
|
+
// This line should be at the top of the webpacked output, so be sure to require createHandler first in any handlers. "Soon" sourcemaps will be built into Node... after that, this package won't be needed.
|
|
2
|
+
// REMOVING FOR NOW UNTIL WE ADD WEBPACK BACK IN
|
|
3
|
+
// require('source-map-support').install();
|
|
4
|
+
|
|
1
5
|
const { initDebugLog, flushDebugLog } = require('../logs');
|
|
2
6
|
const { secretsToEnv } = require('./secrets-to-env');
|
|
3
7
|
|
|
4
|
-
let databaseInitializer = null;
|
|
5
|
-
let databaseInitialized = false;
|
|
6
|
-
let databaseInitPromise = null;
|
|
7
|
-
|
|
8
|
-
function setDatabaseInitializer(initializer) {
|
|
9
|
-
databaseInitializer = initializer;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function getDefaultDatabaseInitializer() {
|
|
13
|
-
if (!databaseInitializer) {
|
|
14
|
-
const { connectPrisma } = require('../database/prisma');
|
|
15
|
-
databaseInitializer = connectPrisma;
|
|
16
|
-
}
|
|
17
|
-
return databaseInitializer;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function ensureDatabaseInitialized() {
|
|
21
|
-
if (databaseInitialized) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!databaseInitPromise) {
|
|
26
|
-
databaseInitPromise = (async () => {
|
|
27
|
-
try {
|
|
28
|
-
const initializer = getDefaultDatabaseInitializer();
|
|
29
|
-
await initializer();
|
|
30
|
-
databaseInitialized = true;
|
|
31
|
-
console.log('✓ Database initialized successfully');
|
|
32
|
-
} catch (error) {
|
|
33
|
-
console.error('Failed to initialize database:', error.message);
|
|
34
|
-
databaseInitPromise = null;
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
})();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return databaseInitPromise;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Creates a Lambda handler with database initialization.
|
|
45
|
-
*
|
|
46
|
-
* Follows hexagonal architecture: database initialization is a port that can be injected.
|
|
47
|
-
* Default adapter uses Prisma's connectPrisma(), which handles both PostgreSQL and MongoDB.
|
|
48
|
-
*
|
|
49
|
-
* @param {Object} options
|
|
50
|
-
* @param {string} options.eventName - Event name for logging
|
|
51
|
-
* @param {boolean} options.isUserFacingResponse - Whether to hide error details (default: true)
|
|
52
|
-
* @param {Function} options.method - The handler method to execute
|
|
53
|
-
* @param {boolean} options.shouldUseDatabase - Whether to initialize database (default: true)
|
|
54
|
-
* @param {Function} options.databaseInitializer - Optional custom database initializer
|
|
55
|
-
* @returns {Function} Lambda handler
|
|
56
|
-
*/
|
|
57
8
|
const createHandler = (optionByName = {}) => {
|
|
58
9
|
const {
|
|
59
10
|
eventName = 'Event',
|
|
60
11
|
isUserFacingResponse = true,
|
|
61
12
|
method,
|
|
62
|
-
shouldUseDatabase = true,
|
|
63
|
-
databaseInitializer: customInitializer = null,
|
|
64
13
|
} = optionByName;
|
|
65
14
|
|
|
66
15
|
if (!method) {
|
|
67
16
|
throw new Error('Method is required for handler.');
|
|
68
17
|
}
|
|
69
18
|
|
|
70
|
-
if (customInitializer) {
|
|
71
|
-
setDatabaseInitializer(customInitializer);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
19
|
return async (event, context) => {
|
|
75
20
|
try {
|
|
76
21
|
initDebugLog(eventName, event);
|
|
@@ -81,18 +26,18 @@ const createHandler = (optionByName = {}) => {
|
|
|
81
26
|
console.info(`${requestMethod} ${requestPath}`);
|
|
82
27
|
}
|
|
83
28
|
|
|
29
|
+
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
84
30
|
await secretsToEnv();
|
|
85
31
|
|
|
86
|
-
|
|
87
|
-
await ensureDatabaseInitialized();
|
|
88
|
-
}
|
|
89
|
-
|
|
32
|
+
// Helps mongoose reuse the connection. Lowers response times.
|
|
90
33
|
context.callbackWaitsForEmptyEventLoop = false;
|
|
91
34
|
|
|
35
|
+
// Run the Lambda
|
|
92
36
|
return await method(event, context);
|
|
93
37
|
} catch (error) {
|
|
94
38
|
flushDebugLog(error);
|
|
95
39
|
|
|
40
|
+
// Don't leak implementation details to end users.
|
|
96
41
|
if (isUserFacingResponse) {
|
|
97
42
|
return {
|
|
98
43
|
statusCode: 500,
|
|
@@ -102,10 +47,14 @@ const createHandler = (optionByName = {}) => {
|
|
|
102
47
|
};
|
|
103
48
|
}
|
|
104
49
|
|
|
50
|
+
// Handle server-to-server responses.
|
|
51
|
+
|
|
52
|
+
// Halt errors are logged but suceed and won't be retried.
|
|
105
53
|
if (error.isHaltError === true) {
|
|
106
54
|
return;
|
|
107
55
|
}
|
|
108
56
|
|
|
57
|
+
// Here we can just rethrow and let AWS build the response.
|
|
109
58
|
throw error;
|
|
110
59
|
}
|
|
111
60
|
};
|
package/database/index.js
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
+
//todo: probably most of this file content can be removed
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Database Module Index
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* Exports Mongoose models and connection utilities
|
|
6
|
+
*
|
|
7
|
+
* Note: Frigg uses the Repository pattern for data access.
|
|
8
|
+
* Models are not meant to be used directly - use repositories instead:
|
|
9
|
+
* - SyncRepository (syncs/sync-repository.js)
|
|
10
|
+
* - IntegrationRepository (integrations/integration-repository.js)
|
|
11
|
+
* - CredentialRepository (credential/credential-repository.js)
|
|
12
|
+
* etc.
|
|
6
13
|
*/
|
|
7
14
|
|
|
8
|
-
const { prisma, connectPrisma, disconnectPrisma } = require('./prisma');
|
|
9
15
|
const { mongoose } = require('./mongoose');
|
|
16
|
+
const { IndividualUser } = require('./models/IndividualUser');
|
|
17
|
+
const { OrganizationUser } = require('./models/OrganizationUser');
|
|
18
|
+
const { UserModel } = require('./models/UserModel');
|
|
19
|
+
const { WebsocketConnection } = require('./models/WebsocketConnection');
|
|
20
|
+
|
|
21
|
+
// Prisma exports
|
|
22
|
+
const { prisma } = require('./prisma');
|
|
23
|
+
const { TokenRepository } = require('../token/repositories/token-repository');
|
|
24
|
+
const {
|
|
25
|
+
WebsocketConnectionRepository,
|
|
26
|
+
} = require('../websocket/repositories/websocket-connection-repository');
|
|
10
27
|
|
|
11
28
|
module.exports = {
|
|
29
|
+
mongoose,
|
|
30
|
+
IndividualUser,
|
|
31
|
+
OrganizationUser,
|
|
32
|
+
UserModel,
|
|
33
|
+
WebsocketConnection,
|
|
34
|
+
// Prisma
|
|
12
35
|
prisma,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mongoose, // Still used for legacy ping operations in some health checks
|
|
36
|
+
TokenRepository,
|
|
37
|
+
WebsocketConnectionRepository,
|
|
16
38
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { mongoose } = require('../mongoose');
|
|
2
|
+
const {
|
|
3
|
+
ApiGatewayManagementApiClient,
|
|
4
|
+
PostToConnectionCommand,
|
|
5
|
+
} = require('@aws-sdk/client-apigatewaymanagementapi');
|
|
6
|
+
|
|
7
|
+
const schema = new mongoose.Schema({
|
|
8
|
+
connectionId: { type: mongoose.Schema.Types.String },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Add a static method to get active connections
|
|
12
|
+
schema.statics.getActiveConnections = async function () {
|
|
13
|
+
try {
|
|
14
|
+
// Return empty array if websockets are not configured
|
|
15
|
+
if (!process.env.WEBSOCKET_API_ENDPOINT) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const connections = await this.find({}, 'connectionId');
|
|
20
|
+
return connections.map((conn) => ({
|
|
21
|
+
connectionId: conn.connectionId,
|
|
22
|
+
send: async (data) => {
|
|
23
|
+
const apigwManagementApi = new ApiGatewayManagementApiClient({
|
|
24
|
+
endpoint: process.env.WEBSOCKET_API_ENDPOINT,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const command = new PostToConnectionCommand({
|
|
29
|
+
ConnectionId: conn.connectionId,
|
|
30
|
+
Data: JSON.stringify(data),
|
|
31
|
+
});
|
|
32
|
+
await apigwManagementApi.send(command);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
|
|
35
|
+
console.log(`Stale connection ${conn.connectionId}`);
|
|
36
|
+
await this.deleteOne({
|
|
37
|
+
connectionId: conn.connectionId,
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error getting active connections:', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const WebsocketConnection =
|
|
52
|
+
mongoose.models.WebsocketConnection ||
|
|
53
|
+
mongoose.model('WebsocketConnection', schema);
|
|
54
|
+
|
|
55
|
+
module.exports = { WebsocketConnection };
|
package/database/prisma.js
CHANGED
|
@@ -14,40 +14,22 @@ const config = require('./config');
|
|
|
14
14
|
* Note: This should only be called when DB_TYPE is 'mongodb'
|
|
15
15
|
*/
|
|
16
16
|
function ensureMongoDbUrl() {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// If DATABASE_URL is already set, use it
|
|
19
18
|
if (process.env.DATABASE_URL && process.env.DATABASE_URL.trim()) {
|
|
20
|
-
|
|
21
|
-
} else if (process.env.MONGO_URI && process.env.MONGO_URI.trim()) {
|
|
22
|
-
connectionString = process.env.MONGO_URI;
|
|
23
|
-
logger.debug('Using MONGO_URI as DATABASE_URL for MongoDB connection');
|
|
24
|
-
} else {
|
|
25
|
-
throw new Error(
|
|
26
|
-
'DATABASE_URL or MONGO_URI environment variable must be set for MongoDB'
|
|
27
|
-
);
|
|
19
|
+
return;
|
|
28
20
|
}
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const existingReadPreference = params.get('readPreference');
|
|
37
|
-
if (existingReadPreference && existingReadPreference !== 'primary') {
|
|
38
|
-
params.set('readPreference', 'primary');
|
|
39
|
-
logger.debug(`Overrode readPreference from ${existingReadPreference} to primary for transaction support`);
|
|
40
|
-
} else if (!existingReadPreference) {
|
|
41
|
-
params.set('readPreference', 'primary');
|
|
42
|
-
logger.debug('Added readPreference=primary to MongoDB connection string for transaction support');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
connectionString = url.toString();
|
|
46
|
-
} catch (error) {
|
|
47
|
-
logger.warn('Failed to parse MongoDB connection string as URL, skipping readPreference configuration:', error.message);
|
|
21
|
+
|
|
22
|
+
// Fallback to MONGO_URI for backwards compatibility with DocumentDB deployments
|
|
23
|
+
if (process.env.MONGO_URI && process.env.MONGO_URI.trim()) {
|
|
24
|
+
process.env.DATABASE_URL = process.env.MONGO_URI;
|
|
25
|
+
logger.debug('Using MONGO_URI as DATABASE_URL for MongoDB connection');
|
|
26
|
+
return;
|
|
48
27
|
}
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
|
|
29
|
+
// Neither is set - error
|
|
30
|
+
throw new Error(
|
|
31
|
+
'DATABASE_URL or MONGO_URI environment variable must be set for MongoDB'
|
|
32
|
+
);
|
|
51
33
|
}
|
|
52
34
|
|
|
53
35
|
function getEncryptionConfig() {
|
|
@@ -122,27 +104,14 @@ const prismaClientSingleton = () => {
|
|
|
122
104
|
|
|
123
105
|
// Helper to try loading Prisma client from multiple locations
|
|
124
106
|
const loadPrismaClient = (dbType) => {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (fs.existsSync(layerPath)) {
|
|
130
|
-
try {
|
|
131
|
-
return require(layerPath).PrismaClient;
|
|
132
|
-
} catch (err) {
|
|
133
|
-
// Layer exists but failed to load - fall through to bundled paths
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Layer doesn't exist or failed - use bundled/local paths (when usePrismaLambdaLayer=false)
|
|
138
|
-
const localPaths = [
|
|
139
|
-
// Bundled with function (when usePrismaLambdaLayer=false)
|
|
140
|
-
`@friggframework/core/generated/prisma-${dbType}`,
|
|
141
|
-
// Local development (relative to core package)
|
|
107
|
+
const paths = [
|
|
108
|
+
// Lambda layer location (when using Prisma Lambda layer)
|
|
109
|
+
`/opt/nodejs/node_modules/generated/prisma-${dbType}`,
|
|
110
|
+
// Local development location (relative to core package)
|
|
142
111
|
`../generated/prisma-${dbType}`,
|
|
143
112
|
];
|
|
144
113
|
|
|
145
|
-
for (const path of
|
|
114
|
+
for (const path of paths) {
|
|
146
115
|
try {
|
|
147
116
|
return require(path).PrismaClient;
|
|
148
117
|
} catch (err) {
|
|
@@ -151,8 +120,7 @@ const prismaClientSingleton = () => {
|
|
|
151
120
|
}
|
|
152
121
|
|
|
153
122
|
throw new Error(
|
|
154
|
-
`Cannot find Prisma client for ${dbType}. `
|
|
155
|
-
`Tried layer path: ${layerPath}, local paths: ${localPaths.join(', ')}`
|
|
123
|
+
`Cannot find Prisma client for ${dbType}. Tried paths: ${paths.join(', ')}`
|
|
156
124
|
);
|
|
157
125
|
};
|
|
158
126
|
|
|
@@ -2,7 +2,6 @@ const { mongoose } = require('../mongoose');
|
|
|
2
2
|
const {
|
|
3
3
|
HealthCheckRepositoryInterface,
|
|
4
4
|
} = require('./health-check-repository-interface');
|
|
5
|
-
const { removeUndefinedValues } = require('../utils/documentdb-compatibility');
|
|
6
5
|
|
|
7
6
|
class HealthCheckRepositoryMongoDB extends HealthCheckRepositoryInterface {
|
|
8
7
|
/**
|
|
@@ -30,30 +29,38 @@ class HealthCheckRepositoryMongoDB extends HealthCheckRepositoryInterface {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
return {
|
|
32
|
+
readyState: isConnected ? 1 : 0,
|
|
33
33
|
readyState: isConnected ? 1 : 0,
|
|
34
34
|
stateName,
|
|
35
35
|
isConnected,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* @param {number} maxTimeMS
|
|
41
|
-
* @returns {Promise<number>} Response time in milliseconds
|
|
42
|
-
*/
|
|
43
39
|
async pingDatabase(maxTimeMS = 2000) {
|
|
44
40
|
const pingStart = Date.now();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
|
|
42
|
+
// Create a timeout promise that rejects after maxTimeMS
|
|
43
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
44
|
+
setTimeout(() => reject(new Error('Database ping timeout')), maxTimeMS)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Race between the database ping and the timeout
|
|
48
|
+
await Promise.race([
|
|
49
|
+
prisma.$queryRaw`SELECT 1`.catch(() => {
|
|
50
|
+
// For MongoDB, use runCommandRaw instead
|
|
51
|
+
return prisma.$runCommandRaw({ ping: 1 });
|
|
52
|
+
}),
|
|
53
|
+
timeoutPromise
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
return Date.now() - pingStart;
|
|
57
|
+
}
|
|
48
58
|
return Date.now() - pingStart;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
async createCredential(credentialData) {
|
|
52
|
-
// Remove undefined values to prevent Prisma from using $$REMOVE (DocumentDB unsupported)
|
|
53
|
-
const cleanedData = removeUndefinedValues(credentialData);
|
|
54
|
-
|
|
55
62
|
return await this.prisma.credential.create({
|
|
56
|
-
data:
|
|
63
|
+
data: credentialData,
|
|
57
64
|
});
|
|
58
65
|
}
|
|
59
66
|
|
|
@@ -68,17 +75,10 @@ class HealthCheckRepositoryMongoDB extends HealthCheckRepositoryInterface {
|
|
|
68
75
|
* @returns {Promise<Object|null>}
|
|
69
76
|
*/
|
|
70
77
|
async getRawCredentialById(id) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!result.cursor || !result.cursor.firstBatch || result.cursor.firstBatch.length === 0) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return result.cursor.firstBatch[0];
|
|
78
|
+
const { ObjectId } = require('mongodb');
|
|
79
|
+
return await mongoose.connection.db
|
|
80
|
+
.collection('Credential')
|
|
81
|
+
.findOne({ _id: new ObjectId(id) });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async deleteCredential(id) {
|
|
@@ -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 };
|
|
@@ -5,14 +5,11 @@
|
|
|
5
5
|
* handling the constraint that collections cannot be created inside
|
|
6
6
|
* multi-document transactions.
|
|
7
7
|
*
|
|
8
|
-
* Uses Prisma's $runCommandRaw to send native MongoDB commands directly,
|
|
9
|
-
* avoiding the need for mongoose connection.
|
|
10
|
-
*
|
|
11
8
|
* @see https://github.com/prisma/prisma/issues/8305
|
|
12
9
|
* @see https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-operations
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
|
-
const {
|
|
12
|
+
const { mongoose } = require('../mongoose');
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
15
|
* Ensures a MongoDB collection exists
|
|
@@ -21,9 +18,6 @@ const { prisma } = require('../prisma');
|
|
|
21
18
|
* transactions. This function checks if a collection exists and creates it if needed,
|
|
22
19
|
* preventing "Cannot create namespace in multi-document transaction" errors.
|
|
23
20
|
*
|
|
24
|
-
* Uses Prisma's $runCommandRaw to send native MongoDB commands, which works with
|
|
25
|
-
* both MongoDB and DocumentDB.
|
|
26
|
-
*
|
|
27
21
|
* @param {string} collectionName - Name of the collection to ensure exists
|
|
28
22
|
* @returns {Promise<void>}
|
|
29
23
|
*
|
|
@@ -36,24 +30,23 @@ const { prisma } = require('../prisma');
|
|
|
36
30
|
*/
|
|
37
31
|
async function ensureCollectionExists(collectionName) {
|
|
38
32
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
filter: { name: collectionName }
|
|
43
|
-
});
|
|
33
|
+
const collections = await mongoose.connection.db
|
|
34
|
+
.listCollections({ name: collectionName })
|
|
35
|
+
.toArray();
|
|
44
36
|
|
|
45
|
-
if (collections.
|
|
46
|
-
// Collection doesn't exist, create it
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
console.log(`✓ Created collection: ${collectionName}`);
|
|
37
|
+
if (collections.length === 0) {
|
|
38
|
+
// Collection doesn't exist, create it outside of any transaction
|
|
39
|
+
await mongoose.connection.db.createCollection(collectionName);
|
|
40
|
+
console.log(`Created MongoDB collection: ${collectionName}`);
|
|
51
41
|
}
|
|
52
42
|
} catch (error) {
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
// Collection might already exist due to race condition, or other error
|
|
44
|
+
// Log warning but don't fail - let subsequent operations handle errors
|
|
45
|
+
if (error.codeName === 'NamespaceExists') {
|
|
46
|
+
// This is expected in race conditions, silently continue
|
|
47
|
+
return;
|
|
56
48
|
}
|
|
49
|
+
console.warn(`Error ensuring collection ${collectionName} exists:`, error.message);
|
|
57
50
|
}
|
|
58
51
|
}
|
|
59
52
|
|
|
@@ -80,12 +73,11 @@ async function ensureCollectionsExist(collectionNames) {
|
|
|
80
73
|
*/
|
|
81
74
|
async function collectionExists(collectionName) {
|
|
82
75
|
try {
|
|
83
|
-
const collections = await
|
|
84
|
-
listCollections:
|
|
85
|
-
|
|
86
|
-
});
|
|
76
|
+
const collections = await mongoose.connection.db
|
|
77
|
+
.listCollections({ name: collectionName })
|
|
78
|
+
.toArray();
|
|
87
79
|
|
|
88
|
-
return collections.
|
|
80
|
+
return collections.length > 0;
|
|
89
81
|
} catch (error) {
|
|
90
82
|
console.error(`Error checking if collection ${collectionName} exists:`, error.message);
|
|
91
83
|
return false;
|