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