Files
linumiq_net-web_app/lib/admin/csv.ts
T

46 lines
1.6 KiB
TypeScript

/**
* Minimal, dependency-free CSV serialization for the admin export endpoints.
* RFC-4180 style: fields containing a comma, double-quote, CR or LF are wrapped
* in double-quotes with embedded quotes doubled. Everything is coerced to a
* string first; null/undefined become empty fields.
*/
export function csvField(v: unknown): string {
let s: string;
if (v === null || v === undefined) s = '';
else if (typeof v === 'string') s = v;
else if (typeof v === 'object') s = JSON.stringify(v);
else s = String(v);
// Spreadsheet formula-injection guard: a field whose first character is one
// of = + - @ (or a leading tab/CR) is interpreted as a formula by Excel /
// Sheets / LibreOffice. Neutralize it by prefixing a single quote BEFORE the
// RFC-4180 quote-escaping below, so the value renders as literal text.
if (s.length > 0 && /^[=+\-@\t\r]/.test(s)) {
s = `'${s}`;
}
if (/[",\r\n]/.test(s)) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
}
export function csvRow(values: unknown[]): string {
return values.map(csvField).join(',');
}
/**
* Build a full CSV document (header + rows). A leading row is always the
* provided header. Lines are CRLF-terminated for maximal spreadsheet
* compatibility.
*/
export function toCsv(header: string[], rows: unknown[][]): string {
const lines = [csvRow(header), ...rows.map(csvRow)];
return lines.join('\r\n') + '\r\n';
}
/**
* Max rows any single export will emit. Keeps a runaway export bounded; the
* caller notes the cap in the report when hit.
*/
export const EXPORT_MAX_ROWS = 10000;