mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Add the `/api/allergies` and `/api/family-history` endpoints (list / create / read / update / soft-delete), each wrapped in `apiHandler` with Zod `safeParse` + `returnAllZodIssues`. `userId` is always narrowed from the session/Bearer — never a body field — and the Prisma `data` is built field-by-field (no mass assignment). Creates wrap in `withIdempotency`. The free-text reaction/note are encrypted before write and decrypted fail-soft on read via the server-authoritative DTOs. Register the query-key factory entries and the OpenAPI route tables (new `Records` tag); regenerate `docs/api/openapi.yaml`.
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
/**
|
|
* Allergy / intolerance request/response validation (v1.25, W-RECORDS).
|
|
*
|
|
* Source of truth for the `/api/allergies/*` wire contract. The Zod schemas
|
|
* here are reused by the OpenAPI registry so the spec stays single-source.
|
|
* `userId` is NEVER a body field — it is narrowed from the session/Bearer in
|
|
* every route and fed to the Prisma `where`.
|
|
*
|
|
* A structured AllergyIntolerance-style RECORD (not a time-series signal):
|
|
* `substance` is the queryable human label; the free-text reaction
|
|
* description + notes are encrypted at rest (`reactionEncrypted` /
|
|
* `notesEncrypted` Bytes columns). Patient-reported — never a clinical
|
|
* diagnosis the app asserts.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
import { isPlausibleEntryInstant } from "@/lib/validations/entry-instant";
|
|
|
|
/**
|
|
* Plausible-instant bound shared with the measurement / illness paths: no
|
|
* future instant beyond the 5-min skew, no instant before 1900. Stays a
|
|
* string on the wire (iOS reads it back verbatim).
|
|
*/
|
|
const boundedInstant = z.iso
|
|
.datetime({ offset: true })
|
|
.refine((s) => isPlausibleEntryInstant(new Date(s)), {
|
|
message: "must be a plausible instant (not future, not pre-1900)",
|
|
});
|
|
|
|
/* ── enums (mirror the Prisma enums) ─────────────────────────────── */
|
|
|
|
export const allergyCategoryEnum = z.enum([
|
|
"FOOD",
|
|
"MEDICATION",
|
|
"ENVIRONMENT",
|
|
"BIOLOGIC",
|
|
"OTHER",
|
|
]);
|
|
|
|
export const allergyTypeEnum = z.enum(["ALLERGY", "INTOLERANCE"]);
|
|
|
|
export const allergySeverityEnum = z.enum(["MILD", "MODERATE", "SEVERE"]);
|
|
|
|
export const allergyStatusEnum = z.enum(["ACTIVE", "INACTIVE", "RESOLVED"]);
|
|
|
|
/* ── CRUD ─────────────────────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Create an allergy. `substance` is the user-facing allergen name; the
|
|
* free-text `reaction` + `note` are encrypted at rest. `onsetAt` is optional
|
|
* (unknown when omitted).
|
|
*/
|
|
export const allergyCreateSchema = z.object({
|
|
substance: z.string().min(1).max(160),
|
|
category: allergyCategoryEnum.optional().default("OTHER"),
|
|
type: allergyTypeEnum.optional().default("ALLERGY"),
|
|
severity: allergySeverityEnum.nullable().optional(),
|
|
status: allergyStatusEnum.optional().default("ACTIVE"),
|
|
onsetAt: boundedInstant.nullable().optional(),
|
|
reaction: z.string().max(2000).nullable().optional(),
|
|
note: z.string().max(2000).nullable().optional(),
|
|
});
|
|
|
|
export type AllergyCreate = z.infer<typeof allergyCreateSchema>;
|
|
|
|
/**
|
|
* Edit an allergy — every field optional; an omitted field is left
|
|
* untouched. Rejects unknown keys.
|
|
*/
|
|
export const allergyUpdateSchema = z
|
|
.object({
|
|
substance: z.string().min(1).max(160).optional(),
|
|
category: allergyCategoryEnum.optional(),
|
|
type: allergyTypeEnum.optional(),
|
|
severity: allergySeverityEnum.nullable().optional(),
|
|
status: allergyStatusEnum.optional(),
|
|
onsetAt: boundedInstant.nullable().optional(),
|
|
reaction: z.string().max(2000).nullable().optional(),
|
|
note: z.string().max(2000).nullable().optional(),
|
|
})
|
|
.strict();
|
|
|
|
export type AllergyUpdate = z.infer<typeof allergyUpdateSchema>;
|
|
|
|
/** History/list query — newest-first, bounded. */
|
|
export const allergyListQuerySchema = z.object({
|
|
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
includeInactive: z.union([z.literal("true"), z.literal("false")]).optional(),
|
|
});
|
|
|
|
export type AllergyListQuery = z.infer<typeof allergyListQuerySchema>;
|