import { utils, write } from 'xlsx'; import type { UnifiedResultProjection, UnifiedResultQuery } from '../domain/unified-result'; import { buildUnifiedResultJsonl } from './unified-result-jsonl'; export const RESULT_DB_EXCEL_MEDIA_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; const CELL_LIMIT = 32767; const SHEET_ROWS = 1048576; export function unifiedResultExcelFileName(generatedAt: string): string { return `assessor-result-db-filtered-${generatedAt.slice(0, 10)}.xlsx`; } /** Consume the exact JSONL disclosure boundary; never export raw source projections. */ export function buildUnifiedResultExcel(query: UnifiedResultQuery, results: UnifiedResultProjection[], generatedAt: string): ArrayBuffer { const [manifest, ...entries] = buildUnifiedResultJsonl(query, results, generatedAt).trimEnd().split('\n') .map(line => JSON.parse(line) as Record); const records = entries.map(entry => entry.result as Record); const preferred = ['title', 'sourceRuleId', 'sourceTool', 'severityNormalized', 'outcomeNormalized', 'resultType', 'isIoe', 'isDeviation', 'description', 'recommendation']; const fields = [...new Set(records.flatMap(record => Object.keys(record)))]; fields.sort((a, b) => { const rank = (field: string) => preferred.includes(field) ? preferred.indexOf(field) : preferred.length; return rank(a) - rank(b) || a.localeCompare(b); }); const overflow: (string | number | boolean)[][] = [['Position', 'Field', 'Part', 'Text']]; const cell = (value: unknown, position: number, field: string): string | number | boolean => { if (value === undefined || value === null) return ''; if (typeof value === 'number' || typeof value === 'boolean') return value; const text = typeof value === 'string' ? value : JSON.stringify(value); if (text.length <= CELL_LIMIT) return text; let offset = 0, part = 1; while (offset < text.length) { let end = Math.min(offset + CELL_LIMIT, text.length); // Do not split a UTF-16 surrogate pair across Excel cells. if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1])) end--; overflow.push([position, field, part++, text.slice(offset, end)]); offset = end; } return `Complete value in Long text: Position ${position}, Field ${field}`; }; const rows = [['Position', ...fields], ...records.map((record, index) => [index + 1, ...fields.map(field => cell(record[field], index + 1, field))])]; const workbook = utils.book_new(); const append = (name: string, data: (string | number | boolean)[][], widths: number[]) => { if (data.length > SHEET_ROWS) throw new Error('The Excel worksheet row limit was exceeded. Narrow the filters or use JSONL.'); const sheet = utils.aoa_to_sheet(data); sheet['!cols'] = widths.map(wch => ({ wch })); sheet['!autofilter'] = { ref: sheet['!ref']! }; utils.book_append_sheet(workbook, sheet, name); }; append('Results', rows, [12, ...fields.map(field => ['title', 'description', 'recommendation'].includes(field) ? 65 : 25)]); append('Export info', [['Property', 'Value'], ...Object.entries(manifest).map(([key, value]) => [key, cell(value, 0, key)]), ['Excel notes', 'Nested values are JSON text. Long values are complete in Long text, ordered by Position, Field and Part. Position 0 refers to Export info.']], [28, 100]); if (overflow.length > 1) append('Long text', overflow, [12, 30, 10, 100]); // aoa_to_sheet creates literal string cells, including values beginning with =, +, - or @. return write(workbook, { type: 'array', bookType: 'xlsx', compression: true }) as ArrayBuffer; }