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