mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
Add a `hidden` boolean to the Biomarker model (migration 0195_biomarker_hidden) so a marker the user no longer needs can be soft-removed instead of deleted: it leaves the active catalog and the lab-entry pickers but keeps its readings and canonical unit/range. Thread `hidden` through the DTO, the update schema, the PUT handler (field-by-field), and the list endpoint (visible markers first). Regen the OpenAPI contract.
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
/**
|
|
* v1.18.1 — user-scoped Biomarker catalog validation.
|
|
*
|
|
* A `Biomarker` defines a lab marker ONCE: canonical name, unit, reference
|
|
* bounds, and an optional context note. Recording a value later is just
|
|
* picking the biomarker — the unit and reference range are resolved
|
|
* server-side from the catalog row, never re-entered (ending the per-row
|
|
* free-text divergence: "LDL" vs "ldl", `mg/dL` vs `mg/dl`).
|
|
*
|
|
* The reference window is the lab/sex/age-dependent "normal" range; the
|
|
* server's neutral range verdict (`classifyReferenceRange`) is computed
|
|
* against these bounds. Both are independently optional (a marker may report
|
|
* only an upper bound, e.g. LDL < 116); when both are present the lower must
|
|
* not exceed the upper.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
|
|
const requiredText = (max: number) => z.string().trim().min(1).max(max);
|
|
|
|
const lowerBound = z.number().finite().optional();
|
|
const upperBound = z.number().finite().optional();
|
|
|
|
const optionalContext = z
|
|
.string()
|
|
.trim()
|
|
.max(2000)
|
|
.optional()
|
|
// An empty / whitespace-only context normalises to "no context" so a
|
|
// blank field clears rather than stores an empty string.
|
|
.transform((v) => (v && v.length > 0 ? v : undefined));
|
|
|
|
const optionalPanel = z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(120)
|
|
.optional()
|
|
.or(z.literal("").transform(() => undefined));
|
|
|
|
export const createBiomarkerSchema = z
|
|
.object({
|
|
name: requiredText(120),
|
|
unit: requiredText(40),
|
|
lowerBound,
|
|
upperBound,
|
|
context: optionalContext,
|
|
panel: optionalPanel,
|
|
})
|
|
.refine(
|
|
(d) =>
|
|
d.lowerBound === undefined ||
|
|
d.upperBound === undefined ||
|
|
d.lowerBound <= d.upperBound,
|
|
{
|
|
message: "lowerBound must not exceed upperBound",
|
|
path: ["lowerBound"],
|
|
},
|
|
);
|
|
|
|
export type CreateBiomarkerInput = z.infer<typeof createBiomarkerSchema>;
|
|
|
|
/**
|
|
* Update schema: every field optional (partial edit). `context` / `panel` /
|
|
* a bound accept an explicit `null` to clear the stored value — distinct from
|
|
* an omitted key, which leaves the column untouched.
|
|
*/
|
|
export const updateBiomarkerSchema = z
|
|
.object({
|
|
name: requiredText(120).optional(),
|
|
unit: requiredText(40).optional(),
|
|
lowerBound: lowerBound.or(z.null()),
|
|
upperBound: upperBound.or(z.null()),
|
|
context: optionalContext.or(z.null()),
|
|
panel: optionalPanel.or(z.null()),
|
|
// v1.22 — hide / unhide a marker. An omitted key leaves visibility
|
|
// untouched; `true` drops the marker from the active list + pickers.
|
|
hidden: z.boolean().optional(),
|
|
})
|
|
.refine(
|
|
(d) =>
|
|
d.lowerBound === undefined ||
|
|
d.lowerBound === null ||
|
|
d.upperBound === undefined ||
|
|
d.upperBound === null ||
|
|
d.lowerBound <= d.upperBound,
|
|
{
|
|
message: "lowerBound must not exceed upperBound",
|
|
path: ["lowerBound"],
|
|
},
|
|
);
|
|
|
|
export type UpdateBiomarkerInput = z.infer<typeof updateBiomarkerSchema>;
|