mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
fix(labs): auto-link lab readings to a biomarker on write
A free-text lab write resolves or mints a user-scoped biomarker by (userId, lower(analyte)) so no LabResult row ever persists unlinked. An existing marker is reused as-is (its unit/range stay authoritative); a miss mints one from the reading's own spelling. Every reading is now editable and detail-navigable, and the boot-time backfill becomes a pure pre-upgrade migration. Closes the unlinked-row dead-end and the contract hole in one path.
This commit is contained in:
1 parent
b28fa4d4a5
commit
796f6bacd6
3 files changed
+281
-17
No files matched your search
+46
-17
@@ -13,6 +13,7 @@ import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { resolveOrMintBiomarker } from "@/lib/labs/biomarker-store";
|
||||
import {
|
||||
type ResolvedBiomarker,
|
||||
serialiseLabResult,
|
||||
@@ -27,6 +28,13 @@ import {
|
||||
/**
|
||||
* v1.17.1 — structured lab-result store (`/api/labs`).
|
||||
*
|
||||
* Module gate: unlike `/api/illness/*` (born-gated, every route 403s when the
|
||||
* module is off), Labs is intentionally NOT server-gated. The Labs module
|
||||
* toggle is a UX-only nav/visibility preference — the data is always
|
||||
* owner-scoped and safe to read/write, and the Vorsorge lab-panel reminder
|
||||
* + doctor-report PDF read these rows regardless of the nav toggle. Gating
|
||||
* here would 403 those legitimate cross-feature reads. Deliberate opt-out.
|
||||
*
|
||||
* GET lists the caller's live results with optional biomarker / analyte /
|
||||
* panel / date filters. POST records a single reading. `userId` is always
|
||||
* narrowed from the session — never a body field — and the write `data`
|
||||
@@ -36,8 +44,12 @@ import {
|
||||
*
|
||||
* v1.18.1 — structured entry: when the body carries a `biomarkerId`, the row
|
||||
* links the user-scoped catalog marker and the response resolves its unit +
|
||||
* reference range FROM the biomarker (server-authoritative). The web + iOS
|
||||
* clients render the resolved DTO and never recompute.
|
||||
* reference range FROM the biomarker (server-authoritative). A free-text body
|
||||
* (no `biomarkerId`) resolves-or-mints a catalog marker by
|
||||
* `(userId, lower(analyte))`, so NO row ever persists unlinked — every reading
|
||||
* is editable + detail-navigable, and the boot-time backfill becomes a pure
|
||||
* pre-upgrade migration. The web + iOS clients render the resolved DTO and
|
||||
* never recompute.
|
||||
*/
|
||||
|
||||
/** Map the joined biomarker (or null) into the resolver's shape. */
|
||||
@@ -168,8 +180,13 @@ async function postLabResult(request: NextRequest) {
|
||||
note,
|
||||
} = parsed.data;
|
||||
|
||||
// Structured-entry path: resolve the catalog marker (and verify ownership)
|
||||
// so the row's name + unit derive from it. A forged / foreign id is a 404.
|
||||
// Every row links a catalog marker — no `LabResult` ever persists unlinked
|
||||
// (v1.18.1 High). Two paths converge on a `biomarker`:
|
||||
// - Structured: a `biomarkerId` is supplied; resolve + verify ownership
|
||||
// (a forged / foreign id is a 404).
|
||||
// - Free-text: no `biomarkerId`; resolve-or-mint the marker by
|
||||
// `(userId, lower(analyte))` so the legacy quick-capture path still
|
||||
// yields a linked, editable, detail-navigable reading.
|
||||
let biomarker: ResolvedBiomarker | null = null;
|
||||
if (biomarkerId) {
|
||||
const found = await prisma.biomarker.findFirst({
|
||||
@@ -187,24 +204,30 @@ async function postLabResult(request: NextRequest) {
|
||||
return apiError("Biomarker not found", 404);
|
||||
}
|
||||
biomarker = found;
|
||||
} else {
|
||||
biomarker = await resolveOrMintBiomarker(user.id, {
|
||||
analyte: analyte as string,
|
||||
unit: unit as string,
|
||||
referenceLow: referenceLow ?? null,
|
||||
referenceHigh: referenceHigh ?? null,
|
||||
panel: panel ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// Field-by-field assignment — never spread `parsed.data`. With a catalog
|
||||
// link the row stamps the resolved name/unit/range as historical truth
|
||||
// (so a later catalog edit does not silently rewrite a past reading) AND
|
||||
// keeps the FK; reads resolve the CURRENT catalog values via `serialise`.
|
||||
// Field-by-field assignment — never spread `parsed.data`. The row stamps the
|
||||
// resolved name/unit/range as historical truth (so a later catalog edit does
|
||||
// not silently rewrite a past reading) AND keeps the FK; reads resolve the
|
||||
// CURRENT catalog values via `serialise`.
|
||||
const created = await prisma.labResult.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
biomarkerId: biomarker?.id ?? null,
|
||||
panel: biomarker ? biomarker.panel : (panel ?? null),
|
||||
analyte: biomarker ? biomarker.name : (analyte as string),
|
||||
biomarkerId: biomarker.id,
|
||||
panel: biomarker.panel,
|
||||
analyte: biomarker.name,
|
||||
value,
|
||||
unit: biomarker ? biomarker.unit : (unit as string),
|
||||
referenceLow: biomarker ? biomarker.lowerBound : (referenceLow ?? null),
|
||||
referenceHigh: biomarker
|
||||
? biomarker.upperBound
|
||||
: (referenceHigh ?? null),
|
||||
unit: biomarker.unit,
|
||||
referenceLow: biomarker.lowerBound,
|
||||
referenceHigh: biomarker.upperBound,
|
||||
takenAt,
|
||||
source: "MANUAL",
|
||||
noteEncrypted: note ? encryptNoteToBytes(note) : null,
|
||||
@@ -219,7 +242,13 @@ async function postLabResult(request: NextRequest) {
|
||||
|
||||
annotate({
|
||||
action: { name: "labs.create" },
|
||||
meta: { labResultId: created.id, structured: biomarker !== null },
|
||||
meta: {
|
||||
labResultId: created.id,
|
||||
biomarkerId: biomarker.id,
|
||||
// Whether the client picked a catalog marker (structured) vs the
|
||||
// free-text path that resolved-or-minted one server-side.
|
||||
structured: biomarkerId !== undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// v1.18.1 (D2) — eventful Lab↔Vorsorge satisfaction. A lab panel just
|
||||
|
||||
Reference in new issue
Block a user