UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

420 lines 17.9 kB
import { BaseService } from './base.js'; import { DatabaseError, ValidationError } from '../middleware/error-handling.js'; import { securityManager } from '../security/index.js'; import { logger } from '../utils/logger.js'; import { mongoClient } from '../utils/mongodb.js'; export class DatabaseService extends BaseService { constructor(config) { super('DatabaseService', config?.circuitBreaker, config?.retry); this.connectionMap = new Map(); this.queryCount = 0; this.errorCount = 0; this.lastConnectionCheck = new Date(); this.mongoClient = mongoClient; } async initialize() { try { await this.mongoClient.initialize(); logger.info('Database service initialized successfully'); } catch (error) { logger.error('Database service initialization failed:', error); throw new DatabaseError('Failed to initialize database service', { originalError: error.message }); } } async performHealthCheck() { const startTime = Date.now(); try { // Test each connection const connections = ['default', 'devSyiaApi', 'syiaEtlDev']; const healthChecks = await Promise.allSettled(connections.map(async (connName) => { try { const db = await this.mongoClient.getDatabase(connName); await db.admin().ping(); return { connection: connName, status: 'healthy' }; } catch (error) { return { connection: connName, status: 'unhealthy', error: error.message }; } })); const responseTime = Date.now() - startTime; const unhealthyConnections = healthChecks .filter(result => result.status === 'fulfilled' && result.value.status === 'unhealthy') .map(result => result.value); if (unhealthyConnections.length > 0) { return { isHealthy: false, lastCheck: new Date(), responseTime, error: 'Some database connections are unhealthy', details: { unhealthyConnections } }; } this.lastConnectionCheck = new Date(); return { isHealthy: true, lastCheck: new Date(), responseTime, details: { connectionsChecked: connections.length, queryCount: this.queryCount, errorCount: this.errorCount } }; } catch (error) { return { isHealthy: false, lastCheck: new Date(), responseTime: Date.now() - startTime, error: error.message }; } } async findDocuments(collection, query, connectionName = 'default') { return this.executeWithResilience(async () => { const startTime = Date.now(); try { // Security validation and sanitization const securityCheck = securityManager.performSecurityCheck(query.filter, { sanitizationContext: 'mongodb' }); if (!securityCheck.isSecure) { throw new ValidationError(`Database query security validation failed: ${securityCheck.issues.join(', ')}`, 'filter'); } const sanitizedFilter = securityCheck.sanitizedInput; const coll = await this.getCollection(collection, connectionName); // Apply default limits to prevent resource exhaustion const safeLimit = Math.min(query.limit || 100, 1000); const safeSkip = Math.max(query.skip || 0, 0); const options = { ...query.options, limit: safeLimit, skip: safeSkip }; if (query.sort) { options.sort = query.sort; } // Execute query with timeout const documents = await coll.find(sanitizedFilter, options).toArray(); // Get total count for pagination (with timeout) const total = await coll.countDocuments(sanitizedFilter); this.queryCount++; const queryTime = Date.now() - startTime; logger.debug(`Database query completed`, { collection, connectionName, documentsFound: documents.length, queryTime, filter: this.sanitizeLogData(sanitizedFilter) }); return { data: documents, total, limit: safeLimit, skip: safeSkip, hasMore: (safeSkip + documents.length) < total }; } catch (error) { this.errorCount++; const queryTime = Date.now() - startTime; logger.error('Database query failed', { collection, connectionName, queryTime, error: error.message, filter: this.sanitizeLogData(query.filter) }); if (error instanceof ValidationError) { throw error; } throw new DatabaseError(`Database query failed for collection ${collection}`, { originalError: error.message, collection, connectionName, queryTime }, this.isRetryableError(error)); } }, 'findDocuments'); } async findDocumentById(collection, id, connectionName = 'default') { return this.executeWithResilience(async () => { try { const coll = await this.getCollection(collection, connectionName); const document = await coll.findOne({ _id: id }); this.queryCount++; if (document) { logger.debug(`Document found by ID`, { collection, connectionName, id: this.sanitizeLogData(id) }); } return document; } catch (error) { this.errorCount++; logger.error('Database findById failed', { collection, connectionName, id: this.sanitizeLogData(id), error: error.message }); throw new DatabaseError(`Database findById failed for collection ${collection}`, { originalError: error.message, collection, connectionName }, this.isRetryableError(error)); } }, 'findDocumentById'); } async insertDocument(collection, document, connectionName = 'default') { return this.executeWithResilience(async () => { try { // Security validation and sanitization const securityCheck = securityManager.performSecurityCheck(document, { sanitizationContext: 'mongodb' }); if (!securityCheck.isSecure) { throw new ValidationError(`Document security validation failed: ${securityCheck.issues.join(', ')}`, 'document'); } const sanitizedDocument = securityCheck.sanitizedInput; const coll = await this.getCollection(collection, connectionName); const result = await coll.insertOne(sanitizedDocument); this.queryCount++; logger.debug(`Document inserted`, { collection, connectionName, insertedId: result.insertedId }); return { ...sanitizedDocument, _id: result.insertedId }; } catch (error) { this.errorCount++; logger.error('Database insert failed', { collection, connectionName, error: error.message }); if (error instanceof ValidationError) { throw error; } throw new DatabaseError(`Database insert failed for collection ${collection}`, { originalError: error.message, collection, connectionName }, this.isRetryableError(error)); } }, 'insertDocument'); } async updateDocuments(collection, query, connectionName = 'default') { return this.executeWithResilience(async () => { try { // Security validation for filter and update const filterCheck = securityManager.performSecurityCheck(query.filter, { sanitizationContext: 'mongodb' }); const updateCheck = securityManager.performSecurityCheck(query.update, { sanitizationContext: 'mongodb' }); if (!filterCheck.isSecure) { throw new ValidationError(`Update filter security validation failed: ${filterCheck.issues.join(', ')}`, 'filter'); } if (!updateCheck.isSecure) { throw new ValidationError(`Update document security validation failed: ${updateCheck.issues.join(', ')}`, 'update'); } const coll = await this.getCollection(collection, connectionName); const result = await coll.updateMany(filterCheck.sanitizedInput, updateCheck.sanitizedInput, query.options); this.queryCount++; logger.debug(`Documents updated`, { collection, connectionName, matchedCount: result.matchedCount, modifiedCount: result.modifiedCount }); return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount }; } catch (error) { this.errorCount++; logger.error('Database update failed', { collection, connectionName, error: error.message }); if (error instanceof ValidationError) { throw error; } throw new DatabaseError(`Database update failed for collection ${collection}`, { originalError: error.message, collection, connectionName }, this.isRetryableError(error)); } }, 'updateDocuments'); } async deleteDocuments(collection, query, connectionName = 'default') { return this.executeWithResilience(async () => { try { // Security validation for filter const securityCheck = securityManager.performSecurityCheck(query.filter, { sanitizationContext: 'mongodb' }); if (!securityCheck.isSecure) { throw new ValidationError(`Delete filter security validation failed: ${securityCheck.issues.join(', ')}`, 'filter'); } const coll = await this.getCollection(collection, connectionName); const result = await coll.deleteMany(securityCheck.sanitizedInput, query.options); this.queryCount++; logger.debug(`Documents deleted`, { collection, connectionName, deletedCount: result.deletedCount }); return { deletedCount: result.deletedCount }; } catch (error) { this.errorCount++; logger.error('Database delete failed', { collection, connectionName, error: error.message }); if (error instanceof ValidationError) { throw error; } throw new DatabaseError(`Database delete failed for collection ${collection}`, { originalError: error.message, collection, connectionName }, this.isRetryableError(error)); } }, 'deleteDocuments'); } async aggregateDocuments(collection, pipeline, connectionName = 'default') { return this.executeWithResilience(async () => { try { // Security validation for aggregation pipeline const securityCheck = securityManager.performSecurityCheck(pipeline, { sanitizationContext: 'mongodb' }); if (!securityCheck.isSecure) { throw new ValidationError(`Aggregation pipeline security validation failed: ${securityCheck.issues.join(', ')}`, 'pipeline'); } const coll = await this.getCollection(collection, connectionName); const results = await coll.aggregate(securityCheck.sanitizedInput).toArray(); this.queryCount++; logger.debug(`Aggregation completed`, { collection, connectionName, resultsCount: results.length, pipelineStages: pipeline.length }); return results; } catch (error) { this.errorCount++; logger.error('Database aggregation failed', { collection, connectionName, error: error.message }); if (error instanceof ValidationError) { throw error; } throw new DatabaseError(`Database aggregation failed for collection ${collection}`, { originalError: error.message, collection, connectionName }, this.isRetryableError(error)); } }, 'aggregateDocuments'); } async getCollection(collectionName, connectionName = 'default') { try { return await this.mongoClient.getCollection(collectionName, connectionName); } catch (error) { throw new DatabaseError(`Failed to get collection ${collectionName} from connection ${connectionName}`, { originalError: error.message }); } } isRetryableError(error) { // MongoDB specific retryable errors const retryableErrorCodes = [ 11000, // Duplicate key (in some cases) 16500, // Replica set not found 91, // Shutdown in progress 189, // Primary stepped down 11602, // Interrupted 11601, // Interrupted at shutdown 10107, // Not master 13435, // Not master or secondary 13436, // Not master, no secondary 10054, // Socket error 6 // Host unreachable ]; if (error.code && retryableErrorCodes.includes(error.code)) { return true; } // Network related errors const retryableNetworkCodes = ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET']; if (retryableNetworkCodes.includes(error.code)) { return true; } return false; } sanitizeLogData(data) { return securityManager.sanitizeLogData(data); } getConnectionStats() { const stats = {}; try { const connections = ['default', 'devSyiaApi', 'syiaEtlDev']; for (const connName of connections) { try { const db = this.mongoClient.getDatabase(connName); stats[connName] = { connected: true, lastConnectionCheck: this.lastConnectionCheck }; } catch (error) { stats[connName] = { connected: false, error: error instanceof Error ? error.message : String(error) }; } } } catch (error) { logger.error('Failed to get connection stats:', error); } return stats; } getServiceStats() { const successRate = this.queryCount > 0 ? ((this.queryCount - this.errorCount) / this.queryCount) * 100 : 100; return { queryCount: this.queryCount, errorCount: this.errorCount, successRate: Math.round(successRate * 100) / 100, lastHealthCheck: this.lastConnectionCheck, connections: this.getConnectionStats() }; } async shutdown() { try { await this.mongoClient.disconnect(); await super.shutdown(); logger.info('Database service shutdown completed'); } catch (error) { logger.error('Error during database service shutdown:', error); throw error; } } } export const databaseService = new DatabaseService(); //# sourceMappingURL=database.js.map