25 lines
1.9 KiB
TypeScript
25 lines
1.9 KiB
TypeScript
import type { UnifiedResultProjection, UnifiedResultQuery } from '../domain/unified-result';
|
|
import { buildUnifiedResultJsonl } from './unified-result-jsonl';
|
|
import { rowsToSpreadsheetSafeCsv } from './csv-export';
|
|
|
|
export function unifiedResultCsvFileName(generatedAt: string): string {
|
|
return `assessor-result-db-filtered-${generatedAt.slice(0, 10)}.csv`;
|
|
}
|
|
|
|
/** Use the same disclosed projection as JSONL; nested values remain complete JSON text. */
|
|
export function buildUnifiedResultCsv(query: UnifiedResultQuery, results: UnifiedResultProjection[], generatedAt: string): string {
|
|
const [manifest, ...entries] = buildUnifiedResultJsonl(query, results, generatedAt).trimEnd().split('\n')
|
|
.map(line => JSON.parse(line) as Record<string, unknown>);
|
|
const records = entries.map(entry => entry.result as Record<string, unknown>);
|
|
const preferred = ['title', 'sourceRuleId', 'sourceTool', 'severityNormalized', 'outcomeNormalized', 'resultType', 'isIoe', 'isDeviation', 'description', 'recommendation'];
|
|
const fields = [...new Set(records.flatMap(record => Object.keys(record)))];
|
|
const rank = (field: string) => preferred.includes(field) ? preferred.indexOf(field) : preferred.length;
|
|
fields.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
const handling = manifest.handling as { classification: string; notice: string };
|
|
const metadata = [manifest.generatedOn, manifest.disclosurePolicyVersion, handling.classification, handling.notice];
|
|
const headers = ['Position', ...fields, 'exportGeneratedOn', 'exportDisclosurePolicyVersion', 'exportClassification', 'exportNotice'];
|
|
const value = (cell: unknown) => cell !== null && typeof cell === 'object' ? JSON.stringify(cell) : cell;
|
|
const rows = [headers, ...records.map((record, index) => [index + 1, ...fields.map(field => value(record[field])), ...metadata])];
|
|
return '\uFEFF' + rowsToSpreadsheetSafeCsv(rows, ';') + '\n';
|
|
}
|