/** * Convert a value to a CSV cell-friendly format. * Primitives pass through; objects/arrays are stringified; dates are formatted. */ function toCsvValue(val) { if (val === null || val === undefined) return ''; if (val instanceof Date) return val.toISOString(); if (typeof val === 'number' || typeof val === 'boolean') return String(val); if (typeof val === 'string') return val; if (typeof val === 'object') { if (Array.isArray(val)) return val.map(toCsvValue).join(', '); return JSON.stringify(val); } return String(val); } /** * Escape a CSV field per RFC 4180: wrap in double quotes, escape internal quotes by doubling. */ function escapeCsvField(str) { if (str == null) return '""'; const s = String(str); if (s.includes('"') || s.includes(',') || s.includes('\n') || s.includes('\r')) { return '"' + s.replace(/"/g, '""') + '"'; } return s; } /** * Generate a CSV buffer from tabular data. * @param {Array} data - Array of row objects (keys = column headers) * @param {Object} options - Options * @param {string[]} [options.columnOrder] - Optional column order (uses Object.keys of first row if not provided) * @returns {Buffer} CSV file as buffer */ export function generateCsvTable(data, options = {}) { const { columnOrder } = options; if (!data || data.length === 0) { return Buffer.from('', 'utf8'); } const keys = columnOrder || Object.keys(data[0]).filter((k) => !k.startsWith('@')); const headerRow = keys.map((k) => escapeCsvField(k)).join(','); const lines = [headerRow]; for (const row of data) { const values = keys.map((key) => escapeCsvField(toCsvValue(row[key]))); lines.push(values.join(',')); } const csv = lines.join('\n'); return Buffer.from(csv, 'utf8'); }