// ============================================================ // ZOD SCHEMAS — runtime validation at API boundaries // ============================================================ // Each incoming request body gets parsed through one of these schemas // BEFORE the handler sees it. If parse fails, a 400 is returned with // the detailed validation error — no more silent "undefined reading // X" crashes on malformed input. TypeScript types for req.body can // be inferred from the schema with `z.infer`. // // Keep wire-shape aligned with shared/types.ts. The types file is // for RESPONSES (what the server returns); this file is for REQUESTS // (what the client sends). // ============================================================ import { z } from 'zod'; // ── Common fragments ───────────────────────────────────────── const NonEmptyString = z.string().min(1); const OptionalTrimmed = z.string().optional(); const OptionalModel = z.string().optional(); // ── Auth ───────────────────────────────────────────────────── export const LoginRequestSchema = z.object({ email: z.string().email(), password: NonEmptyString, turnstileToken: OptionalTrimmed, totpCode: OptionalTrimmed, }); export const RegisterRequestSchema = z.object({ email: z.string().email(), password: z.string().min(8, 'Password must be at least 8 characters'), name: NonEmptyString, turnstileToken: OptionalTrimmed, }); export const ForgotPasswordRequestSchema = z.object({ email: z.string().email(), turnstileToken: OptionalTrimmed, }); // ── AI generation requests ─────────────────────────────────── export const HpiEncounterRequestSchema = z.object({ transcript: NonEmptyString, patientAge: OptionalTrimmed, patientGender: OptionalTrimmed, model: OptionalModel, setting: z.enum(['outpatient', 'inpatient']).optional(), physicianMemories: OptionalTrimmed, }); export const SoapRequestSchema = z.object({ transcript: NonEmptyString, patientAge: OptionalTrimmed, patientGender: OptionalTrimmed, model: OptionalModel, type: z.enum(['full', 'subjective']).optional(), additionalInstructions: OptionalTrimmed, physicianMemories: OptionalTrimmed, }); export const SickVisitRequestSchema = z.object({ patientAge: OptionalTrimmed, patientGender: OptionalTrimmed, chiefComplaint: NonEmptyString, transcript: OptionalTrimmed, dictation: OptionalTrimmed, ros: OptionalTrimmed, physicalExam: OptionalTrimmed, diagnoses: OptionalTrimmed, physicianMemories: OptionalTrimmed, model: OptionalModel, }); export const RefineRequestSchema = z.object({ currentDocument: NonEmptyString, instructions: NonEmptyString, sourceContext: OptionalTrimmed, model: OptionalModel, }); export const PeNarrativeRequestSchema = z.object({ steps: z.array(z.object({ component: OptionalTrimmed, label: NonEmptyString, method: OptionalTrimmed, normal: OptionalTrimmed, status: z.enum(['normal', 'abnormal']).nullable().optional(), note: OptionalTrimmed, })).min(1), ageGroup: OptionalTrimmed, system: OptionalTrimmed, patientAge: OptionalTrimmed, patientGender: OptionalTrimmed, model: OptionalModel, format: z.enum(['narrative', 'list']).optional(), }); // ── Extensions CRUD ────────────────────────────────────────── export const ExtensionCreateSchema = z.object({ location: z.string().min(1).max(120), name: z.string().min(1).max(120), number: z.string().min(1).max(40), type: z.enum(['extension', 'pager']).optional(), notes: z.string().max(500).optional(), }); export const ExtensionUpdateSchema = ExtensionCreateSchema; // ── Inferred types (use instead of hand-written interfaces) ─ export type LoginRequest = z.infer; export type RegisterRequest = z.infer; export type ForgotPasswordRequest = z.infer; export type HpiEncounterRequest = z.infer; export type SoapRequest = z.infer; export type SickVisitRequest = z.infer; export type RefineRequest = z.infer; export type PeNarrativeRequest = z.infer; export type ExtensionCreate = z.infer;