UNPKG

crew-management-mcp-server

Version:

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

182 lines 7.75 kB
import { logger } from './logger.js'; export function convertToCsv(columns, rows) { logger.debug('Converting query results to CSV format', { columnCount: columns.length, rowCount: rows.length }); if (!columns || columns.length === 0) { return ''; } // Helper function to escape CSV values function escapeCsvValue(value) { if (value === null || value === undefined) { return ''; } let strValue = String(value); // Handle dates if (value instanceof Date) { strValue = value.toISOString(); } // Handle boolean values if (typeof value === 'boolean') { strValue = value ? 'true' : 'false'; } // Handle numbers if (typeof value === 'number') { strValue = value.toString(); } // Escape special CSV characters if (strValue.includes('"') || strValue.includes(',') || strValue.includes('\n') || strValue.includes('\r')) { // Escape double quotes by doubling them strValue = strValue.replace(/"/g, '""'); // Wrap in quotes strValue = `"${strValue}"`; } return strValue; } // Build CSV header const csvLines = []; const headerLine = columns.map(col => escapeCsvValue(col)).join(','); csvLines.push(headerLine); // Build CSV data rows for (const row of rows) { if (Array.isArray(row)) { // Row is array of values (Snowflake array format) const csvRow = row.map(value => escapeCsvValue(value)).join(','); csvLines.push(csvRow); } else if (typeof row === 'object' && row !== null) { // Row is object with column names as keys (Snowflake object format) const csvRow = columns.map(col => escapeCsvValue(row[col])).join(','); csvLines.push(csvRow); } else { logger.warn('Unexpected row format in CSV conversion', { row, rowType: typeof row }); // Handle single value rows (unlikely but defensive) csvLines.push(escapeCsvValue(row)); } } const csvContent = csvLines.join('\n'); logger.info('CSV conversion completed', { totalLines: csvLines.length, headerColumns: columns.length, dataRows: csvLines.length - 1, outputSize: csvContent.length }); return csvContent; } export function validateSqlQuery(query) { if (!query || typeof query !== 'string') { return { isValid: false, error: 'Query is required and must be a string' }; } const trimmedQuery = query.trim().toLowerCase(); // Check if query is empty if (!trimmedQuery) { return { isValid: false, error: 'Query cannot be empty' }; } // Only allow SELECT statements if (!trimmedQuery.startsWith('select') && !trimmedQuery.startsWith('with')) { return { isValid: false, error: 'Only SELECT and WITH (CTE) statements are allowed. Found: ' + trimmedQuery.split(' ')[0] }; } // Block dangerous operations const dangerousOperations = [ 'insert', 'update', 'delete', 'drop', 'create', 'alter', 'truncate', 'merge', 'grant', 'revoke', 'exec', 'execute', 'sp_', 'xp_', 'declare', 'set', 'call' ]; for (const operation of dangerousOperations) { if (trimmedQuery.includes(operation)) { return { isValid: false, error: `Dangerous operation '${operation}' is not allowed in queries` }; } } // Check for basic SQL injection patterns const injectionPatterns = [ /;\s*(drop|delete|insert|update|create|alter|truncate)/i, /union.*select/i, /\/\*.*\*\//, /--.*$/m, /xp_cmdshell/i, /sp_executesql/i ]; for (const pattern of injectionPatterns) { if (pattern.test(query)) { return { isValid: false, error: 'Query contains potentially dangerous patterns and was blocked for security' }; } } // Validate table references - should primarily use revised_base_view if (!query.toLowerCase().includes('revised_base_view')) { logger.warn('Query does not reference revised_base_view table', { query: query.substring(0, 100) }); // Don't block, but log warning as user might be doing meta-queries } return { isValid: true }; } export function applySqlLimit(query, limitResults) { // If query already has LIMIT clause, respect it but cap at max const lowerQuery = query.toLowerCase(); const hasLimit = lowerQuery.includes(' limit '); if (hasLimit) { logger.debug('Query already contains LIMIT clause'); return query; } // Add LIMIT clause to the query const trimmedQuery = query.trim(); const limitedQuery = `${trimmedQuery}${trimmedQuery.endsWith(';') ? '' : ''} LIMIT ${limitResults}`; logger.debug('Applied LIMIT to query', { originalLength: query.length, limitResults }); return limitedQuery; } export function applyDefaultSorting(query, defaultSortField = 'MONTHS', defaultSortOrder = 'DESC') { const trimmedQuery = query.trim(); const lowerQuery = trimmedQuery.toLowerCase(); // Check if querying revised_base_view table (which always has MONTHS field) if (!lowerQuery.includes('revised_base_view')) { logger.debug(`Query does not use revised_base_view table, skipping default MONTHS sorting`); return trimmedQuery; } // Check if query already has ORDER BY clause const hasOrderBy = lowerQuery.includes(' order by '); if (hasOrderBy) { // Extract the existing ORDER BY clause const orderByMatch = lowerQuery.match(/order\s+by\s+(.+?)(?:\s+limit|\s*$)/i); if (orderByMatch) { const existingOrderBy = orderByMatch[1].trim(); const firstSortField = existingOrderBy.split(',')[0].trim().split(/\s+/)[0]; // If MONTHS is already the primary sort field, keep as is if (firstSortField.toLowerCase() === 'months') { logger.debug('Query already has MONTHS as primary sort field'); return trimmedQuery; } else { // Modify existing ORDER BY to put MONTHS first, then existing fields const queryWithoutOrderBy = trimmedQuery.substring(0, trimmedQuery.toLowerCase().indexOf('order by')).trim(); const queryWithoutSemicolon = queryWithoutOrderBy.endsWith(';') ? queryWithoutOrderBy.slice(0, -1) : queryWithoutOrderBy; const modifiedQuery = `${queryWithoutSemicolon} ORDER BY ${defaultSortField} ${defaultSortOrder}, ${existingOrderBy}`; logger.info('Modified existing ORDER BY to prioritize MONTHS field', { originalOrderBy: existingOrderBy, newOrderBy: `${defaultSortField} ${defaultSortOrder}, ${existingOrderBy}` }); return modifiedQuery; } } logger.debug('Query contains ORDER BY but could not parse, skipping default sorting'); return trimmedQuery; } // No ORDER BY clause exists, add MONTHS as primary sort const queryWithoutSemicolon = trimmedQuery.endsWith(';') ? trimmedQuery.slice(0, -1) : trimmedQuery; const sortedQuery = `${queryWithoutSemicolon} ORDER BY ${defaultSortField} ${defaultSortOrder}`; logger.info('Applied default MONTHS sorting to query', { defaultSortField, defaultSortOrder, originalLength: query.length }); return sortedQuery; } //# sourceMappingURL=csvUtils.js.map