@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
328 lines • 12.2 kB
JavaScript
/**
* 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