Step 3 (production-readiness): PII storage, validated reads, seams
Implement-now:
- G1: keep PII out of persistent storage — never persist BSN (only `naam`);
move both wizard drafts (address/email, work data) localStorage → sessionStorage
so they clear on tab close.
- G2: validate storage reads before trusting the cast — shape/tag guard in every
restore() (mirrors the parse* HTTP boundary); corrupt/foreign shape → start fresh.
- G3: already satisfied (debug-state redacts via mask.ts).
Show-the-seam (hook + doc, not fully built):
- G4: problemFieldErrors() maps a server validation envelope (ASP.NET
ValidationProblemDetails `errors`) to the field-keyed map the wizards already
render; returns {} until the backend sends it. +spec.
- G5: documented the retry/backoff seam at the adapter GET loader; reads may
retry, mutating submits never do.
Out of scope (named): unsaved-changes warning (persistence prevents data loss),
real auth/tokens, axe-core in CI.
Gate green: lint, check:tokens, build, test 79/79.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { problemDetail } from './api-error';
|
||||
import { problemDetail, problemFieldErrors } from './api-error';
|
||||
|
||||
describe('problemDetail', () => {
|
||||
it('extracts the detail from an RFC-7807 ProblemDetails', () => {
|
||||
@@ -12,3 +12,16 @@ describe('problemDetail', () => {
|
||||
expect(problemDetail(undefined, 'fallback')).toBe('fallback');
|
||||
});
|
||||
});
|
||||
|
||||
describe('problemFieldErrors (G4 seam)', () => {
|
||||
it('maps a ValidationProblemDetails errors dict to first-message-per-field', () => {
|
||||
expect(problemFieldErrors({ errors: { straat: ['Verplicht.'], postcode: ['Ongeldig.', 'x'] } }))
|
||||
.toEqual({ straat: 'Verplicht.', postcode: 'Ongeldig.' });
|
||||
});
|
||||
|
||||
it('returns {} when there is no errors envelope (the current backend shape)', () => {
|
||||
expect(problemFieldErrors({ detail: 'one banner' })).toEqual({});
|
||||
expect(problemFieldErrors(new Error('boom'))).toEqual({});
|
||||
expect(problemFieldErrors(undefined)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,3 +12,25 @@ export function problemDetail(e: unknown, fallback: string): string {
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* SEAM (G4): map a server validation envelope to field-level errors.
|
||||
*
|
||||
* ASP.NET's ValidationProblemDetails carries `errors: { field: string[] }`. The
|
||||
* backend today returns only `detail` (one banner message), so this returns `{}`.
|
||||
* When the backend starts sending `errors`, a machine's `SubmitFailed` handler can
|
||||
* merge this into its own `errors` map — the field-keyed shape the wizards already
|
||||
* render — so a rejection shows inline per field, not just as a banner. The
|
||||
* consumer hook is the only thing left to wire; the contract boundary lives here.
|
||||
*/
|
||||
export function problemFieldErrors(e: unknown): Record<string, string> {
|
||||
if (!e || typeof e !== 'object' || !('errors' in e)) return {};
|
||||
const errors = (e as { errors?: unknown }).errors;
|
||||
if (!errors || typeof errors !== 'object') return {};
|
||||
const out: Record<string, string> = {};
|
||||
for (const [field, msgs] of Object.entries(errors as Record<string, unknown>)) {
|
||||
const first = Array.isArray(msgs) ? msgs[0] : msgs;
|
||||
if (typeof first === 'string') out[field] = first;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user