Merge branch 'fix/openapi-meta-b' into fix/openapi-meta

This commit is contained in:
Marc-André Bombeck committed 2026-08-22 15:29:51 +02:00
commit 50287d2743
11 files changed
+1719 -1558

No files matched your search

+1612 -1465
View File
File diff suppressed because it is too large. Load diff
+11 -8
View File
@@ -30,25 +30,28 @@ import {
stdResponses,
} from "./shared";
createCustomMetricSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const createCustomMetricRequest = createCustomMetricSchema.meta({
id: "CreateCustomMetricRequest",
description:
"Define a user-scoped custom metric ONCE: free-text `name` + `unit`, an optional target window (`targetLow` / `targetHigh`; when both present `targetLow` must not exceed `targetHigh`), optional display `decimals`, an optional `description`, and explicit `correlationEnabled` opt-in. The name is unique per user. Logging a value later snapshots its unit.",
});
updateCustomMetricSchema.meta({
const updateCustomMetricRequest = updateCustomMetricSchema.meta({
id: "UpdateCustomMetricRequest",
description:
"Partial edit of a custom metric. An omitted key leaves the column untouched; an explicit `null` on a target bound / `decimals` / `description` clears it. `correlationEnabled` explicitly controls use as a bounded behaviour channel. A conflicting rename is rejected 409.",
});
createCustomMetricEntrySchema.meta({
const createCustomMetricEntryRequest = createCustomMetricEntrySchema.meta({
id: "CreateCustomMetricEntryRequest",
description:
"Log a value against a custom metric: numeric `value`, ISO 8601 `measuredAt`, optional free-text `note`. The metric's current unit is snapshotted onto the entry server-side.",
});
updateCustomMetricEntrySchema.meta({
const updateCustomMetricEntryRequest = updateCustomMetricEntrySchema.meta({
id: "UpdateCustomMetricEntryRequest",
description:
"Partial edit of a logged value. An omitted key leaves the column untouched; an explicit `null` on `note` clears it.",
@@ -164,7 +167,7 @@ export const customMetricPaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: createCustomMetricSchema } },
content: { "application/json": { schema: createCustomMetricRequest } },
},
responses: {
...idempotentWrite(),
@@ -213,7 +216,7 @@ export const customMetricPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ id: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: updateCustomMetricSchema } },
content: { "application/json": { schema: updateCustomMetricRequest } },
},
responses: {
"200": {
@@ -296,7 +299,7 @@ export const customMetricPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: createCustomMetricEntrySchema },
"application/json": { schema: createCustomMetricEntryRequest },
},
},
responses: {
@@ -326,7 +329,7 @@ export const customMetricPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: updateCustomMetricEntrySchema },
"application/json": { schema: updateCustomMetricEntryRequest },
},
},
responses: {
+11 -8
View File
@@ -78,25 +78,28 @@ const documentChatListSchema = z
})
.meta({ id: "DocumentChatList" });
inboundConfirmSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const inboundConfirmRequest = inboundConfirmSchema.meta({
id: "InboundConfirmRequest",
description:
"The approve/reject decisions a human made on the review screen. Each names a staged fact by id. Approved facts are committed to the structured stores (labs / conditions / medications) through their normal create paths; rejected facts are discarded. A fact still flagged `needsReview` (below the confidence floor, never edited) cannot be approved — it is reported back as `needsReview`. No `userId` field — it is narrowed from the session; every fact id is re-scoped to the document + caller.",
});
inboundFactEditSchema.meta({
const inboundFactEditRequest = inboundFactEditSchema.meta({
id: "InboundFactEditRequest",
description:
"A correction to a staged fact before approval (fixes OCR / units / dates / codes). Discriminated by `factType` so the edit cannot change the fact's resource type. A successful edit clears `needsReview` — the values become user-asserted.",
});
documentUpdateSchema.meta({
const documentUpdateRequest = documentUpdateSchema.meta({
id: "DocumentUpdateRequest",
description:
"Metadata edit for a stored document: `title` (user label; null clears it), `kind` (category), `documentDate` (user filing date, YYYY-MM-DD; null clears it), `episodeIds` (REPLACE-SET of condition links — the document's links become exactly this set; an empty array unlinks everything; every id must be a live episode of the caller or the whole request answers 404), `encounterIds` (the same replace-set semantics over the caller's visits). At least one field required. No `userId` field — narrowed from the session and fed to the Prisma `where` with the row id.",
});
documentBulkSchema.meta({
const documentBulkRequest = documentBulkSchema.meta({
id: "DocumentBulkRequest",
description:
"One bulk action over up to 100 owner-scoped documents: `setKind` (requires `kind`), `linkEpisode` / `unlinkEpisode` (require `episodeId`), `linkEncounter` / `unlinkEncounter` (require `encounterId`), `delete` (tombstone, undo-able for 30 days), `restore` (clear tombstone). Partial failures never abort the batch — see the per-id result array.",
@@ -514,7 +517,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Applies one action to up to 100 owner-scoped documents and returns a per-id result array — a partial failure never aborts the batch. `error` is `notFound` (unknown / foreign / tombstoned where liveness is required) or `conflict` (restore blocked by a live duplicate of the same bytes).",
requestBody: {
required: true,
content: { "application/json": { schema: documentBulkSchema } },
content: { "application/json": { schema: documentBulkRequest } },
},
responses: {
"200": {
@@ -587,7 +590,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
],
requestBody: {
required: true,
content: { "application/json": { schema: documentUpdateSchema } },
content: { "application/json": { schema: documentUpdateRequest } },
},
responses: {
"200": {
@@ -1078,7 +1081,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
],
requestBody: {
required: true,
content: { "application/json": { schema: inboundFactEditSchema } },
content: { "application/json": { schema: inboundFactEditRequest } },
},
responses: {
"200": {
@@ -1110,7 +1113,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
],
requestBody: {
required: true,
content: { "application/json": { schema: inboundConfirmSchema } },
content: { "application/json": { schema: inboundConfirmRequest } },
},
responses: {
...idempotentWrite(),
+9 -6
View File
@@ -30,19 +30,22 @@ import {
stdResponses,
} from "./shared";
familyHistoryCreateSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const createFamilyHistoryRequest = familyHistoryCreateSchema.meta({
id: "CreateFamilyHistoryRequest",
description:
"Record one condition for one relative (a FHIR FamilyMemberHistory with a single condition). `condition` is the user-facing label (the `condition.code.text` anchor — never a machine-guessed code); `ageAtOnset` is the relative's age (years) when it began. The free-text `note` is encrypted at rest. Patient-reported.",
});
familyHistoryUpdateSchema.meta({
const updateFamilyHistoryRequest = familyHistoryUpdateSchema.meta({
id: "UpdateFamilyHistoryRequest",
description:
"Partial edit of a family-history entry; an omitted key leaves the column untouched. A `null` `ageAtOnset`/`note` clears that field. Rejects unknown keys.",
});
familyHistoryListQuerySchema.meta({
const listFamilyHistoryQuery = familyHistoryListQuerySchema.meta({
id: "ListFamilyHistoryQuery",
description:
"Query params for the family-history list: optional `limit` (1–200, default 100). Newest-first.",
@@ -78,7 +81,7 @@ export const familyHistoryPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List family history (v1.25)",
description:
"Returns the caller's live (non-deleted) family-history entries, newest-first.",
requestParams: { query: familyHistoryListQuerySchema },
requestParams: { query: listFamilyHistoryQuery },
responses: {
...recordRefusal(),
"200": {
@@ -104,7 +107,7 @@ export const familyHistoryPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: familyHistoryCreateSchema },
"application/json": { schema: createFamilyHistoryRequest },
},
},
responses: {
@@ -158,7 +161,7 @@ export const familyHistoryPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: familyHistoryUpdateSchema },
"application/json": { schema: updateFamilyHistoryRequest },
},
},
responses: {
+15 -26
View File
@@ -22,8 +22,6 @@ import {
illnessEpisodeResolveSchema,
illnessEpisodeListQuerySchema,
illnessDayLogInputSchema,
illnessDayLogQuerySchema,
illnessDayLogListQuerySchema,
illnessInsightsQuerySchema,
illnessTypeEnum,
illnessLifecycleEnum,
@@ -38,48 +36,39 @@ import {
stdResponses,
} from "./shared";
illnessEpisodeCreateSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const createIllnessEpisodeRequest = illnessEpisodeCreateSchema.meta({
id: "CreateIllnessEpisodeRequest",
description:
"Open an illness/condition episode. `label` is the user-facing name; `type` + `lifecycle` classify it. `onsetAt` defaults to 'now' server-side when omitted. `parentConditionId` threads a FLARE/RECURRING bout under a parent condition (must be an owned, live episode). The optional `note` is encrypted at rest.",
});
illnessEpisodeUpdateSchema.meta({
const updateIllnessEpisodeRequest = illnessEpisodeUpdateSchema.meta({
id: "UpdateIllnessEpisodeRequest",
description:
"Partial edit of an episode; an omitted key leaves the column untouched. A `null` `resolvedAt` re-opens a resolved episode; a `null` `parentConditionId` detaches it from its parent. Rejects unknown keys.",
});
illnessEpisodeResolveSchema.meta({
const resolveIllnessEpisodeRequest = illnessEpisodeResolveSchema.meta({
id: "ResolveIllnessEpisodeRequest",
description:
"Mark an episode recovered. `resolvedAt` defaults to 'now' when omitted, so the one-tap 'mark recovered' affordance can send an empty body. A CHRONIC_ONGOING episode has no recovery date by design and 422s.",
});
illnessEpisodeListQuerySchema.meta({
const listIllnessEpisodesQuery = illnessEpisodeListQuerySchema.meta({
id: "ListIllnessEpisodesQuery",
description:
"Query params for the episode history: optional `limit` (1–100, default 50) and `includeResolved` ('true' | 'false'); 'false' hides episodes that already carry a `resolvedAt`. Newest-first by `onsetAt`.",
});
illnessDayLogInputSchema.meta({
const upsertIllnessDayLogRequest = illnessDayLogInputSchema.meta({
id: "UpsertIllnessDayLogRequest",
description:
"Upsert one day's symptom / functional-impact / fever row for an episode (keyed on `(episodeId, date)`). `date` is a `YYYY-MM-DD` tz-anchored day. `functionalImpact` (0–3) + `feverC` are queryable plaintext; `symptoms` carry an optional 0–3 Jackson/WURSS severity per link; the optional `note` is encrypted at rest.",
});
illnessDayLogQuerySchema.meta({
id: "GetIllnessDayLogQuery",
description:
"Single-day read query: `date` is a `YYYY-MM-DD` day. Returns the matching day-log or `null` when nothing is logged for that day.",
});
illnessDayLogListQuerySchema.meta({
id: "ListIllnessDayLogsQuery",
description:
"Date-less LIST query (v1.18.3): omit `date` to page the episode's whole day-log history. `limit` (1–200, default 60) + `offset` (default 0) + `sortDir` ('asc' | 'desc', default 'desc') with `meta.total`. Mutually exclusive with the single-day `date` read — presence of `date` selects the single-day mode.",
});
/**
* The documented query for `GET .../day-logs`: a `date` (single-day read) OR
* the list pagination params (date-less list). Both are optional on the wire;
@@ -97,7 +86,7 @@ const illnessDayLogGetQuery = z
})
.meta({ id: "GetIllnessDayLogsQuery" });
illnessInsightsQuerySchema.meta({
const illnessInsightsQuery = illnessInsightsQuerySchema.meta({
id: "IllnessInsightsQuery",
description:
"Cross-episode retrospective window query: optional `windowDays` (30–1095, default 365) and optional `includeRecoveryGap` (default false). With `includeRecoveryGap` off the response carries the count breakdown and a null typical recovery gap on a single fast query; set it true to pay for the per-episode correlation that computes the recovery gap. Retrospective only — the engine summarises past episodes, never forecasts.",
@@ -298,7 +287,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List illness episodes (v1.18.1)",
description:
"Returns the caller's live (non-deleted) illness/condition episodes, newest-first by onset. `includeResolved=false` hides episodes that already carry a `resolvedAt`. Born-gated.",
requestParams: { query: illnessEpisodeListQuerySchema },
requestParams: { query: listIllnessEpisodesQuery },
responses: {
...recordRefusal(),
"200": {
@@ -324,7 +313,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: illnessEpisodeCreateSchema },
"application/json": { schema: createIllnessEpisodeRequest },
},
},
responses: {
@@ -375,7 +364,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: illnessEpisodeUpdateSchema },
"application/json": { schema: updateIllnessEpisodeRequest },
},
},
responses: {
@@ -456,7 +445,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: false,
content: {
"application/json": { schema: illnessEpisodeResolveSchema },
"application/json": { schema: resolveIllnessEpisodeRequest },
},
},
responses: {
@@ -514,7 +503,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: illnessDayLogInputSchema },
"application/json": { schema: upsertIllnessDayLogRequest },
},
},
responses: {
@@ -576,7 +565,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "Cross-episode retrospective insights (v1.18.1)",
description:
"Returns the cross-episode retrospective summary over a trailing window: 'sick N times · typical recovery gap X days', a recurrence-by-month tally, and a per-type breakdown. The typical gap is withheld (null) below the min-sample floor (asserts nothing thin). Retrospective ONLY — the recurrence figure is a count of the past, never a forecast. Born-gated + owner-scoped.",
requestParams: { query: illnessInsightsQuerySchema },
requestParams: { query: illnessInsightsQuery },
responses: {
"200": {
description: "The cross-episode retrospective summary.",
+9 -6
View File
@@ -25,19 +25,22 @@ import {
stdResponses,
} from "./shared";
createLabResultSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const createLabResultRequest = createLabResultSchema.meta({
id: "CreateLabResultRequest",
description:
'Record a single biomarker reading (HbA1c, LDL, ferritin, TSH, …). A reading is EITHER numeric (`value` + `unit`, optional reference bounds) OR qualitative (`valueText`, e.g. "negativ" / "positiv") — exactly one, never both, never neither. `analyte` + `unit` are free-form (a lab prints its own naming); a qualitative reading needs no unit / bounds. When both numeric bounds are present `referenceLow` must not exceed `referenceHigh`. `takenAt` is a backdatable ISO instant (no future, ≤ 50 years past). The optional `note` is encrypted at rest. The optional `source` records provenance: `"OCR"` for an on-device-OCR capture (the raw image never reaches the server on this path), defaulting to `"MANUAL"` when omitted. The optional `encounterId` files the reading against a visit the caller owns — always optional, and a link that could not be made never fails the reading.',
});
updateLabResultSchema.meta({
const updateLabResultRequest = updateLabResultSchema.meta({
id: "UpdateLabResultRequest",
description:
"Partial edit of a lab result. An omitted key leaves the column untouched; an explicit `null` on `panel` / `note` / a reference bound clears it. Edit the matching value field for the row's type — `value` for a numeric row, `valueText` for a qualitative one; a request must not carry both, and a cross-type edit (the wrong field for the row) is rejected.",
});
listLabResultsSchema.meta({
const listLabResultsQuery = listLabResultsSchema.meta({
id: "ListLabResultsQuery",
description:
"Query params for the lab-result list: optional `analyte` (exact) + `panel` filters, an inclusive `from`/`to` date range, and `limit` (≤ 500) / `offset` pagination. Defaults to `takenAt` DESC.",
@@ -122,7 +125,7 @@ export const labsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List the caller's lab results",
description:
"Returns the caller's live (non-deleted) lab results, newest first, with optional analyte / panel / date-range filters. Each row carries a server-computed neutral `rangeStatus` and a `hasNote` flag (the encrypted note itself is not echoed).",
requestParams: { query: listLabResultsSchema },
requestParams: { query: listLabResultsQuery },
responses: {
...recordRefusal(),
"200": {
@@ -144,7 +147,7 @@ export const labsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: createLabResultSchema } },
content: { "application/json": { schema: createLabResultRequest } },
},
responses: {
...idempotentWrite(),
@@ -190,7 +193,7 @@ export const labsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ id: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: updateLabResultSchema } },
content: { "application/json": { schema: updateLabResultRequest } },
},
responses: {
...recordRefusal(),
+9 -6
View File
@@ -19,7 +19,6 @@ import {
bulkDeleteIntakeEventsSchema,
updateIntakeEventSchema,
} from "@/lib/validations/medication";
import { medicationExtractionSchema } from "@/lib/ai/coach/medication-extract-prompt";
import {
scheduleRevisionCreateSchema,
scheduleRevisionUpdateSchema,
@@ -39,13 +38,16 @@ import {
stdResponses,
} from "../shared";
efficacyTargetOverrideSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const setMedicationEfficacyTargetRequest = efficacyTargetOverrideSchema.meta({
id: "SetMedicationEfficacyTargetRequest",
description:
"Set or clear the user's explicit efficacy-target override for a medication. `clear:true` removes the override so the resolver reverts to the derived (ATC class prefix → name inference) target; otherwise pin exactly ONE of `measurementType` (a metric series) / `biomarkerId` (a lab analyte). `userId` is never a field — ownership is narrowed through the medication (and the biomarker for a lab target).",
});
medicationEfficacyResponseSchema.meta({
const medicationEfficacyResponse = medicationEfficacyResponseSchema.meta({
id: "MedicationEfficacyResponse",
description:
"Server-authoritative, strictly-descriptive efficacy view relating a medication to the outcome metric(s)/lab(s) its class is prescribed to move, around its start. Carries the resolved target(s) with their series, the start/dose-change/pause markers, a before/after-start comparison (honest `{present:false}` below the per-side data floor), an adherence lane (cadence-aware per-day rate, never recomputed), an optional conservative level-shift note, and the retarget options. There is NO verdict / score / assessment field by construction — the client renders numbers and neutral connective phrasing only, never a causal or dose-advice claim.",
@@ -62,6 +64,7 @@ import {
scheduleRevisionResource,
scheduleRevisionListResponse,
medicationExtractRequest,
medicationExtractionResult,
medicationListLayoutPutBody,
medicationListLayoutResult,
doseHistoryQuery,
@@ -1336,7 +1339,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
medicationExtractionSchema,
medicationExtractionResult,
"MedicationExtractResponse",
),
},
@@ -1462,7 +1465,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
medicationEfficacyResponseSchema,
medicationEfficacyResponse,
"GetMedicationEfficacyResponse",
),
},
@@ -1488,7 +1491,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: efficacyTargetOverrideSchema },
"application/json": { schema: setMedicationEfficacyTargetRequest },
},
},
responses: {
@@ -967,7 +967,10 @@ export const doseHistoryResponse = z
"Per-slot dose ledger over [from, to]: every expected slot with a status plus every off-schedule intake tagged ad-hoc. Built from the same band minter + `reconstructDoseHistory` the compliance % consumes, so the history view and the rate never contradict each other.",
});
medicationExtractionSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so the annotated clone is exported and the path table
// references it — a bare call would register nothing.
export const medicationExtractionResult = medicationExtractionSchema.meta({
id: "MedicationExtractionResult",
description:
"Citation-guarded partial extraction of medication scheduling fields. Every field is optional; the wizard merges what is present onto the form state and leaves the rest blank. `name` and `dose` are post-validated against the original free-text and dropped when not substring-matched, so the wizard cannot silently land a hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped to the wizard's wire bounds.",
+7 -4
View File
@@ -27,13 +27,16 @@ import {
stdResponses,
} from "./shared";
createAssessmentSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const createMentalHealthAssessmentRequest = createAssessmentSchema.meta({
id: "CreateMentalHealthAssessmentRequest",
description:
"Record one completed screener administration: PHQ-9 (9 items, 0–3), GAD-7 (7 items, 0–3), WHO-5 (5 items, 0–5) or SCI (8 items, 0–4). The array length and per-item ceiling must match the instrument. The item answers are encrypted at rest and never returned. The server computes the total (WHO-5 reports raw-sum × 4 as 0–100) + severity band + item-9 safety flag. Opt-in, beside mood tracking — this is a screening surface, not a diagnosis.",
});
listAssessmentsSchema.meta({
const listMentalHealthAssessmentsQuery = listAssessmentsSchema.meta({
id: "ListMentalHealthAssessmentsQuery",
description:
"Filter the caller's screener history by instrument; paginate with limit/offset.",
@@ -111,7 +114,7 @@ export const mentalHealthPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List the caller's screener history",
description:
"Returns PHQ-9 / GAD-7 / WHO-5 / SCI administrations (newest first). Totals + bands + flags only; raw item answers are never returned.",
requestParams: { query: listAssessmentsSchema },
requestParams: { query: listMentalHealthAssessmentsQuery },
responses: {
...recordRefusal(),
"200": {
@@ -136,7 +139,7 @@ export const mentalHealthPaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
content: {
"application/json": { schema: createAssessmentSchema },
"application/json": { schema: createMentalHealthAssessmentRequest },
},
},
responses: {
+16 -15
View File
@@ -61,40 +61,41 @@ import {
} from "./shared";
// ── Request schemas — annotated for spec emission ────────────────────
//
// Assigned rather than annotated in place: Zod 4's `.meta()` returns a NEW
// instance carrying the id, so the route table below must reference these
// consts for the names to reach `components.schemas`.
createCustomTagSchema.meta({
const createMoodTagRequest = createCustomTagSchema.meta({
id: "CreateMoodTagRequest",
description:
"Create a per-user custom mood tag (BINARY only). `label` is encrypted at rest. `icon` must come from the curated allowlist (unknown name → 422). `categoryKey` picks the home group — any seeded category key or one of the caller's own `customcat:` group keys; omitted → the seeded `custom` category. Capped at 50 active custom tags per user (422).",
});
updateCustomTagSchema.meta({
const updateMoodTagRequest = updateCustomTagSchema.meta({
id: "UpdateMoodTagRequest",
description:
"Partial custom-tag edit; at least one field required. `isActive:false` archives (history intact), `isActive:true` restores. `categoryKey` moves the tag to another group (a real categoryId move).",
});
createCustomGroupSchema.meta({
const createMoodTagGroupRequest = createCustomGroupSchema.meta({
id: "CreateMoodTagGroupRequest",
description:
"Create a per-user custom mood-tag group. `label` is encrypted at rest; `icon` must come from the curated allowlist. Capped at 12 active custom groups per user (422).",
});
updateCustomGroupSchema.meta({
const updateMoodTagGroupRequest = updateCustomGroupSchema.meta({
id: "UpdateMoodTagGroupRequest",
description:
"Partial custom-group edit; at least one field required. `isActive:false` retires the group without touching its tags.",
});
hideCatalogueTagSchema.meta({
const hideMoodTagRequest = hideCatalogueTagSchema.meta({
id: "HideMoodTagRequest",
description:
"Hide / show a CATALOGUE tag for the calling user. Custom tags are hidden via their own `isActive` flag on the custom PATCH instead (this route 400s a `custom:` key).",
});
// Assigned rather than annotated in place: Zod 4's `.meta()` returns a NEW
// instance carrying the id, so the route table below must reference these
// consts for the names to reach `components.schemas`.
const createMoodEntryRequest = createMoodEntrySchema.meta({
id: "CreateMoodEntryRequest",
description:
@@ -113,7 +114,7 @@ const listMoodEntriesQuery = listMoodEntriesSchema.meta({
"Filters + paging for the mood-entry list. `from` / `to` bound the local-day `date` label (YYYY-MM-DD, inclusive). `limit` capped at 500.",
});
moodTagLayoutSchema.meta({
const moodTagLayout = moodTagLayoutSchema.meta({
id: "MoodTagLayout",
description:
"Per-user mood-tag presentation blob. `groupOrder`: category keys in display order (unknown dropped, missing appended in seeded order). `placements`: categoryKey → ordered tag keys; a placed tag renders in that group at that index, un-placed tags follow in their home category. Display-only — placements referencing hidden/archived/unknown keys are silently dropped at read time. Both fields optional: PUT merges preserve-when-absent.",
@@ -220,7 +221,7 @@ const moodTagLayoutResolved = z
// v1.32.22 (M2) — the PUT request extends the layout schema with the base
// token (stripped pre-Zod at runtime).
const moodTagLayoutPutRequest = moodTagLayoutSchema
const moodTagLayoutPutRequest = moodTagLayout
.extend({ baseUpdatedAt: baseUpdatedAtField })
.meta({
id: "MoodTagLayoutPutRequest",
@@ -1349,7 +1350,7 @@ export const moodPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Mints a `custom:<uuid>` key, encrypts the label at rest, stores a BINARY tag owned by the caller under the chosen group (default: the seeded `custom` category). 422 over the 50-tag cap or on an unknown / foreign `categoryKey`.",
requestBody: {
required: true,
content: { "application/json": { schema: createCustomTagSchema } },
content: { "application/json": { schema: createMoodTagRequest } },
},
responses: {
"201": {
@@ -1376,7 +1377,7 @@ export const moodPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ key: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: updateCustomTagSchema } },
content: { "application/json": { schema: updateMoodTagRequest } },
},
responses: {
"200": {
@@ -1429,7 +1430,7 @@ export const moodPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ key: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: hideCatalogueTagSchema } },
content: { "application/json": { schema: hideMoodTagRequest } },
},
responses: {
"200": {
@@ -1460,7 +1461,7 @@ export const moodPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Mints a `customcat:<uuid>` key, encrypts the label at rest, stores a group owned by the caller. 422 over the 12-group cap.",
requestBody: {
required: true,
content: { "application/json": { schema: createCustomGroupSchema } },
content: { "application/json": { schema: createMoodTagGroupRequest } },
},
responses: {
"201": {
@@ -1487,7 +1488,7 @@ export const moodPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ key: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: updateCustomGroupSchema } },
content: { "application/json": { schema: updateMoodTagGroupRequest } },
},
responses: {
"200": {
+16 -13
View File
@@ -41,37 +41,40 @@ import {
stdResponses,
} from "./shared";
vaccinationCreateSchema.meta({
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
// annotating in place, so each annotated schema is bound to a const and the
// route table below references that const — a bare call would register nothing.
const createVaccinationRequest = vaccinationCreateSchema.meta({
id: "CreateVaccinationRequest",
description:
"Log one administered dose. `occurredAt` is required and must not be in the future — a vaccination is recorded after the fact, and a planned booster is a reminder rather than a record. Exactly one identity arm is required: either `antigenSlug` (a slug the catalogue offers) or `vaccineName` (whatever the person's own record says, verbatim, which may be a trade name). Everything else is optional, so a decades-old paper entry with nothing but a date and a name is loggable. `note` is encrypted at rest. `documentIds` pre-links the scanned pages; an id naming nothing the caller owns is dropped rather than refused, so a link never blocks a save. Logging a dose satisfies any booster reminder keyed to one of its component antigens.",
});
vaccinationUpdateSchema.meta({
const updateVaccinationRequest = vaccinationUpdateSchema.meta({
id: "UpdateVaccinationRequest",
description:
"Partial edit of a dose; an omitted key leaves the column untouched, and a body naming nothing is a 422. `occurredAt` is editable because a transcription typo is the common case — and editing it deliberately does NOT re-run the booster satisfaction, so correcting a date can never move a reminder's due date. An edit that would leave the record with neither identity arm is refused. A present `documentIds` array replaces the links, empty array included.",
});
vaccinationListQuerySchema.meta({
const listVaccinationsQuery = vaccinationListQuerySchema.meta({
id: "ListVaccinationsQuery",
description:
"Filters for the immunization list: `antigenSlug`, `from` / `to` (ISO-8601 instants) and `limit` (1–500, default 300). The list is full and bounded rather than paged — a vaccination record is a lifetime document but a short one.",
});
vaccinationBoosterSchema.meta({
const vaccinationBoosterRequest = vaccinationBoosterSchema.meta({
id: "VaccinationBoosterRequest",
description:
"The confirm body for a booster reminder — the person's own values, accepted or edited from the catalogue's prefill. `intervalMonths` is bounded at 600 (a decade booster is 120); `label` is composed client-side so its locale is the person's own. There is no antigen field: the server reads the antigen from the dose's catalogue entry, never from the request, so a client cannot key a reminder onto an antigen the dose does not contain.",
});
vaccinationSuggestQuerySchema.meta({
const vaccinationSuggestQuery = vaccinationSuggestQuerySchema.meta({
id: "VaccinationSuggestQuery",
description:
"The single input to the upload suggestion: the document's anchor date (ISO-8601 with offset). No ids — the candidate doses come from the caller's own record, narrowed from the session.",
});
vaccinationLinkSchema.meta({
const vaccinationLinkRequest = vaccinationLinkSchema.meta({
id: "VaccinationLinkRequest",
description:
"Documents to file against, or unfile from, a dose. Idempotent in both directions and capped at 100 ids. Ids naming nothing the caller owns come back in `unknown` rather than failing the request.",
@@ -224,7 +227,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List vaccinations",
description:
"Returns the caller's live doses, newest first, each with its series resolved per component antigen. Soft-deleted doses are excluded.",
requestParams: { query: vaccinationListQuerySchema },
requestParams: { query: listVaccinationsQuery },
responses: {
...recordRefusal(),
"200": {
@@ -246,7 +249,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Records one administered dose and satisfies every live booster reminder keyed to one of its component antigens — so a single combined dose settles each of the antigens it contains. Satisfaction is forward-only: a dose older than a reminder's last satisfaction is a no-op, which is what makes back-entering an old paper record safe. Honours `Idempotency-Key`. Audits as `vaccination.record.create`.",
requestBody: {
required: true,
content: { "application/json": { schema: vaccinationCreateSchema } },
content: { "application/json": { schema: createVaccinationRequest } },
},
responses: {
...idempotentWrite(),
@@ -292,7 +295,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: vaccinationUpdateSchema } },
content: { "application/json": { schema: updateVaccinationRequest } },
},
responses: {
...recordRefusal(),
@@ -363,7 +366,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: vaccinationBoosterSchema } },
content: { "application/json": { schema: vaccinationBoosterRequest } },
},
responses: {
...recordRefusal(),
@@ -406,7 +409,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "Suggest the dose a document belongs to",
description:
"Which dose does a document dated `anchor` most plausibly belong to? The document upload review asks this when a scan is classified `VACCINATION`. The window (±7 days, shared with the visit moment) and the one-pre-selects / many-offer-a-picker verdict are decided server-side so the browser never re-derives them. Owner-scoped; the anchor is the only input and it is a date, not an id.",
requestParams: { query: vaccinationSuggestQuerySchema },
requestParams: { query: vaccinationSuggestQuery },
responses: {
...recordRefusal(),
"200": {
@@ -433,7 +436,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: vaccinationLinkSchema } },
content: { "application/json": { schema: vaccinationLinkRequest } },
},
responses: {
"200": {
@@ -462,7 +465,7 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: vaccinationLinkSchema } },
content: { "application/json": { schema: vaccinationLinkRequest } },
},
responses: {
"200": {