PATCH endpoint v UI mlčky zahazuje změny — snake_case vs camelCase mismatch
import { Aside } from ‘@astrojs/starlight/components’;
Příznak
Sekce “Příznak”- Uživatel mění hodnotu v UI tabulce (např. “next number” číselné řady)
- Klikne Uložit → 200 OK, žádná chyba, UI dokonce uloží edit state
- Při dalším refresh hodnota je původní
- Žádná entry v auditu, žádný error v logu
Příčina
Sekce “Příčina”API endpoint mapuje whitelist polí pouze v camelCase (např. nextNumber: "next_number"). UI posílá přímo DB-style snake_case (next_number). For-loop neprojde keys, sets je prázdný, endpoint vrátí {updated: 0} jako úspěch:
// API endpointconst map: Record<string, string> = { nextNumber: "next_number", // ← očekává camelCase currentYear: "current_year",};for (const [k, col] of Object.entries(map)) { if (body[k] !== undefined) { // ← snake_case key NEZASÁHNE sets.push(`${col} = $${pi}`); values.push(body[k]); pi++; }}if (sets.length === 0) { return NextResponse.json({ data: { updated: 0 } }); // ← tichý úspěch}// UI komponenta — Series typing odpovídá DB rows (snake_case)const [edits, setEdits] = useState<Record<number, Partial<Series>>>({});// ...<input onChange={e => setEdit(id, { next_number: parseInt(e.target.value) })} />// ...await fetch(`/api/invoice-series/${id}`, { method: "PATCH", body: JSON.stringify(patch), // ← posílá { next_number: 5000 }, NE { nextNumber: 5000 }});Oprava
Sekce “Oprava”Vrstva převodu v UI před fetch — explicitní mapping podle endpoint kontraktu:
const apiPatch: Record<string, unknown> = {};if (patch.name !== undefined) apiPatch.name = patch.name;if (patch.next_number !== undefined) apiPatch.nextNumber = patch.next_number;if (patch.current_year !== undefined) apiPatch.currentYear = patch.current_year;if (patch.is_active !== undefined) apiPatch.isActive = patch.is_active;await fetch(url, { method: "PATCH", body: JSON.stringify(apiPatch) });Lepší trvalá oprava: endpoint by měl vracet 400 při neznámém klíči, ne mlčet:
const knownKeys = new Set(["nextNumber", "currentYear", "isActive", "name", "prefix"]);for (const k of Object.keys(body)) { if (!knownKeys.has(k)) { return NextResponse.json({ error: `Unknown field: ${k}` }, { status: 400 }); }}Jak na to přijít
Sekce “Jak na to přijít”E2E test, který:
- Načte aktuální hodnotu →
before - PATCH novou hodnotu (např.
before + 1000) - Otestuje viditelný side-effect (např. další záznam má číslo z nového setpointu), nikoli jen 200 OK
Pokud test ověřuje jen response code, tenhle bug uteče. Mocney await readNextNumberForSeries(code) po PATCH a porovnej.
Detection
Sekce “Detection”- Žádná chyba v logu nebo Sentry
{updated: 0}ve výstupu PATCH endpointu, kdyby někdo monitoroval- Audit log mutací prázdný kdyby existoval