99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import type {
|
|
UnifiedResultProjection,
|
|
UnifiedResultQuery,
|
|
UnifiedResultQueryResult
|
|
} from '../domain/unified-result';
|
|
import { discloseResultDbProjection, RESULT_DB_DISCLOSURE_POLICY_VERSION } from '../domain/result-db-disclosure';
|
|
|
|
export const RESULT_DB_JSONL_FORMAT = 'assessor-result-db-jsonl';
|
|
export const RESULT_DB_JSONL_SCHEMA_VERSION = '2.0.0';
|
|
export const RESULT_DB_EXPORT_PAGE_SIZE = 1000;
|
|
|
|
type ResultPageReader = (query: UnifiedResultQuery) => Promise<UnifiedResultQueryResult>;
|
|
|
|
function definedQueryEntries(query: UnifiedResultQuery): Array<[string, unknown]> {
|
|
return Object.entries(query).filter(([, value]) => value !== undefined);
|
|
}
|
|
|
|
function exportScope(query: UnifiedResultQuery): Record<string, unknown> {
|
|
return {
|
|
projectScoped: Boolean(query.projectId),
|
|
assessmentScopeCount: query.assessmentIds?.length ?? (query.assessmentId ? 1 : 0),
|
|
scanScopeCount: query.scanRunIds?.length ?? (query.scanRunId ? 1 : 0),
|
|
evaluationScoped: Boolean(query.evaluationRunId)
|
|
};
|
|
}
|
|
|
|
function exportFilters(query: UnifiedResultQuery): Record<string, unknown> {
|
|
const excluded = new Set([
|
|
'projectId', 'assessmentId', 'assessmentIds', 'scanRunId', 'scanRunIds', 'evaluationRunId',
|
|
'offset', 'limit', 'search', 'affectedAsset', 'observedFrom', 'observedTo'
|
|
]);
|
|
return Object.fromEntries(definedQueryEntries(query)
|
|
.filter(([key]) => !excluded.has(key))
|
|
.map(([key, value]) => [key, Array.isArray(value) ? { active: true, selectionCount: value.length } : value]));
|
|
}
|
|
|
|
function resultForExport(result: UnifiedResultProjection): Record<string, unknown> {
|
|
const disclosed = discloseResultDbProjection(result);
|
|
if (disclosed.sourceKind === 'environment-observation'
|
|
&& (!disclosed.supplementalDisclosurePolicyVersion || !disclosed.supplementalDisclosureClass)) {
|
|
throw new Error('An unclassified supplemental result cannot be exported.');
|
|
}
|
|
const blocked = new Set([
|
|
'resultProjectionId', 'sourceResultId', 'projectId', 'assessmentId', 'collectionRunId', 'scanRunId',
|
|
'gpoSnapshotId', 'evaluationRunId', 'importArtifactId', 'sourceViewId', 'sourceLocator',
|
|
'sourceObservedAt', 'importedAt', 'evaluatedAt', 'createdAt'
|
|
]);
|
|
return Object.fromEntries(Object.entries(disclosed)
|
|
.filter(([key, value]) => !blocked.has(key) && value !== undefined));
|
|
}
|
|
|
|
export function buildUnifiedResultJsonl(
|
|
query: UnifiedResultQuery,
|
|
results: UnifiedResultProjection[],
|
|
generatedAt: string
|
|
): string {
|
|
const lines = [{
|
|
recordType: 'manifest',
|
|
format: RESULT_DB_JSONL_FORMAT,
|
|
schemaVersion: RESULT_DB_JSONL_SCHEMA_VERSION,
|
|
disclosurePolicyVersion: RESULT_DB_DISCLOSURE_POLICY_VERSION,
|
|
generatedAt,
|
|
resultCount: results.length,
|
|
scope: exportScope(query),
|
|
filters: exportFilters(query)
|
|
}, ...results.map((result, index) => ({
|
|
recordType: 'result',
|
|
position: index + 1,
|
|
result: resultForExport(result)
|
|
}))];
|
|
return `${lines.map(line => JSON.stringify(line)).join('\n')}\n`;
|
|
}
|
|
|
|
export function unifiedResultJsonlFileName(generatedAt: string): string {
|
|
const timestamp = generatedAt.replace(/\.\d{3}Z$/, 'Z').replaceAll(':', '-');
|
|
return `assessor-result-db-filtered-${timestamp}.jsonl`;
|
|
}
|
|
|
|
export async function queryAllUnifiedResults(
|
|
readPage: ResultPageReader,
|
|
query: UnifiedResultQuery,
|
|
pageSize = RESULT_DB_EXPORT_PAGE_SIZE
|
|
): Promise<UnifiedResultProjection[]> {
|
|
const baseQuery = { ...query, offset: undefined, limit: undefined };
|
|
const results: UnifiedResultProjection[] = [];
|
|
let expectedTotal: number | undefined;
|
|
|
|
while (expectedTotal === undefined || results.length < expectedTotal) {
|
|
const page = await readPage({ ...baseQuery, offset: results.length, limit: pageSize });
|
|
if (expectedTotal === undefined) expectedTotal = page.total;
|
|
if (page.total !== expectedTotal) throw new Error('The filtered result set changed during export. Please retry.');
|
|
if (!page.items.length && results.length < expectedTotal) throw new Error('The filtered result export ended before all rows were returned.');
|
|
results.push(...page.items);
|
|
}
|
|
|
|
if (results.length !== expectedTotal) throw new Error('The filtered result export returned an inconsistent row count.');
|
|
return results;
|
|
}
|