Assessor_2.0/src/importers/source-extractors.ts

101 lines
3.8 KiB
TypeScript

import type { JsonValue } from '../domain/canonical-model';
import type { DecodedObjectView, DecodedSourceDocument } from '../domain/import-intermediate';
import type { FindingCandidate, SourceExtractor, SourceFieldCandidate } from '../domain/source-extraction';
import { stringifyCell } from '../shared/text';
import { cisCatSourceExtractor } from './cis-cat-source-extractor';
import { pingCastleSourceExtractor } from './pingcastle-source-extractor';
import { maesterHtmlSourceExtractor } from './maester-html-source-extractor';
import { purpleKnightHtmlSourceExtractor } from './purple-knight-html-source-extractor';
function safeId(value: string): string {
return value.replace(/[^a-zA-Z0-9_-]/g, '-');
}
function objectFields(
fileName: string,
value: JsonValue,
objectPath: string
): SourceFieldCandidate[] {
const entries = value && typeof value === 'object' && !Array.isArray(value)
? Object.entries(value)
: [['value', value] as const];
return entries.map(([key, item]) => ({
key,
label: key,
originalValue: item,
decodedValue: stringifyCell(item),
source: { fileName, objectPath: `${objectPath}.${key}` }
}));
}
function objectCandidates(document: DecodedSourceDocument, view: DecodedObjectView): FindingCandidate[] {
const values = Array.isArray(view.value) ? view.value : [view.value];
return values.map((value, index) => {
const objectPath = Array.isArray(view.value) ? `$[${index}]` : '$';
return {
version: 1,
id: `candidate-${safeId(view.id)}-${index}`,
source: { fileName: document.file.name, objectPath },
fields: objectFields(document.file.name, value, objectPath)
};
});
}
export const nativeObjectSourceExtractor: SourceExtractor = {
id: 'native-object-source-extractor',
version: '1.0.0',
detect(document) {
const count = document.views.filter(view => view.kind === 'object').length;
return count
? { score: 1, evidence: [`${count} native Objekt- oder Arrayansicht(en) vorhanden.`] }
: { score: 0, evidence: [] };
},
extract(document) {
const candidates = document.views
.filter((view): view is DecodedObjectView => view.kind === 'object')
.flatMap(view => objectCandidates(document, view));
return { version: 1, candidates, diagnostics: [] };
}
};
export const genericTableSourceExtractor: SourceExtractor = {
id: 'generic-table-source-extractor',
version: '1.0.0',
fallback: true,
detect(document) {
const count = document.tables.length;
return count
? { score: 0.1, evidence: [`${count} kompatible Tabellenansicht(en) vorhanden.`] }
: { score: 0, evidence: [] };
},
extract(document) {
const candidates = document.tables.flatMap(table => {
const header = table.headerCandidates[0];
return table.rows
.filter(row => row.index !== header?.rowIndex && row.cells.some(cell => cell.decodedValue.trim()))
.map(row => ({
version: 1 as const,
id: `candidate-${safeId(table.id)}-${row.index}`,
source: { fileName: document.file.name, tableIndex: table.index, rowIndex: row.index },
fields: row.cells.map(cell => ({
key: `column-${cell.source.columnIndex ?? 0}`,
label: header?.headers[cell.source.columnIndex ?? 0] || `Spalte ${(cell.source.columnIndex ?? 0) + 1}`,
originalValue: cell.rawValue,
decodedValue: cell.decodedValue,
source: { ...cell.source, fileName: document.file.name }
}))
}));
});
return { version: 1, candidates, diagnostics: [] };
}
};
export const sourceExtractors: SourceExtractor[] = [
pingCastleSourceExtractor,
cisCatSourceExtractor,
purpleKnightHtmlSourceExtractor,
maesterHtmlSourceExtractor,
nativeObjectSourceExtractor,
genericTableSourceExtractor
];