46 lines
1.6 KiB
TypeScript
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;
|