UNPKG

@bhagat-surya-dev/dashchat-database-manager

Version:

AI-powered database schema analysis and management library

304 lines 14 kB
"use strict"; 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