Adds the three high-ROI tools the planning conversation identified:
vitest 4.x — fast TS-native unit test runner. Runs against pure
functions (calculators, validators, prompt builders). Playwright
stays for e2e. package.json `npm test` now runs Vitest;
`npm run test:node` preserves the old `node --test` runner
for the 3 legacy tests under test/.
zod 4.x — runtime request-body validation at API boundaries. The
new shared/schemas.ts exports a schema per endpoint request
(LoginRequestSchema, SoapRequestSchema, PeNarrativeRequestSchema,
ExtensionCreateSchema, etc.). Routes will adopt these one at a
time post-migration — usage pattern:
const body = SoapRequestSchema.parse(req.body);
Invalid input becomes a structured 400 instead of a silent
undefined-access crash.
knip 6.x — dead-code / unused-export detector. knip.json scopes
it to the backend (client/ excluded since it lives in its own
workspace). Run with `npm run lint:dead`. Catches the class of
bug that kept public/js/adminMilestones.js dead-loaded for a
year — a future orphaned file would fail the lint.
@vitest/coverage-v8 — coverage reporter backed by v8 profiler.
Shipped schemas.test.ts with 8 example cases to prove the toolchain
(`npx vitest run` green).
Not done on day 5 (punted to post-migration): flipping tsconfig to
`strict: true`. That cascade would light up hundreds of implicit-
any errors in handler signatures that would cost more commit bandwidth
than available in this pass. The Day 4 permissive mode is already
catching the big wins (wrong response shapes, orphan refs, undefined
destructures). Post-migration, Codex/vendor model can flip strict flags
one at a time and fix handler-by-handler.
118 lines
4.6 KiB
TypeScript
118 lines
4.6 KiB
TypeScript
// ============================================================
|
|
// 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<typeof XxxSchema>`.
|
|
//
|
|
// 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<typeof LoginRequestSchema>;
|
|
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>;
|
|
export type ForgotPasswordRequest = z.infer<typeof ForgotPasswordRequestSchema>;
|
|
export type HpiEncounterRequest = z.infer<typeof HpiEncounterRequestSchema>;
|
|
export type SoapRequest = z.infer<typeof SoapRequestSchema>;
|
|
export type SickVisitRequest = z.infer<typeof SickVisitRequestSchema>;
|
|
export type RefineRequest = z.infer<typeof RefineRequestSchema>;
|
|
export type PeNarrativeRequest = z.infer<typeof PeNarrativeRequestSchema>;
|
|
export type ExtensionCreate = z.infer<typeof ExtensionCreateSchema>;
|