UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

328 lines 12.2 kB
/** * ExportManager - Handles data export functionality for large datasets * Supports CSV, JSON, and YAML formats with configurable options */ import { writeFile, mkdir } from 'fs/promises'; import { join, dirname } from 'path'; import * as yaml from 'js-yaml'; import { getLogger } from '../logging/Logger.js'; /** * Main ExportManager class for handling data exports */ export class ExportManager { defaultOutputDir; constructor(outputDir = './exports') { this.defaultOutputDir = outputDir; } /** * Export data to the specified format */ async export(data, options, metadata) { const startTime = Date.now(); try { // Validate inputs if (!Array.isArray(data)) { throw new Error('Data must be an array'); } if (!['csv', 'json', 'yaml'].includes(options.format)) { throw new Error('Format must be csv, json, or yaml'); } // Process data based on field options const processedData = this.processData(data, options); // Generate filename if not provided const filename = this.generateFilename(options); const outputDir = options.output_directory || this.defaultOutputDir; const filePath = join(outputDir, filename); // Ensure output directory exists await mkdir(dirname(filePath), { recursive: true }); // Create export metadata const exportMetadata = { exported_at: new Date().toISOString(), total_records: processedData.length, format: options.format, source_query: metadata?.source_query, pagination_info: metadata?.pagination_info, export_options: options }; // Export based on format let fileContent; switch (options.format) { case 'csv': fileContent = this.generateCSV(processedData, exportMetadata, options); break; case 'json': fileContent = this.generateJSON(processedData, exportMetadata, options); break; case 'yaml': fileContent = this.generateYAML(processedData, exportMetadata, options); break; default: throw new Error(`Unsupported format: ${options.format}`); } // Write file await writeFile(filePath, fileContent, 'utf8'); const exportTime = Date.now() - startTime; const fileSizeBytes = Buffer.byteLength(fileContent, 'utf8'); return { success: true, file_path: filePath, total_records: processedData.length, format: options.format, file_size_bytes: fileSizeBytes, export_time_ms: exportTime }; } catch (error) { return { success: false, total_records: data.length, format: options.format, export_time_ms: Date.now() - startTime, error: error.message }; } } /** * Process data according to field selection options */ processData(data, options) { return data.map(item => { let processed = { ...item }; // Handle custom fields (only include specified fields) if (options.custom_fields && options.custom_fields.length > 0) { const filtered = {}; options.custom_fields.forEach(field => { if (processed.hasOwnProperty(field)) { filtered[field] = processed[field]; } }); processed = filtered; } // Handle exclude fields if (options.exclude_fields && options.exclude_fields.length > 0) { options.exclude_fields.forEach(field => { delete processed[field]; }); } return processed; }); } /** * Generate filename based on options and current timestamp */ generateFilename(options) { if (options.filename) { // Sanitize filename by replacing invalid characters with underscores const sanitized = options.filename.replace(/[^a-zA-Z0-9_\-\.]/g, '_'); // Ensure filename has correct extension const extension = `.${options.format}`; if (!sanitized.endsWith(extension)) { return `${sanitized}${extension}`; } return sanitized; } // Generate default filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); return `optimizely_export_${timestamp}.${options.format}`; } /** * Generate CSV content */ generateCSV(data, metadata, options) { if (data.length === 0) { return options.include_metadata ? this.generateMetadataComment(metadata) : ''; } const lines = []; // Add metadata as comments if requested if (options.include_metadata) { lines.push(this.generateMetadataComment(metadata)); } // Get all unique headers from the data const headers = this.getAllHeaders(data); lines.push(headers.map(h => this.escapeCsvValue(h)).join(',')); // Add data rows data.forEach(item => { const row = headers.map(header => { const value = this.getNestedValue(item, header); return this.escapeCsvValue(value); }); lines.push(row.join(',')); }); return lines.join('\n'); } /** * Generate JSON content */ generateJSON(data, metadata, options) { const output = { data: data }; if (options.include_metadata) { output.metadata = metadata; } return options.pretty_print ? JSON.stringify(output, null, 2) : JSON.stringify(output); } /** * Generate YAML content */ generateYAML(data, metadata, options) { const output = { data: data }; if (options.include_metadata) { output.metadata = metadata; } return yaml.dump(output, { indent: 2, lineWidth: 120, noRefs: true }); } /** * Get all unique headers from array of objects */ getAllHeaders(data) { const headers = new Set(); data.forEach(item => { this.extractKeys(item).forEach(key => headers.add(key)); }); return Array.from(headers).sort(); } /** * Recursively extract all keys from an object, using dot notation for nested objects */ extractKeys(obj, prefix = '') { const keys = []; if (obj && typeof obj === 'object' && !Array.isArray(obj)) { Object.keys(obj).forEach(key => { const fullKey = prefix ? `${prefix}.${key}` : key; const value = obj[key]; if (value && typeof value === 'object' && !Array.isArray(value)) { // Recursively extract nested keys keys.push(...this.extractKeys(value, fullKey)); } else { // Add the key for primitive values and arrays keys.push(fullKey); } }); } else { // For primitive values, just add the prefix if (prefix) { keys.push(prefix); } } return keys; } /** * Get nested value using dot notation */ getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : ''; }, obj); } /** * Escape CSV values */ escapeCsvValue(value) { if (value === null || value === undefined) { return ''; } let stringValue = String(value); // Handle arrays and objects by serializing them if (typeof value === 'object') { stringValue = JSON.stringify(value); } // Escape quotes and wrap in quotes if necessary if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) { stringValue = '"' + stringValue.replace(/"/g, '""') + '"'; } return stringValue; } /** * Generate metadata comment for CSV files */ generateMetadataComment(metadata) { const lines = [ '# Optimizely Export Metadata', `# Exported at: ${metadata.exported_at}`, `# Total records: ${metadata.total_records}`, `# Format: ${metadata.format}` ]; if (metadata.source_query) { lines.push(`# Source query: ${metadata.source_query}`); } if (metadata.pagination_info) { lines.push(`# Pagination: Page ${metadata.pagination_info.current_page} of ${metadata.pagination_info.total_pages}`); } lines.push(''); // Empty line before data return lines.join('\n'); } /** * Validate export options */ static validateOptions(options) { const errors = []; if (!['csv', 'json', 'yaml'].includes(options.format)) { errors.push('Format must be one of: csv, json, yaml'); } if (options.custom_fields && options.exclude_fields) { errors.push('Cannot specify both custom_fields and exclude_fields'); } // Sanitize filename instead of rejecting it if (options.filename) { // Auto-correct filename by replacing invalid characters with underscores const sanitized = options.filename.replace(/[^a-zA-Z0-9_\-\.]/g, '_'); if (sanitized !== options.filename) { // Log that we sanitized the filename but don't error getLogger().info({ original: options.filename, sanitized: sanitized }, 'ExportManager: Sanitized filename'); } } return { valid: errors.length === 0, errors }; } /** * Get export format suggestions based on data characteristics */ static getFormatSuggestions(data, recordCount) { const hasNestedObjects = data.some(item => Object.values(item).some(value => value && typeof value === 'object' && !Array.isArray(value))); const hasArrays = data.some(item => Object.values(item).some(value => Array.isArray(value))); if (recordCount > 10000) { return { recommended: 'csv', reasons: ['Large dataset - CSV is most efficient for storage and processing'], alternatives: [ { format: 'json', reason: 'Better for programmatic access but larger file size' } ] }; } if (hasNestedObjects || hasArrays) { return { recommended: 'json', reasons: ['Data contains nested objects/arrays - JSON preserves structure'], alternatives: [ { format: 'yaml', reason: 'More human-readable but larger file size' }, { format: 'csv', reason: 'Flattened structure - some data will be serialized' } ] }; } return { recommended: 'csv', reasons: ['Simple tabular data - CSV is most compatible'], alternatives: [ { format: 'json', reason: 'Better for API integration' }, { format: 'yaml', reason: 'Most human-readable' } ] }; } } //# sourceMappingURL=ExportManager.js.map