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
JavaScript
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