UNPKG

pms-analysis-reports-mcp-server

Version:

PMS analysis reports server handling maintenance reports, equipment analysis, compliance tracking, and performance metrics with ERP access for data extraction

372 lines 13.9 kB
import { MongoClient } from 'mongodb'; import { logger } from "../utils/logger.js"; import { BaseService } from "./base-service.js"; import { SanitizationMiddleware } from "../middleware/sanitization-middleware.js"; export class DatabaseService extends BaseService { constructor(dbConfig) { super(dbConfig); this.dbConfig = dbConfig; this.isConnected = false; } /** * Connect to database */ async connect() { return this.executeWithResilience(async () => { if (this.isConnected && this.client) { return true; } logger.info('Connecting to database', { dbName: this.dbConfig.dbName, uri: this.sanitizeUri(this.dbConfig.uri) }); this.client = new MongoClient(this.dbConfig.uri, { maxPoolSize: this.dbConfig.connectionOptions?.maxPoolSize || 10, minPoolSize: this.dbConfig.connectionOptions?.minPoolSize || 2, maxIdleTimeMS: this.dbConfig.connectionOptions?.maxIdleTimeMS || 30000, serverSelectionTimeoutMS: this.dbConfig.connectionOptions?.serverSelectionTimeoutMS || 5000 }); await this.client.connect(); this.db = this.client.db(this.dbConfig.dbName); this.isConnected = true; logger.info('Database connected successfully', { dbName: this.dbConfig.dbName }); return true; }, { toolName: 'database_connect' }); } /** * Disconnect from database */ async disconnect() { return this.executeWithResilience(async () => { if (this.client && this.isConnected) { await this.client.close(); this.isConnected = false; logger.info('Database disconnected'); } return true; }, { toolName: 'database_disconnect' }); } /** * Find documents in collection */ async find(collectionName, query, options = {}) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedQuery = this.sanitizeQuery(query, options); const findOptions = this.buildFindOptions(options); logger.debug('Executing find query', { collection: collectionName, query: SanitizationMiddleware.sanitizeForLogging(sanitizedQuery), options: SanitizationMiddleware.sanitizeForLogging(findOptions) }); const cursor = collection.find(sanitizedQuery, findOptions); const results = await cursor.toArray(); logger.debug('Find query completed', { collection: collectionName, resultCount: results.length }); return results; }, { toolName: 'database_find', arguments: { collection: collectionName, query } }); } /** * Find one document in collection */ async findOne(collectionName, query, options = {}) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedQuery = this.sanitizeQuery(query, options); const findOptions = this.buildFindOptions(options); logger.debug('Executing findOne query', { collection: collectionName, query: SanitizationMiddleware.sanitizeForLogging(sanitizedQuery) }); const result = await collection.findOne(sanitizedQuery, findOptions); logger.debug('FindOne query completed', { collection: collectionName, found: !!result }); return result; }, { toolName: 'database_find_one', arguments: { collection: collectionName, query } }); } /** * Aggregate documents in collection */ async aggregate(collectionName, pipeline, options = {}) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedPipeline = this.sanitizeAggregationPipeline(pipeline, options); logger.debug('Executing aggregation', { collection: collectionName, pipelineLength: sanitizedPipeline.length, pipeline: SanitizationMiddleware.sanitizeForLogging(sanitizedPipeline) }); const cursor = collection.aggregate(sanitizedPipeline); const results = await cursor.toArray(); logger.debug('Aggregation completed', { collection: collectionName, resultCount: results.length }); return results; }, { toolName: 'database_aggregate', arguments: { collection: collectionName, pipelineLength: pipeline.length } }); } /** * Count documents in collection */ async count(collectionName, query = {}, options = {}) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedQuery = this.sanitizeQuery(query, options); logger.debug('Executing count query', { collection: collectionName, query: SanitizationMiddleware.sanitizeForLogging(sanitizedQuery) }); const count = await collection.countDocuments(sanitizedQuery); logger.debug('Count query completed', { collection: collectionName, count }); return count; }, { toolName: 'database_count', arguments: { collection: collectionName, query } }); } /** * Insert document into collection */ async insertOne(collectionName, document) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedDocument = SanitizationMiddleware.sanitizeToolResponse(document); logger.debug('Executing insertOne', { collection: collectionName }); const result = await collection.insertOne(sanitizedDocument); logger.debug('InsertOne completed', { collection: collectionName, insertedId: result.insertedId.toString() }); return result.insertedId.toString(); }, { toolName: 'database_insert_one', arguments: { collection: collectionName } }); } /** * Update document in collection */ async updateOne(collectionName, filter, update, options = {}) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedFilter = this.sanitizeQuery(filter, options); const sanitizedUpdate = this.sanitizeUpdate(update); logger.debug('Executing updateOne', { collection: collectionName, filter: SanitizationMiddleware.sanitizeForLogging(sanitizedFilter) }); const result = await collection.updateOne(sanitizedFilter, sanitizedUpdate); logger.debug('UpdateOne completed', { collection: collectionName, matchedCount: result.matchedCount, modifiedCount: result.modifiedCount }); return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount }; }, { toolName: 'database_update_one', arguments: { collection: collectionName, filter } }); } /** * Delete document from collection */ async deleteOne(collectionName, filter, options = {}) { return this.executeWithResilience(async () => { await this.ensureConnected(); const collection = this.getCollection(collectionName); const sanitizedFilter = this.sanitizeQuery(filter, options); logger.debug('Executing deleteOne', { collection: collectionName, filter: SanitizationMiddleware.sanitizeForLogging(sanitizedFilter) }); const result = await collection.deleteOne(sanitizedFilter); logger.debug('DeleteOne completed', { collection: collectionName, deletedCount: result.deletedCount }); return result.deletedCount; }, { toolName: 'database_delete_one', arguments: { collection: collectionName, filter } }); } /** * Ensure database connection */ async ensureConnected() { if (!this.isConnected) { const result = await this.connect(); if (!result.success) { throw new Error('Failed to connect to database'); } } } /** * Get collection instance */ getCollection(name) { if (!this.db) { throw new Error('Database not connected'); } return this.db.collection(name); } /** * Sanitize MongoDB query */ sanitizeQuery(query, options) { if (options.sanitize === false) { return query; } return SanitizationMiddleware.sanitizeMongoQuery(query, { allowedFields: options.allowedFields, maxStringLength: 1000, allowObjectIds: true }); } /** * Sanitize update operations */ sanitizeUpdate(update) { // Ensure update operations are safe const sanitized = SanitizationMiddleware.sanitizeMongoQuery(update, { maxStringLength: 1000, allowObjectIds: true }); // Remove dangerous update operators const dangerousOps = ['$where', '$eval', '$function']; for (const op of dangerousOps) { delete sanitized[op]; } return sanitized; } /** * Sanitize aggregation pipeline */ sanitizeAggregationPipeline(pipeline, options) { if (options.sanitize === false) { return pipeline; } const maxLength = options.maxPipelineLength || 10; const allowedStages = options.allowedStages || [ '$match', '$group', '$sort', '$limit', '$skip', '$project', '$lookup', '$unwind' ]; if (pipeline.length > maxLength) { throw new Error(`Aggregation pipeline too long (max ${maxLength} stages)`); } return pipeline.map((stage, index) => { if (typeof stage !== 'object' || stage === null) { throw new Error(`Invalid pipeline stage at index ${index}`); } const stageKeys = Object.keys(stage); if (stageKeys.length !== 1) { throw new Error(`Pipeline stage must have exactly one operator at index ${index}`); } const operator = stageKeys[0]; if (!allowedStages.includes(operator)) { throw new Error(`Disallowed pipeline operator: ${operator}`); } return SanitizationMiddleware.sanitizeMongoQuery(stage, { maxStringLength: 1000, allowObjectIds: true }); }); } /** * Build find options */ buildFindOptions(options) { const findOptions = {}; if (options.limit !== undefined) { findOptions.limit = Math.min(Math.max(1, options.limit), 1000); // Cap at 1000 } if (options.skip !== undefined) { findOptions.skip = Math.max(0, options.skip); } if (options.sort) { findOptions.sort = options.sort; } return findOptions; } /** * Sanitize URI for logging */ sanitizeUri(uri) { return uri.replace(/\/\/[^@]*@/, '//***@'); } /** * Health check implementation */ async healthCheck() { try { await this.ensureConnected(); if (!this.db) { return false; } // Test with a simple ping operation await this.db.admin().ping(); return true; } catch (error) { logger.error('Database health check failed', { error: SanitizationMiddleware.sanitizeErrorMessage(error) }); return false; } } /** * Get database statistics */ async getStats() { return this.executeWithResilience(async () => { await this.ensureConnected(); if (!this.db) { throw new Error('Database not connected'); } const stats = await this.db.stats(); return SanitizationMiddleware.sanitizeToolResponse(stats); }, { toolName: 'database_stats' }); } /** * List collections */ async listCollections() { return this.executeWithResilience(async () => { await this.ensureConnected(); if (!this.db) { throw new Error('Database not connected'); } const collections = await this.db.listCollections().toArray(); return collections.map(col => col.name); }, { toolName: 'database_list_collections' }); } } //# sourceMappingURL=database-service.js.map