survey-mcp-server
Version:
Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management
335 lines • 13 kB
JavaScript
import { MongoClient } from 'mongodb';
import { config } from './config.js';
import { logger } from './logger.js';
export class MongoDBConnection {
constructor(connectionString, databaseName) {
this.client = null;
this.db = null;
this.connectionString = connectionString;
this.databaseName = databaseName;
}
async connect() {
if (this.client) {
return; // Already connected
}
try {
this.client = new MongoClient(this.connectionString);
await this.client.connect();
this.db = this.client.db(this.databaseName);
logger.info(`Connected to MongoDB database: ${this.databaseName}`);
}
catch (error) {
logger.error(`Failed to connect to MongoDB database ${this.databaseName}:`, error);
throw error;
}
}
async disconnect() {
if (this.client) {
await this.client.close();
this.client = null;
this.db = null;
logger.info(`Disconnected from MongoDB database: ${this.databaseName}`);
}
}
getDatabase() {
if (!this.db) {
throw new Error(`Database ${this.databaseName} is not connected`);
}
return this.db;
}
getCollection(name) {
return this.getDatabase().collection(name);
}
}
export class MongoDBClient {
constructor() {
this.connections = new Map();
this.initialized = false;
// Don't initialize connections immediately
// They will be initialized on first use or explicit initialization
}
static getInstance() {
if (!MongoDBClient.instance) {
MongoDBClient.instance = new MongoDBClient();
}
return MongoDBClient.instance;
}
async initialize() {
try {
logger.info('Initializing MongoDB client with config:', {
defaultUri: config.mongodb.uri ? 'configured' : 'missing',
defaultDbName: config.mongodb.dbName,
devSyiaApiUri: config.devSyiaApi.uri ? 'configured' : 'missing',
devSyiaApiDbName: config.devSyiaApi.dbName,
syiaEtlDevUri: config.syiaEtlDev.uri ? 'configured' : 'missing',
syiaEtlDevDbName: config.syiaEtlDev.dbName
});
// Initialize connections with current config
this.connections.clear();
this.connections.set('default', new MongoDBConnection(config.mongodb.uri, config.mongodb.dbName));
this.connections.set('devSyiaApi', new MongoDBConnection(config.devSyiaApi.uri, config.devSyiaApi.dbName));
this.connections.set('syiaEtlDev', new MongoDBConnection(config.syiaEtlDev.uri, config.syiaEtlDev.dbName));
// Actually connect to all databases during initialization
const connectionPromises = [];
for (const [name, connection] of this.connections.entries()) {
logger.info(`Connecting to MongoDB database: ${name}`);
connectionPromises.push(connection.connect().then(() => {
logger.info(`Successfully connected to MongoDB database: ${name}`);
}).catch((error) => {
logger.error(`Failed to connect to MongoDB database ${name}:`, error);
throw error;
}));
}
// Wait for all connections to complete
await Promise.all(connectionPromises);
this.initialized = true;
logger.info('MongoDB client initialized successfully with all database connections established');
}
catch (error) {
logger.error('Failed to initialize MongoDB client:', error);
this.initialized = false;
throw error;
}
}
async reinitialize() {
logger.info('Reinitializing MongoDB client...');
await this.disconnect();
this.connections.clear();
this.initialized = false;
await this.initialize();
logger.info('MongoDB client reinitialized successfully');
}
async ensureInitialized() {
if (!this.initialized) {
await this.initialize();
}
}
isInitialized() {
return this.initialized;
}
async getConnection(name = 'default') {
await this.ensureInitialized();
const connection = this.connections.get(name);
if (!connection) {
throw new Error(`MongoDB connection '${name}' not found`);
}
await connection.connect();
return connection;
}
async getDatabase(connectionName = 'default') {
const connection = await this.getConnection(connectionName);
return connection.getDatabase();
}
async getCollection(collectionName, connectionName = 'default') {
const connection = await this.getConnection(connectionName);
return connection.getCollection(collectionName);
}
// Legacy property for backward compatibility
get db() {
if (!this.initialized) {
throw new Error('MongoDB client not initialized. Call initialize() first.');
}
const connection = this.connections.get('default');
if (!connection) {
throw new Error('Default MongoDB connection not found');
}
return connection.getDatabase();
}
async disconnect(connectionName) {
if (connectionName) {
const connection = this.connections.get(connectionName);
if (connection) {
await connection.disconnect();
}
}
else {
// Disconnect all connections
for (const connection of this.connections.values()) {
await connection.disconnect();
}
}
}
}
// Helper function to get a MongoDB client with specific connection string
export async function getMongoClient(connectionString, databaseName) {
try {
const client = new MongoClient(connectionString);
await client.connect();
logger.info(`Connected to MongoDB: ${databaseName || 'Unknown DB'}`);
return client;
}
catch (error) {
logger.error('Failed to connect to MongoDB:', error);
throw error;
}
}
// Helper function to get component data
async function getComponentData(componentId) {
const match = componentId.match(/^(\d+)_(\d+)_(\d+)$/);
if (!match) {
return `⚠️ Invalid component_id format: ${componentId}`;
}
const [, componentNumber, questionNumber, imo] = match;
const componentNo = `${componentNumber}_${questionNumber}_${imo}`;
try {
const client = MongoDBClient.getInstance();
const db = await client.getDatabase('syiaEtlDev');
const collection = db.collection('vesselinfocomponents');
const doc = await collection.findOne({ componentNo });
if (!doc) {
return `⚠️ No component found for ID: ${componentId}`;
}
if (!doc.data) {
return "No data found in the table component";
}
if (!doc.data.headers || !Array.isArray(doc.data.headers)) {
logger.error(`Invalid headers in component data: ${componentId}`);
return "Invalid headers in table component";
}
if (!doc.data.body || !Array.isArray(doc.data.body)) {
logger.error(`Invalid body in component data: ${componentId}`);
return "Invalid body in table component";
}
// Extract headers excluding lineitem
const headers = doc.data.headers
.filter((h) => h && h.name !== "lineitem")
.map((h) => h.name);
const rows = doc.data.body;
// Build markdown table
let md = "| " + headers.join(" | ") + " |\n";
md += "| " + headers.map(() => "---").join(" | ") + " |\n";
for (const [rowIndex, row] of rows.entries()) {
try {
if (!Array.isArray(row))
continue;
const formattedRow = row
.filter((cell) => cell && !cell.lineitem)
.map((cell) => {
try {
if (cell.value && cell.link) {
return `[${cell.value}](${cell.link})`;
}
else if (cell.status && cell.color) {
return cell.status;
}
return String(cell);
}
catch (cellError) {
return '';
}
});
if (formattedRow.length > 0) {
md += "| " + formattedRow.join(" | ") + " |\n";
}
}
catch (rowError) {
logger.error(`Error processing row ${rowIndex}:`, rowError);
continue;
}
}
return md;
}
catch (error) {
logger.error(`Error getting component data:`, error);
throw error;
}
}
// Helper function to add component data
async function addComponentData(answer, imo) {
// The actual URL format in the response
const pattern = /httpsdev\.syia\.ai\/chat\/ag-grid-table\?component=(\d+_\d+)/g;
const matches = Array.from(answer.matchAll(pattern));
let result = answer;
for (const match of matches) {
const component = match[1];
try {
logger.info(`Processing component ${component}_${imo}`);
const replacement = await getComponentData(`${component}_${imo}`);
// Replace the full URL with the table
result = result.replace(match[0], replacement);
logger.info(`Successfully replaced component ${component}_${imo}`);
}
catch (error) {
logger.error(`Error replacing component ${component}_${imo}:`, error);
}
}
return result;
}
// Helper function for vessel QnA operations
export async function fetchQaDetails(imo, questionNo) {
try {
const client = MongoDBClient.getInstance();
const db = await client.getDatabase('syiaEtlDev');
const collection = db.collection('vesselinfos');
const query = {
imo: imo,
questionNo: parseInt(questionNo)
};
const projection = {
'_id': 0,
'imo': 1,
'vesselName': 1,
'refreshDate': 1,
'answer': 1
};
const result = await collection.findOne(query, { projection });
if (!result) {
return {
imo: imo,
vesselName: null,
refreshDate: null,
answer: null
};
}
// Format refresh date
if (result.refreshDate instanceof Date) {
const dateStr = result.refreshDate.toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
result.refreshDate = dateStr;
}
// Process answer to include component data
if (result.answer) {
result.answer = await addComponentData(result.answer, imo);
}
// Add snapshot link
try {
result.link = await getVesselQnaSnapshot(String(imo), questionNo);
}
catch (error) {
logger.warn(`Failed to get QnA snapshot for IMO ${imo}, question ${questionNo}:`, error);
result.link = null;
}
return result;
}
catch (error) {
logger.error(`Error fetching QA details for IMO ${imo}, question ${questionNo}:`, error);
throw error;
}
}
// Helper function to get vessel QnA snapshot
async function getVesselQnaSnapshot(imo, questionNo) {
try {
const axios = await import('axios');
const snapshotUrl = `https://app-api.siya.com/v1.0/vessel-info/qna-snapshot/${imo}/${questionNo}`;
const jwtToken = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjoiNjRkMzdhMDM1Mjk5YjFlMDQxOTFmOTJhIiwiZmlyc3ROYW1lIjoiU3lpYSIsImxhc3ROYW1lIjoiRGV2IiwiZW1haWwiOiJkZXZAc3lpYS5haSIsInJvbGUiOiJhZG1pbiIsInJvbGVJZCI6IjVmNGUyODFkZDE4MjM0MzY4NDE1ZjViZiIsImlhdCI6MTc0MDgwODg2OH0sImlhdCI6MTc0MDgwODg2OCwiZXhwIjoxNzcyMzQ0ODY4fQ.1grxEO0aO7wfkSNDzpLMHXFYuXjaA1bBguw2SJS9r2M";
const response = await axios.default.get(snapshotUrl, {
headers: {
Authorization: jwtToken
}
});
if (response.data && response.data.resultData) {
return response.data.resultData;
}
return response.data;
}
catch (error) {
logger.warn(`Failed to get QnA snapshot for IMO ${imo}, question ${questionNo}:`, error);
return null;
}
}
// Export singleton instance - but don't initialize it yet
export const mongoClient = MongoDBClient.getInstance();
//# sourceMappingURL=mongodb.js.map