refactor(api): read the health-record route bodies through parseJsonBody

Labs, biomarkers, custom metrics, cycle, anamnesis, illness, measurement
reminders, the measurement edit, medication sub-routes, mood and mental
health assessments still spelled out safeJson, safeParse and the 422 by
hand. Each becomes one parseJsonBody call with the size cap, action,
issue count, error code and status it had. The audit rows and the
unstable-externalId breadcrumbs move into onInvalid, in the order they
ran; where a breadcrumb reads the body, the wide-event annotate moves with
it. A `body ?? {}` or a payload unwrapped from the body becomes `input`.

The medication ingest route reads through the helper too; its Bearer
resolution happens before the read and is unchanged.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 11:29:22 +02:00
1 parent f06df5fd71
commit af14d0aa7e
28 files changed
+495 -698

No files matched your search

+22 -28
View File
@@ -5,10 +5,9 @@ import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { overwriteDetails } from "@/lib/sharing/audit-details";
import { prisma } from "@/lib/db";
@@ -111,34 +110,29 @@ export const PUT = apiHandler(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, updateLabResultSchema, {
maxBytes: 16 * 1024,
action: "labs.update.validation-failed",
countIssues: true,
meta: { labResultId: id },
errorCode: "labs.update.invalid",
onInvalid: (error) => {
const auditIssues = sanitiseZodIssues(error.issues, {
stripValuesFromMessage: true,
});
// v1.37.0 — through `auditLog()` rather than a bare `prisma.auditLog
// .create`, because that helper is the only writer that stamps
// `actorUserId`. Filed under the resolved record either way; without
// the stamp a manager's malformed payload would read as the owner's own.
void auditLog("labs.update.validation-failed", {
userId: user.id,
details: { issues: auditIssues, labResultId: id },
}).catch(() => {
/* swallow — the 422 response is the contract */
});
},
});
if (jsonError) return jsonError;
const parsed = updateLabResultSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "labs.update.validation-failed" },
meta: { issue_count: parsed.error.issues.length, labResultId: id },
});
const auditIssues = sanitiseZodIssues(parsed.error.issues, {
stripValuesFromMessage: true,
});
// v1.37.0 — through `auditLog()` rather than a bare `prisma.auditLog
// .create`, because that helper is the only writer that stamps
// `actorUserId`. Filed under the resolved record either way; without the
// stamp a manager's malformed payload would read as the owner's own.
void auditLog("labs.update.validation-failed", {
userId: user.id,
details: { issues: auditIssues, labResultId: id },
}).catch(() => {
/* swallow — the 422 response is the contract */
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.update.invalid",
});
}
if (!parsed.ok) return parsed.error;
const d = parsed.data;
+21 -31
View File
@@ -19,13 +19,8 @@ import { NextRequest } from "next/server";
import { fireAndForget } from "@/lib/logging/fire-and-forget";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiSuccess, getClientIp, sanitiseZodIssues } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
@@ -159,32 +154,27 @@ async function commitOcrRows(request: NextRequest) {
// OCR_MAX_ROWS rows at every field's longest bound run past 256 KB; 512
// keeps a full report in one body with room to spare.
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, ocrCommitSchema, {
maxBytes: 512 * 1024,
action: "labs.ocr.commit.validation-failed",
countIssues: true,
onInvalid: (error) => {
// Free-text analyte / unit could land in a Zod issue message — strip values.
const auditIssues = sanitiseZodIssues(error.issues, {
stripValuesFromMessage: true,
});
prisma.auditLog
.create({
data: {
userId: user.id,
action: "labs.ocr.commit.validation-failed",
details: JSON.stringify({ issues: auditIssues }),
},
})
.catch(() => {});
},
});
if (jsonError) return jsonError;
const parsed = ocrCommitSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "labs.ocr.commit.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
// Free-text analyte / unit could land in a Zod issue message — strip values.
const auditIssues = sanitiseZodIssues(parsed.error.issues, {
stripValuesFromMessage: true,
});
prisma.auditLog
.create({
data: {
userId: user.id,
action: "labs.ocr.commit.validation-failed",
details: JSON.stringify({ issues: auditIssues }),
},
})
.catch(() => {});
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const inserted: ReturnType<typeof serialiseLabResult>[] = [];
const skipped: OcrSkippedRowDto[] = [];
+22 -28
View File
@@ -7,9 +7,9 @@ import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
@@ -157,35 +157,29 @@ async function postLabResult(request: NextRequest) {
// that leaves the owner with a usable catalogue afterwards.
const { user } = await requireRecordAuth("write", "labs");
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, createLabResultSchema, {
maxBytes: 16 * 1024,
action: "labs.create.validation-failed",
countIssues: true,
errorCode: "labs.create.invalid",
onInvalid: (error) => {
// Free-text `analyte` / `unit` / `note` could land verbatim in a Zod
// issue message — strip values from the audit-ledger breadcrumb.
const auditIssues = sanitiseZodIssues(error.issues, {
stripValuesFromMessage: true,
});
// v1.36.x — through `auditLog()`, the only writer that stamps
// `actorUserId`. A bare `prisma.auditLog.create` files a delegate's
// malformed payload as if the owner had typed it.
void auditLog("labs.create.validation-failed", {
userId: user.id,
details: { issues: auditIssues },
}).catch(() => {
/* swallow — the 422 response is the contract */
});
},
});
if (jsonError) return jsonError;
const parsed = createLabResultSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "labs.create.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
// Free-text `analyte` / `unit` / `note` could land verbatim in a Zod
// issue message — strip values from the audit-ledger breadcrumb.
const auditIssues = sanitiseZodIssues(parsed.error.issues, {
stripValuesFromMessage: true,
});
// v1.36.x — through `auditLog()`, the only writer that stamps
// `actorUserId`. A bare `prisma.auditLog.create` files a delegate's
// malformed payload as if the owner had typed it.
void auditLog("labs.create.validation-failed", {
userId: user.id,
details: { issues: auditIssues },
}).catch(() => {
/* swallow — the 422 response is the contract */
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.create.invalid",
});
}
if (!parsed.ok) return parsed.error;
const {
biomarkerId,