UNPKG

crew-management-mcp-server

Version:

Crew management server handling crew records, certifications, scheduling, payroll, and vessel assignments with ERP access for data extraction

337 lines 12.7 kB
import { MongoClient } from 'mongodb'; import { config } from './config.js'; import { logger } from './logger.js'; // Global state for company IMO management let companyDatabaseClient = null; let companyImoNumbers = []; /** * Initialize company database connection for IMO filtering */ export async function initializeCompanyDatabase() { if (!config.companyDbUri) { logger.warn('Company database URI not configured. IMO filtering will be limited.'); return; } try { companyDatabaseClient = new MongoClient(config.companyDbUri, { maxPoolSize: 10, minPoolSize: 2, connectTimeoutMS: 10000, socketTimeoutMS: 45000, }); await companyDatabaseClient.connect(); logger.info('Company database connection established successfully'); } catch (error) { logger.error('Failed to initialize company database connection:', error); companyDatabaseClient = null; throw error; } } /** * Close company database connection */ export async function closeCompanyDatabase() { if (companyDatabaseClient) { await companyDatabaseClient.close(); companyDatabaseClient = null; logger.info('Company database connection closed'); } } /** * Get company database instance */ function getCompanyDatabase() { if (!companyDatabaseClient) { throw new Error('Company database not initialized. Call initializeCompanyDatabase() first.'); } const dbName = config.companyDbName || 'syia-etl-dev'; return companyDatabaseClient.db(dbName); } /** * Fetch company IMO numbers from the database * @param companyName - Name of the company to fetch IMOs for * @returns Array of IMO numbers as strings */ export async function fetchCompanyImoNumbers(companyName) { if (!companyDatabaseClient) { logger.warn('Company database not available. Returning empty IMO list.'); return []; } try { const db = getCompanyDatabase(); const collection = db.collection('common_group_details'); logger.info(`Fetching IMO numbers for company: ${companyName}`); const result = await collection.findOne({ groupName: companyName }, { projection: { imo: 1, imoList: 1, groupImoList: 1, _id: 0 } }); if (!result) { logger.warn(`No IMO data found for company: ${companyName}`); return []; } // Combine IMO numbers from all three fields const imoNumbers = []; // Add single IMO number if (result.imo && typeof result.imo === 'number') { imoNumbers.push(result.imo); } // Add IMO list array if (result.imoList && Array.isArray(result.imoList)) { imoNumbers.push(...result.imoList.filter(imo => typeof imo === 'number')); } // Add group IMO list array (optional field) if (result.groupImoList && Array.isArray(result.groupImoList)) { imoNumbers.push(...result.groupImoList.filter(imo => typeof imo === 'number')); } // Convert to strings and remove duplicates const uniqueImoStrings = [...new Set(imoNumbers.map(imo => String(imo)))]; logger.info(`Successfully fetched ${uniqueImoStrings.length} unique IMO numbers for company: ${companyName}`); // Cache the IMO numbers companyImoNumbers = uniqueImoStrings; return uniqueImoStrings; } catch (error) { logger.error(`Failed to fetch IMO numbers for company ${companyName}:`, error); throw error; } } /** * Set company IMO numbers (for testing and development) * @param imos - Array of IMO numbers */ export function setCompanyImoNumbers(imos) { companyImoNumbers = imos; logger.debug(`Set company IMO numbers: ${imos.join(', ')}`); } /** * Get cached company IMO numbers * @returns Array of cached IMO numbers */ export function getCompanyImoNumbers() { return [...companyImoNumbers]; } /** * Check if an IMO number is valid for the current company * @param imoNumber - IMO number to validate (string or number) * @returns True if the IMO is authorized for the company */ export function isValidImoForCompany(imoNumber) { if (companyImoNumbers.length === 0) { // If no company IMOs are loaded, allow all access (for development) return true; } const imoNum = Number(imoNumber); const companyImosNum = companyImoNumbers.map(imo => Number(imo)); return companyImosNum.includes(imoNum); } /** * Check if IMO filtering should be bypassed for admin companies * @param companyName - Company name to check * @returns True if filtering should be bypassed */ export function shouldBypassImoFiltering(companyName) { const bypassCompanies = ['Synergy', 'Admin', 'SYIA', 'System']; return bypassCompanies.includes(companyName); } /** * Validate IMO number with detailed error messages * @param imoNumber - IMO number to validate * @param companyName - Company name for error messages * @returns Validation result with error message */ export function validateImoNumber(imoNumber, companyName) { const currentCompanyName = companyName || config.companyName || 'current company'; // Skip validation for admin companies if (shouldBypassImoFiltering(currentCompanyName)) { return { isValid: true }; } // Check if IMO is valid for company if (!isValidImoForCompany(imoNumber)) { const availableImos = getCompanyImoNumbers(); const maxDisplayImos = 10; const displayImos = availableImos.slice(0, maxDisplayImos); const moreCount = availableImos.length > maxDisplayImos ? ` and ${availableImos.length - maxDisplayImos} more` : ''; return { isValid: false, errorMessage: `IMO number ${imoNumber} is not associated with ${currentCompanyName}. Available IMO numbers: ${displayImos.join(', ')}${moreCount}` }; } return { isValid: true }; } /** * Enhanced filtering for Typesense hits to remove unauthorized IMO documents * @param hits - Array of Typesense hits to filter * @returns Filtered hits with statistics */ export function filterTypesenseHitsByCompanyImos(hits) { const startTime = Date.now(); const totalHits = hits.length; const filteredHits = []; const unauthorizedImos = []; // Skip filtering for admin companies const companyName = config.companyName || ''; if (shouldBypassImoFiltering(companyName)) { return { filteredHits: hits, filteringStats: { totalHits, filteredHits: 0, unauthorizedImos: [], processingTimeMs: Date.now() - startTime } }; } // Field names to check for IMO numbers const IMO_FIELD_NAMES = ['imo', 'vesselImo', 'imoNumber', 'IMO', 'vessel_imo', 'imo_number', 'vesselIMO', 'IMO_NUMBER']; for (const hit of hits) { const unauthorizedImo = findUnauthorizedImoInDocument(hit.document || hit); if (unauthorizedImo.hasUnauthorizedImo) { // Document contains unauthorized IMO, exclude it unauthorizedImos.push({ field: unauthorizedImo.fullPath || unauthorizedImo.imoField || 'unknown', value: unauthorizedImo.imoValue }); } else { // Document is authorized, include it filteredHits.push(hit); } } return { filteredHits, filteringStats: { totalHits, filteredHits: totalHits - filteredHits.length, unauthorizedImos, processingTimeMs: Date.now() - startTime } }; } /** * Recursively find unauthorized IMO numbers in a document */ function findUnauthorizedImoInDocument(document, path = 'root') { if (!document || typeof document !== 'object') { return { hasUnauthorizedImo: false }; } const IMO_FIELD_NAMES = ['imo', 'vesselImo', 'imoNumber', 'IMO', 'vessel_imo', 'imo_number', 'vesselIMO', 'IMO_NUMBER']; // Handle arrays - check each item recursively if (Array.isArray(document)) { for (let i = 0; i < document.length; i++) { const result = findUnauthorizedImoInDocument(document[i], `${path}[${i}]`); if (result.hasUnauthorizedImo) { return result; } } return { hasUnauthorizedImo: false }; } // Check direct IMO fields at current level for (const fieldName of IMO_FIELD_NAMES) { if (document.hasOwnProperty(fieldName) && document[fieldName] != null) { if (!isValidImoForCompany(document[fieldName])) { return { hasUnauthorizedImo: true, imoField: fieldName, imoValue: document[fieldName], fullPath: `${path}.${fieldName}` }; } } } // Recursively check nested objects for (const key in document) { if (document.hasOwnProperty(key) && typeof document[key] === 'object' && document[key] !== null) { const result = findUnauthorizedImoInDocument(document[key], `${path}.${key}`); if (result.hasUnauthorizedImo) { return result; } } } return { hasUnauthorizedImo: false }; } export function filterQueryResultsByCompanyImos(columns, rows) { const startTime = Date.now(); const totalRows = rows.length; const filteredRows = []; const unauthorizedImos = []; // Skip filtering for admin companies const companyName = config.companyName || ''; if (shouldBypassImoFiltering(companyName)) { return { filteredRows: rows, filteringStats: { totalRows, filteredRows: 0, unauthorizedImos: [], processingTimeMs: Date.now() - startTime } }; } // Field names to check for IMO numbers const IMO_FIELD_NAMES = ['imo', 'vesselImo', 'imoNumber', 'IMO', 'vessel_imo', 'imo_number', 'vesselIMO', 'IMO_NUMBER']; // Find IMO column indices const imoColumnIndices = []; columns.forEach((column, index) => { if (IMO_FIELD_NAMES.some(imoField => column.toLowerCase().includes(imoField.toLowerCase()))) { imoColumnIndices.push(index); } }); // If no IMO columns found, return all rows if (imoColumnIndices.length === 0) { logger.debug('No IMO columns found in query results, skipping IMO filtering'); return { filteredRows: rows, filteringStats: { totalRows, filteredRows: 0, unauthorizedImos: [], processingTimeMs: Date.now() - startTime } }; } // Filter rows based on IMO values for (const row of rows) { let isAuthorized = true; for (const imoIndex of imoColumnIndices) { let imoValue; if (Array.isArray(row)) { imoValue = row[imoIndex]; } else if (typeof row === 'object' && row !== null) { imoValue = row[columns[imoIndex]]; } if (imoValue !== null && imoValue !== undefined) { const imoString = String(imoValue); if (imoString && !isValidImoForCompany(imoString)) { isAuthorized = false; // Track unauthorized IMO const unauthorizedEntry = { field: columns[imoIndex], value: imoValue }; if (!unauthorizedImos.some(entry => entry.field === unauthorizedEntry.field && entry.value === unauthorizedEntry.value)) { unauthorizedImos.push(unauthorizedEntry); } break; // If any IMO is unauthorized, exclude the entire row } } } if (isAuthorized) { filteredRows.push(row); } } const processingTimeMs = Date.now() - startTime; const filteredCount = totalRows - filteredRows.length; const filteringStats = { totalRows, filteredRows: filteredCount, unauthorizedImos, processingTimeMs }; logger.debug(`Query result IMO filtering completed`, { totalRows, authorizedRows: filteredRows.length, filteredOutRows: filteredCount, unauthorizedImosCount: unauthorizedImos.length, processingTimeMs, imoColumnsFound: imoColumnIndices.length }); return { filteredRows, filteringStats }; } //# sourceMappingURL=imoUtils.js.map