@bhagat-surya-dev/dashchat-database-manager
Version:
AI-powered database schema analysis and management library
304 lines • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoDbHandler = void 0;
// MongoDB specific handler
const mongodb_1 = require("mongodb");
const perf_hooks_1 = require("perf_hooks");
const base_handler_1 = require("./base-handler");
class MongoDbHandler extends base_handler_1.BaseDatabaseHandler {
async checkConnection(client) {
try {
await client.db().command({ ping: 1 });
return { success: true };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
success: false,
error: `MongoDB connection failed: ${errorMessage}`
};
}
}
getDatabaseType() {
return 'mongodb';
} // Extract database name from connection string
extractDatabaseInfo(databaseUrl) {
try {
const urlObj = new URL(databaseUrl);
let databaseName = null;
// Extract database name from path
const pathname = urlObj.pathname;
if (pathname && pathname.length > 1) {
const pathParts = pathname.split('/').filter(Boolean);
if (pathParts.length > 0) {
const dbName = pathParts[0].split('?')[0]; // Remove query parameters
if (dbName && dbName !== 'admin' && dbName !== 'local' && dbName !== 'config') {
databaseName = dbName;
}
}
}
// If no database name in path, try URL parameters
if (!databaseName) {
const params = new URLSearchParams(urlObj.search);
const defaultDb = params.get('defaultDatabase') || params.get('db');
if (defaultDb && defaultDb !== 'admin' && defaultDb !== 'local' && defaultDb !== 'config') {
databaseName = defaultDb;
}
}
return { databaseName };
}
catch (urlError) {
console.debug('Failed to extract database info from URL:', urlError);
return { databaseName: null };
}
}
// Get collection field info and sample data
async getCollectionInfo(db, collectionName) {
const method = 'getCollectionInfo';
const collection = db.collection(collectionName);
try {
const sampleDocs = await collection.find().limit(this.maxSampleSize).toArray();
if (sampleDocs.length === 0) {
return {
name: collectionName,
columns: [],
sampleData: [],
};
}
const fieldDetails = {};
for (const doc of sampleDocs) {
for (const key in doc) {
if (!fieldDetails[key]) {
fieldDetails[key] = { types: new Set(), count: 0 };
}
// Basic type inference
const valueType = typeof doc[key];
fieldDetails[key].types.add(valueType);
// For arrays, we can add more detail
if (Array.isArray(doc[key])) {
fieldDetails[key].types.add('array');
// Optional: detect array element types
if (doc[key].length > 0) {
const elementType = typeof doc[key][0];
fieldDetails[key].types.add(`array<${elementType}>`);
}
}
// For objects (excluding arrays and nulls), we can add more context
if (valueType === 'object' && doc[key] !== null && !Array.isArray(doc[key])) {
if (key === '_id') {
fieldDetails[key].types.add('ObjectId');
}
else {
fieldDetails[key].types.add('object');
}
}
fieldDetails[key].count++;
}
}
const columns = Object.keys(fieldDetails).map(fieldName => ({
column_name: fieldName,
// Join types with a pipe (e.g., "string | number")
type: Array.from(fieldDetails[fieldName].types).join(' | '),
data_type: Array.from(fieldDetails[fieldName].types)[0] || 'unknown',
present_in_all_samples: fieldDetails[fieldName].count === sampleDocs.length
}));
return {
name: collectionName,
columns,
sampleData: sampleDocs.map(doc => {
// Remove _id to avoid serialization issues with ObjectId
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _id, ...rest } = doc;
return rest;
})
};
}
catch (error) {
this.logError(method, error, { collectionName });
throw error;
}
}
// Enhanced connection retry logic
async connectWithRetry(client, maxRetries = 3) {
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await client.connect();
return; // Success
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries) {
const backoffMs = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Exponential backoff, max 5s
this.logInfo('connectWithRetry', `Connection attempt ${attempt} failed, retrying in ${backoffMs}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffMs));
}
}
}
throw lastError || new Error('All connection attempts failed');
}
// Get MongoDB connection options with timeouts
getConnectionOptions() {
return {
connectTimeoutMS: 10000, // 10 seconds to establish connection
serverSelectionTimeoutMS: 5000, // 5 seconds to select server
socketTimeoutMS: 45000, // 45 seconds for socket operations
heartbeatFrequencyMS: 10000, // Check server status every 10 seconds
maxPoolSize: 10, // Maximum connections in pool
minPoolSize: 2, // Minimum connections in pool
maxIdleTimeMS: 30000, // Close connections after 30 seconds of inactivity
retryWrites: true, // Retry write operations
retryReads: true, // Retry read operations
directConnection: false, // Use connection pooling
};
}
// Test if connection is valid
async testConnection(databaseUrl) {
const method = 'testConnection';
const start = perf_hooks_1.performance.now();
if (!databaseUrl) {
this.logError(method, new Error('Database URL is required'));
return false;
}
const client = new mongodb_1.MongoClient(databaseUrl, this.getConnectionOptions());
try {
await this.connectWithRetry(client);
const connectionCheck = await this.checkConnection(client);
if (!connectionCheck.success) {
this.logError(method, new Error(connectionCheck.error || 'Connection check failed'));
return false;
}
this.logInfo(method, 'Successfully connected to MongoDB database');
return true;
}
catch (error) {
this.logError(method, error, {
databaseUrl: databaseUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'), // Hide credentials in logs
errorMessage: error instanceof Error ? error.message : 'Unknown error'
});
return false;
}
finally {
try {
await client.close();
}
catch (closeError) {
this.logInfo(method, 'Warning: Error closing MongoDB connection');
}
const duration = perf_hooks_1.performance.now() - start;
this.log(base_handler_1.LogLevel.DEBUG, {
method,
action: 'performance_measurement',
duration,
metadata: { durationMs: duration.toFixed(2) }
}, 'Connection test completed');
}
}
// Main method to get schema info
async getSchemaInfo(databaseUrl) {
const method = 'getSchemaInfo';
const start = perf_hooks_1.performance.now();
if (!databaseUrl) {
this.log(base_handler_1.LogLevel.ERROR, { method, action: 'validation' }, 'Database URL is required.');
throw new Error('Please provide a valid database URL');
}
const client = new mongodb_1.MongoClient(databaseUrl, this.getConnectionOptions());
try {
await this.connectWithRetry(client);
// Check connection first
const connectionCheck = await this.checkConnection(client);
if (!connectionCheck.success) {
throw new Error(connectionCheck.error || 'Failed to establish database connection');
} // Determine database name - prioritize URL parsing over listing databases
let dbName;
// First try to extract from URL (more reliable and doesn't require admin permissions)
const extractedInfo = this.extractDatabaseInfo(databaseUrl);
dbName = extractedInfo.databaseName || undefined;
if (dbName) {
this.logInfo(method, `Found database name in URL: ${dbName}`);
}
else {
// Fallback: try to get list of available databases
try {
const adminDb = client.db('admin');
const dbList = await adminDb.admin().listDatabases();
// Find first non-system database
const userDb = dbList.databases.find(db => !['admin', 'local', 'config'].includes(db.name));
if (userDb) {
dbName = userDb.name;
this.logInfo(method, `Found database via listing: ${dbName}`);
}
}
catch (listError) {
this.logInfo(method, `Failed to list databases (likely insufficient permissions), using default database. Error: ${listError}`);
}
}
// Use either the found name or default to the connection's default database
const db = dbName ? client.db(dbName) : client.db();
// Get the actual database name being used
let actualDbName;
try {
// Try to get the actual database name from the db object
actualDbName = db.databaseName;
if (!actualDbName || actualDbName === 'test') {
// If we don't have a good name, use what we extracted or a default
actualDbName = dbName || 'default';
}
}
catch (error) {
this.logInfo(method, `Error retrieving database name, using fallback. Error: ${error}`);
actualDbName = dbName || 'default';
}
this.logInfo(method, `Using database: ${actualDbName}`);
// Get all collections
const collections = await db.listCollections().toArray();
// Process each collection to get its schema
const rawCollectionsInfo = await Promise.all(collections.map(async (collection) => this.getCollectionInfo(db, collection.name)));
// Return the formatted schema with MongoDB-specific connection config
return {
databaseType: this.getDatabaseType(),
databaseName: actualDbName, connectionConfig: {
connectionString: databaseUrl,
dbName: actualDbName, // This is the actual database being used
databaseName: extractedInfo.databaseName || actualDbName // The database name extracted from URL
},
tables: rawCollectionsInfo.map(coll => ({
name: coll.name,
columns: coll.columns.map(field => ({
name: field.column_name,
type: field.type || 'unknown',
nullable: !(field.present_in_all_samples === true),
description: field.column_name === '_id'
? 'MongoDB document identifier'
: `${field.column_name} field of type ${field.type}`
})),
sampleData: coll.sampleData
}))
};
}
catch (error) {
this.logError(method, error, {
databaseUrl: databaseUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'), // Hide credentials in logs
errorDetails: error instanceof Error ? error.message : 'Unknown error'
});
throw error;
}
finally {
try {
await client.close();
}
catch (closeError) {
this.logInfo(method, 'Warning: Error closing MongoDB connection');
}
const duration = perf_hooks_1.performance.now() - start;
this.log(base_handler_1.LogLevel.DEBUG, {
method,
action: 'performance_measurement',
duration,
metadata: { durationMs: duration.toFixed(2) }
}, 'Schema extraction completed');
}
}
}
exports.MongoDbHandler = MongoDbHandler;
//# sourceMappingURL=mongodb-handler.js.map