import { Injectable, inject } from '@angular/core'; import { Result, ok, err } from '@shared/kernel/fp'; import { runSubmit } from '@shared/application/submit'; import { ApiClient, StamdataColumnDto } from '@shared/infrastructure/api-client'; import { ColumnType, StamColumn, StamRow, StamTable } from '@beheer/domain/stamdata'; /** A loaded table: its schema plus the rows for editing (or the peildatum-filtered view). */ export interface LoadedTable { table: StamTable; rows: StamRow[]; } const FAILED = $localize`:@@beheer.load.failed:De stamdata kon niet worden geladen.`; const COLUMN_TYPES: readonly ColumnType[] = ['text', 'date', 'number', 'enum']; /** * The only place stamdata HTTP lives (ADR-0001 boundary). Both endpoints are reads; the * generic `parse*` narrows the untrusted wire shape (schema + opaque rows) into the domain * model. There is no write method — the edit is downloaded and lands as a PR. */ @Injectable({ providedIn: 'root' }) export class StamdataAdapter { private client = inject(ApiClient); /** The tables in the catalog (schema only, no rows) — for the table switcher. */ async list(): Promise> { const r = await runSubmit(() => this.client.stamdataTables(), FAILED); if (!r.ok) return r; const out: StamTable[] = []; for (const t of r.value ?? []) { const parsed = parseTable(t); if (!parsed.ok) return parsed; out.push(parsed.value); } return ok(out); } /** One table's schema + rows. `peildatum` (yyyy-MM-dd) asks the server for only the rows valid on that date; the editor uses it for a server-side cross-check, previewing locally for instant feedback (see `activeOn`). */ async load(tableId: string, peildatum?: string): Promise> { const r = await runSubmit(() => this.client.stamdataTable(tableId, peildatum), FAILED); return r.ok ? parseStamdataTable(r.value) : r; } } // --- parse: wire → domain, validating at the boundary --- /** Trust-boundary parse for one table response: schema + rows → domain. Exported so its spec can exercise it without HTTP (the house `parse*` seam, ADR-0001). */ export function parseStamdataTable(dto: { id?: string; label?: string; columns?: StamdataColumnDto[]; temporal?: boolean; rows?: readonly unknown[]; }): Result { const table = parseTable(dto); if (!table.ok) return table; return ok({ table: table.value, rows: parseRows(dto.rows ?? [], table.value.columns) }); } function parseColumn(dto: StamdataColumnDto): Result { if (typeof dto.name !== 'string' || dto.name === '') return err('stamdata column: bad name'); const raw = dto.type ?? ''; const type = (COLUMN_TYPES as string[]).includes(raw) ? (raw as ColumnType) : 'text'; return ok({ name: dto.name, type, isKey: dto.isKey === true, options: dto.options ?? [] }); } function parseTable(dto: { id?: string; label?: string; columns?: StamdataColumnDto[]; temporal?: boolean }): Result { if (typeof dto.id !== 'string' || !Array.isArray(dto.columns)) return err('stamdata table: bad shape'); const columns: StamColumn[] = []; for (const c of dto.columns) { const parsed = parseColumn(c); if (!parsed.ok) return parsed; columns.push(parsed.value); } if (columns.length === 0) return err('stamdata table: no columns'); return ok({ id: dto.id, label: dto.label ?? dto.id, columns, temporal: dto.temporal === true }); } /** Every cell becomes editable text: null → '' (open-ended), number/bool → its string form. */ function parseRows(rows: readonly unknown[], columns: readonly StamColumn[]): StamRow[] { return rows.map((raw) => { const row: StamRow = {}; const obj = (raw ?? {}) as Record; for (const c of columns) { const v = obj[c.name]; row[c.name] = v === null || v === undefined ? '' : String(v); } return row; }); }