fix(api): return 409 on duplicate-timestamp measurement edit

The PUT /api/measurements/[id] handler dispatched the Prisma
update without a try/catch, so re-pointing measuredAt onto an
existing (userId, type, measuredAt, source, sleepStage) tuple
bubbled the P2002 violation as a 500 and the row-edit Sheet
rendered the generic save-error toast.

Mirror the POST handler's P2002 catch: return a clean 409 with a
stable `meta.errorCode = "measurement.duplicate_timestamp"` and a
new measurements.duplicateTimestamp i18n key across the six
locales. The mutation in measurement-list.tsx reads the error
code from the response envelope and surfaces the localised
banner instead of the upstream English fallback.

iOS contract — PATCH /api/measurements/{id}: additive. The 200
happy-path response shape (MeasurementWireDTO), the 422
validation path and the updateMeasurementSchema are untouched.
The new 409 path is strictly clearer than the 500 it replaces.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 00:15:47 +02:00
1 parent 948fcd9318
commit 538b44f74d
9 files changed
+213 -10

No files matched your search

@@ -232,9 +232,24 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
}),
});
const json = await res.json();
const json = (await res.json()) as {
error?: string;
meta?: { errorCode?: string };
};
if (!res.ok) {
throw new Error(json.error ?? "Update failed");
// v1.4.28 FB-B1 — the PUT route now returns a 409 with
// `meta.errorCode === "measurement.duplicate_timestamp"` when
// the edit collides with an existing row's
// `(type, measuredAt, source, sleepStage)` tuple. Forward the
// code alongside the message so `onError` can pick the
// localised string.
const err = new Error(json.error ?? "Update failed") as Error & {
errorCode?: string;
status?: number;
};
err.errorCode = json.meta?.errorCode;
err.status = res.status;
throw err;
}
},
onSuccess: async () => {
@@ -243,6 +258,11 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
setEditError(null);
},
onError: (err) => {
const errWithCode = err as Error & { errorCode?: string };
if (errWithCode.errorCode === "measurement.duplicate_timestamp") {
setEditError(t("measurements.duplicateTimestamp"));
return;
}
setEditError(
err instanceof Error ? err.message : t("measurements.saveError"),
);