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