UNPKG

voyage-and-consumption-mcp-server

Version:

Voyage and consumption management server handling vessel voyages, fuel consumption, performance monitoring, and operational data with ERP access for data extraction

400 lines 18 kB
/** * Database Query Builder Utility * * Centralizes MongoDB query building logic that was duplicated across * 8 different database operations in the codebase. * * This utility extracts common patterns: * - IMO No filtering (5 identical instances) * - Date range filtering (5 identical instances) * - Field projections (5 different projection patterns) * - Data transformation and sorting (5 similar instances) * - Component queries and CRUD operations */ import { logger } from '../utils/logger.js'; import { ObjectId } from 'mongodb'; import { SanitizationMiddleware } from '../middleware/sanitization-middleware.js'; /** * Builds the basic IMO query filter with input sanitization * @param imo - The IMO number to filter by * @returns MongoDB query object for IMO filtering */ export function buildImoQuery(imo) { // Validate and sanitize IMO number if (!Number.isInteger(imo) || imo <= 0) { throw new Error(`Invalid IMO number: ${imo}`); } return { "IMO No": imo }; } /** * Adds date range filtering to an existing query with input validation * @param query - The existing query object * @param dateRange - Optional start and end dates * @returns Updated query with date filtering */ export function addDateRangeFilter(query, dateRange) { if (!dateRange || (!dateRange.startDate && !dateRange.endDate)) { return query; } const { startDate, endDate } = dateRange; if (startDate || endDate) { query["Report Date"] = {}; if (startDate) { // Sanitize and validate date string const sanitizedStartDate = SanitizationMiddleware.sanitizeDateString(startDate); const start = new Date(sanitizedStartDate); if (isNaN(start.getTime())) { throw new Error(`Invalid start date: ${startDate}`); } query["Report Date"]["$gte"] = start; logger.info(`Added start date filter: ${start.toISOString()}`); } if (endDate) { // Sanitize and validate date string const sanitizedEndDate = SanitizationMiddleware.sanitizeDateString(endDate); const end = new Date(sanitizedEndDate); if (isNaN(end.getTime())) { throw new Error(`Invalid end date: ${endDate}`); } query["Report Date"]["$lte"] = end; logger.info(`Added end date filter: ${end.toISOString()}`); } } return query; } /** * Builds a complete vessel data query with IMO and optional date filtering * @param imo - The IMO number to filter by * @param dateRange - Optional date range filter * @returns Complete MongoDB query object */ export function buildVesselDataQuery(imo, dateRange) { const query = buildImoQuery(imo); return addDateRangeFilter(query, dateRange); } /** * Gets the field projection for different data types * @param dataType - The type of data being queried * @returns MongoDB projection object */ export function getProjectionByType(dataType) { const baseProjection = { "_id": 0, "Report Date": 1, "Vessel Name": 1 }; switch (dataType) { case 'fuel': return { ...baseProjection, "data.Steaming time (HRS)": 1, "data.ME Total Consumption": 1, "data.AE Total Consumption": 1, "data.BLR Total Consumption": 1, "data.Total Consumption HSFO": 1, "data.Total Consumption ULSFO": 1, "data.Total Consumption MDO": 1, "data.Total Consumption LSMGO": 1, "data.Total Consumption VLSFO": 1, "data.Total Consumption LNG": 1, "data.AE HSFO consumption (MT)": 1, "data.ME HSFO consumption (MT)": 1, "data.BLR HSFO consumption (MT)": 1, "data.AE ULSFO consumption (MT)": 1, "data.ME ULSFO consumption (MT)": 1, "data.BLR ULSFO consumption (MT)": 1, "data.AE MDO consumption (MT)": 1, "data.ME MDO consumption (MT)": 1, "data.BLR MDO consumption (MT)": 1, "data.AE LS MGO consumption (MT)": 1, "data.ME LS MGO consumption (MT)": 1, "data.BLR LS MGO consumption (MT)": 1, "data.AE VLSFO consumption (MT)": 1, "data.ME VLSFO consumption (MT)": 1, "data.BLR VLSFO consumption (MT)": 1, "data.AE LNG consumption (MT)": 1, "data.ME LNG consumption (MT)": 1, "data.BLR LNG consumption (MT)": 1, "data.ROB HSFO": 1, "data.ROB ULSFO": 1, "data.ROB MDO": 1, "data.ROB LS MGO": 1, "data.ROB VLSFO": 1, "data.ROB LNG": 1, "data.ChartererPartySpeed": 1, "data.ChartererPartyConsumption": 1 }; case 'freshwater': return { ...baseProjection, "Event": 1, "EventType": 1, "data.Steaming time (HRS)": 1, "data.FW production (MT)": 1, "data.FW consumption (MT)": 1, "data.ROB FW": 1, "data.freshWaterConsumption": 1 }; case 'mecc': return { ...baseProjection, "data.Steaming time (HRS)": 1, "data.MECC consumption (LTRS)": 1, "data.ROB MECC": 1 }; case 'cylinder': return { ...baseProjection, "data.Steaming time (HRS)": 1, "data.ME cyl LS 40 BN ROB consumption (LTRS)": 1, "data.ME cyl 70/100 BN ROB/Cap consumption (LTRS)": 1, "data.MECYL 40BN consumption (MT)": 1, "data.MECYL 20/25 BN consumption (MT)": 1, "data.MECYL 70BN consumption (MT)": 1, "data.MECYL 100BN consumption (MT)": 1, "data.ROBMECYLCalculated": 1, "data.ROBMECYLLSCalculated": 1, "data.ROB MECYL 20/25 BN": 1, "data.ROB MECYL 40BN": 1, "data.ROB MECYL 70BN": 1, "data.ROB MECYL 100BN": 1 }; case 'aecc': return { ...baseProjection, "data.Steaming time (HRS)": 1, "data.AECC consumption (LTRS)": 1, "data.ROB AECC": 1 }; default: throw new Error(`Unknown data type: ${dataType}`); } } /** * Transforms MongoDB documents to flattened structure * @param documents - Array of MongoDB documents * @param dataType - The type of data to transform * @returns Array of transformed documents */ export function transformDocuments(documents, dataType) { return documents.map(doc => { const baseFields = { "Report Date": doc["Report Date"] ?? null, "Vessel Name": doc["Vessel Name"] ?? null }; switch (dataType) { case 'fuel': return { ...baseFields, "Steaming time (HRS)": doc.data?.["Steaming time (HRS)"] ?? null, "ME Total Consumption": doc.data?.["ME Total Consumption"] ?? null, "AE Total Consumption": doc.data?.["AE Total Consumption"] ?? null, "BLR Total Consumption": doc.data?.["BLR Total Consumption"] ?? null, "Total Consumption HSFO": doc.data?.["Total Consumption HSFO"] ?? null, "Total Consumption ULSFO": doc.data?.["Total Consumption ULSFO"] ?? null, "Total Consumption MDO": doc.data?.["Total Consumption MDO"] ?? null, "Total Consumption LSMGO": doc.data?.["Total Consumption LSMGO"] ?? null, "Total Consumption VLSFO": doc.data?.["Total Consumption VLSFO"] ?? null, "Total Consumption LNG": doc.data?.["Total Consumption LNG"] ?? null, "AE HSFO consumption (MT)": doc.data?.["AE HSFO consumption (MT)"] ?? null, "ME HSFO consumption (MT)": doc.data?.["ME HSFO consumption (MT)"] ?? null, "BLR HSFO consumption (MT)": doc.data?.["BLR HSFO consumption (MT)"] ?? null, "AE ULSFO consumption (MT)": doc.data?.["AE ULSFO consumption (MT)"] ?? null, "ME ULSFO consumption (MT)": doc.data?.["ME ULSFO consumption (MT)"] ?? null, "BLR ULSFO consumption (MT)": doc.data?.["BLR ULSFO consumption (MT)"] ?? null, "AE MDO consumption (MT)": doc.data?.["AE MDO consumption (MT)"] ?? null, "ME MDO consumption (MT)": doc.data?.["ME MDO consumption (MT)"] ?? null, "BLR MDO consumption (MT)": doc.data?.["BLR MDO consumption (MT)"] ?? null, "AE LS MGO consumption (MT)": doc.data?.["AE LS MGO consumption (MT)"] ?? null, "ME LS MGO consumption (MT)": doc.data?.["ME LS MGO consumption (MT)"] ?? null, "BLR LS MGO consumption (MT)": doc.data?.["BLR LS MGO consumption (MT)"] ?? null, "AE VLSFO consumption (MT)": doc.data?.["AE VLSFO consumption (MT)"] ?? null, "ME VLSFO consumption (MT)": doc.data?.["ME VLSFO consumption (MT)"] ?? null, "BLR VLSFO consumption (MT)": doc.data?.["BLR VLSFO consumption (MT)"] ?? null, "AE LNG consumption (MT)": doc.data?.["AE LNG consumption (MT)"] ?? null, "ME LNG consumption (MT)": doc.data?.["ME LNG consumption (MT)"] ?? null, "BLR LNG consumption (MT)": doc.data?.["BLR LNG consumption (MT)"] ?? null, "ROB HSFO": doc.data?.["ROB HSFO"] ?? null, "ROB ULSFO": doc.data?.["ROB ULSFO"] ?? null, "ROB MDO": doc.data?.["ROB MDO"] ?? null, "ROB LS MGO": doc.data?.["ROB LS MGO"] ?? null, "ROB VLSFO": doc.data?.["ROB VLSFO"] ?? null, "ROB LNG": doc.data?.["ROB LNG"] ?? null, "ChartererPartySpeed": doc.data?.["ChartererPartySpeed"] ?? null, "ChartererPartyConsumption": doc.data?.["ChartererPartyConsumption"] ?? null }; case 'freshwater': return { ...baseFields, "Event": doc["Event"] ?? null, "EventType": doc["EventType"] ?? null, "Steaming time (HRS)": doc.data?.["Steaming time (HRS)"] ?? null, "FW production (MT)": doc.data?.["FW production (MT)"] ?? null, "FW consumption (MT)": doc.data?.["FW consumption (MT)"] ?? null, "ROB FW": doc.data?.["ROB FW"] ?? null, "freshWaterConsumption": doc.data?.["freshWaterConsumption"] ?? null }; case 'mecc': return { ...baseFields, "Steaming time (HRS)": doc.data?.["Steaming time (HRS)"] ?? null, "MECC consumption (LTRS)": doc.data?.["MECC consumption (LTRS)"] ?? null, "ROB MECC": doc.data?.["ROB MECC"] ?? null }; case 'cylinder': return { ...baseFields, "Steaming time (HRS)": doc.data?.["Steaming time (HRS)"] ?? null, "ME cyl LS 40 BN ROB consumption (LTRS)": doc.data?.["ME cyl LS 40 BN ROB consumption (LTRS)"] ?? null, "ME cyl 70/100 BN ROB/Cap consumption (LTRS)": doc.data?.["ME cyl 70/100 BN ROB/Cap consumption (LTRS)"] ?? null, "MECYL 40BN consumption (MT)": doc.data?.["MECYL 40BN consumption (MT)"] ?? null, "MECYL 20/25 BN consumption (MT)": doc.data?.["MECYL 20/25 BN consumption (MT)"] ?? null, "MECYL 70BN consumption (MT)": doc.data?.["MECYL 70BN consumption (MT)"] ?? null, "MECYL 100BN consumption (MT)": doc.data?.["MECYL 100BN consumption (MT)"] ?? null, "ROBMECYLCalculated": doc.data?.["ROBMECYLCalculated"] ?? null, "ROBMECYLLSCalculated": doc.data?.["ROBMECYLLSCalculated"] ?? null, "ROB MECYL 20/25 BN": doc.data?.["ROB MECYL 20/25 BN"] ?? null, "ROB MECYL 40BN": doc.data?.["ROB MECYL 40BN"] ?? null, "ROB MECYL 70BN": doc.data?.["ROB MECYL 70BN"] ?? null, "ROB MECYL 100BN": doc.data?.["ROB MECYL 100BN"] ?? null }; case 'aecc': return { ...baseFields, "Steaming time (HRS)": doc.data?.["Steaming time (HRS)"] ?? null, "AECC consumption (LTRS)": doc.data?.["AECC consumption (LTRS)"] ?? null, "ROB AECC": doc.data?.["ROB AECC"] ?? null }; default: throw new Error(`Unknown data type: ${dataType}`); } }); } /** * Sorts documents by Report Date (newest first) * @param documents - Array of documents to sort * @returns Sorted array */ export function sortByReportDate(documents) { return documents.sort((a, b) => new Date(b['Report Date']).getTime() - new Date(a['Report Date']).getTime()); } /** * Builds component query patterns for component data lookup with input sanitization * @param imo - The IMO number (sanitized) * @param questionNumber - Optional question number for specific queries (sanitized) * @returns Object with different component query patterns */ export function buildComponentQueries(imo, questionNumber) { // Sanitize IMO input const sanitizedImo = SanitizationMiddleware.sanitizeIMO(imo); // Sanitize question number if provided let sanitizedQuestionNumber; if (questionNumber) { sanitizedQuestionNumber = questionNumber.replace(/[^\w.-]/g, ''); if (sanitizedQuestionNumber.length === 0) { throw new Error(`Invalid question number: ${questionNumber}`); } } const queries = { // IMO-based component lookup - using suffix matching with sanitized input imoQuery: { componentNo: { $regex: `_${sanitizedImo}$`, $options: 'i' } }, // Specific component lookup with sanitization specificQuery: (componentNo) => { const sanitizedComponentNo = componentNo.replace(/[^\w.-]/g, ''); if (sanitizedComponentNo.length === 0) { throw new Error(`Invalid component number: ${componentNo}`); } return { componentNo: sanitizedComponentNo }; }, // Similar components lookup with double sanitization similarQuery: sanitizedQuestionNumber ? { componentNo: { $regex: `_${sanitizedQuestionNumber}_${sanitizedImo}$`, $options: 'i' } } : null }; return queries; } /** * Creates ObjectId filter for MongoDB updates with validation * @param id - The string ID to convert to ObjectId * @returns MongoDB filter object */ export function createObjectIdFilter(id) { // Validate ObjectId format if (!ObjectId.isValid(id)) { throw new Error(`Invalid ObjectId format: ${id}`); } return { "_id": new ObjectId(id) }; } /** * Sanitizes a complete MongoDB query object * @param query - The query object to sanitize * @returns Sanitized query object */ export function sanitizeMongoQuery(query) { return SanitizationMiddleware.sanitizeMongoQuery(query, { allowedOperators: ['$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin', '$and', '$or', '$exists', '$regex'], maxStringLength: 1000, preventJavaScriptInjection: true }); } /** * Builds a safe search query for text fields * @param searchText - The text to search for * @param fields - Array of field names to search in * @returns Safe MongoDB query object */ export function buildSafeSearchQuery(searchText, fields) { if (!searchText || searchText.trim().length === 0) { return {}; } // Sanitize search text const sanitizedText = SanitizationMiddleware.sanitizeInput(searchText, { maxStringLength: 500, preventJavaScriptInjection: true }); // Create safe regex pattern const safePattern = SanitizationMiddleware.sanitizeRegExpPattern(sanitizedText); // Build $or query for multiple fields const searchConditions = fields.map(field => ({ [field]: { $regex: safePattern, $options: 'i' } })); return { $or: searchConditions }; } /** * Executes a complete vessel data query with standardized processing * @param collection - MongoDB collection instance * @param imo - IMO number to query * @param dataType - Type of data to retrieve * @param dateRange - Optional date range filter * @returns Processed query results */ export async function executeVesselDataQuery(collection, imo, dataType, dateRange) { const query = buildVesselDataQuery(imo, dateRange); const projection = getProjectionByType(dataType); logger.info(`Executing ${dataType} query for IMO ${imo}:`, query); const documents = await collection.find(query, { projection }).toArray(); const transformedDocuments = transformDocuments(documents, dataType); const sortedDocuments = sortByReportDate(transformedDocuments); logger.info(`Retrieved ${sortedDocuments.length} ${dataType} records for IMO ${imo}`); return sortedDocuments; } //# sourceMappingURL=query-builder.js.map