mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Merge pull request #824 from MBombeck/fix/openapi-meta
Register the schemas that annotated themselves into nothing
This commit is contained in:
28 files changed
+3807
-4439
No files matched your search
+3506
-4204
File diff suppressed because it is too large.
Load diff
@@ -33,19 +33,23 @@ import {
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
allergyCreateSchema.meta({
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document.
|
||||
const createAllergyRequest = allergyCreateSchema.meta({
|
||||
id: "CreateAllergyRequest",
|
||||
description:
|
||||
"Record an allergy/intolerance. `substance` is the user-facing allergen name (the FHIR AllergyIntolerance `code.text` anchor — never a machine-guessed code). The free-text `reaction` + `note` are encrypted at rest. `onsetAt` is optional (unknown when omitted). Patient-reported.",
|
||||
});
|
||||
|
||||
allergyUpdateSchema.meta({
|
||||
const updateAllergyRequest = allergyUpdateSchema.meta({
|
||||
id: "UpdateAllergyRequest",
|
||||
description:
|
||||
"Partial edit of an allergy; an omitted key leaves the column untouched. A `null` `severity`/`onsetAt`/`reaction`/`note` clears that field. Rejects unknown keys.",
|
||||
});
|
||||
|
||||
allergyListQuerySchema.meta({
|
||||
const listAllergiesQuery = allergyListQuerySchema.meta({
|
||||
id: "ListAllergiesQuery",
|
||||
description:
|
||||
"Query params for the allergy list: optional `limit` (1–200, default 100) and `includeInactive` ('true' | 'false'); 'false' returns only ACTIVE records. Newest-first.",
|
||||
@@ -85,7 +89,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
summary: "List allergies (v1.25)",
|
||||
description:
|
||||
"Returns the caller's live (non-deleted) allergy/intolerance records, newest-first. `includeInactive=false` returns only ACTIVE records.",
|
||||
requestParams: { query: allergyListQuerySchema },
|
||||
requestParams: { query: listAllergiesQuery },
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
"200": {
|
||||
@@ -107,7 +111,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: allergyCreateSchema } },
|
||||
content: { "application/json": { schema: createAllergyRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -153,7 +157,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: { path: z.object({ id: z.string() }) },
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: allergyUpdateSchema } },
|
||||
content: { "application/json": { schema: updateAllergyRequest } },
|
||||
},
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
|
||||
@@ -14,7 +14,6 @@ import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import {
|
||||
changePasswordSchema,
|
||||
loginPasswordSchema,
|
||||
passkeyRenameSchema,
|
||||
registerSchema,
|
||||
} from "@/lib/validations/auth";
|
||||
@@ -36,7 +35,12 @@ import {
|
||||
stepUpMintSchema,
|
||||
stepUpOptionsSchema,
|
||||
} from "@/lib/validations/step-up";
|
||||
import { dataEnvelope, stdResponses, errorEnvelope } from "./shared";
|
||||
import {
|
||||
dataEnvelope,
|
||||
stdResponses,
|
||||
errorEnvelope,
|
||||
loginPasswordSchema,
|
||||
} from "./shared";
|
||||
|
||||
// ── Sub-schemas owned here (route-specific shapes) ───────────────────
|
||||
|
||||
|
||||
@@ -12,14 +12,12 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
|
||||
import {
|
||||
measurementSourceEnum,
|
||||
measurementTypeEnum,
|
||||
} from "@/lib/validations/measurement";
|
||||
import {
|
||||
MODULE_DISABLED_DESCRIPTION,
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
measurementSourceEnum,
|
||||
measurementTypeEnum,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
@@ -28,13 +28,17 @@ import {
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
createBiomarkerSchema.meta({
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document.
|
||||
const createBiomarkerRequest = createBiomarkerSchema.meta({
|
||||
id: "CreateBiomarkerRequest",
|
||||
description:
|
||||
"Define a user-scoped biomarker ONCE: canonical `name`, `unit`, optional reference bounds (`lowerBound` / `upperBound`; when both present `lowerBound` must not exceed `upperBound`), an optional encrypted `context` note, and an optional `panel` grouping. The name is unique per user (no second 'LDL'). Recording a value later just picks this marker — its unit + range are never re-entered.",
|
||||
});
|
||||
|
||||
updateBiomarkerSchema.meta({
|
||||
const updateBiomarkerRequest = updateBiomarkerSchema.meta({
|
||||
id: "UpdateBiomarkerRequest",
|
||||
description:
|
||||
"Partial edit of a biomarker. An omitted key leaves the column untouched; an explicit `null` on `context` / `panel` / a bound clears it. Set `hidden` to drop / restore the marker in the active list + lab-entry pickers. A rename that collides with another of the caller's markers is rejected 409.",
|
||||
@@ -106,7 +110,7 @@ export const biomarkerPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: createBiomarkerSchema } },
|
||||
content: { "application/json": { schema: createBiomarkerRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -153,7 +157,7 @@ export const biomarkerPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: { path: z.object({ id: z.string() }) },
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: updateBiomarkerSchema } },
|
||||
content: { "application/json": { schema: updateBiomarkerRequest } },
|
||||
},
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
|
||||
@@ -33,15 +33,12 @@ import { COACH_PLAN_STATUSES } from "@/lib/ai/coach/plans";
|
||||
import {
|
||||
COACH_PLAN_SCOPES,
|
||||
coachPlanPatchSchema,
|
||||
coachPlansListQuerySchema,
|
||||
} from "@/lib/validations/coach-plan";
|
||||
import { coachChatRequestSchema } from "@/lib/ai/coach/types";
|
||||
import {
|
||||
coachAttachmentCreateSchema,
|
||||
fencedChatRequestSchema,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
import { exportSelectionSchema } from "@/lib/validations/health-record-export";
|
||||
import { createShareLinkSchema } from "@/lib/validations/clinician-share-link";
|
||||
import { coachReminderSuggestionActionSchema } from "@/lib/validations/coach-reminder-suggestion";
|
||||
import { COACH_REMINDER_STATUSES } from "@/lib/ai/coach/reminders";
|
||||
import {
|
||||
@@ -71,13 +68,13 @@ const aboutMeAdoptRequest = aboutMeAdoptSchema.meta({
|
||||
"An answer to fold into the stored self-context. `question` is the clarifying question it belongs to and is what the server matches the target field from; omit it for the remember-a-message path and the field is matched from `answer` instead. `answer` is capped at 500 characters — the same cap as a structured self-context field.",
|
||||
});
|
||||
|
||||
emergencyProfileUpdateSchema.meta({
|
||||
const updateEmergencyProfileRequest = emergencyProfileUpdateSchema.meta({
|
||||
id: "UpdateEmergencyProfileRequest",
|
||||
description:
|
||||
"Partial edit of the emergency (Notfalldaten) profile. An omitted key leaves the column untouched; a `null` enum or an emptied free-text field clears it. `bloodType`, `organDonor` and `advanceDirective` are closed enums; `contacts`, `implants` and `note` are encrypted at rest. Rejects unknown keys.",
|
||||
});
|
||||
|
||||
emergencyProfileDtoSchema.meta({
|
||||
const emergencyProfile = emergencyProfileDtoSchema.meta({
|
||||
id: "EmergencyProfile",
|
||||
description:
|
||||
"The caller's emergency profile: three closed-enum facts plus three decrypted free-text fields. A free-text field is null when unset; its `*Unreadable` flag is true when ciphertext was present but could not be decrypted (a key-rotation gap, fail-soft rather than 500).",
|
||||
@@ -88,7 +85,7 @@ emergencyProfileDtoSchema.meta({
|
||||
// client sends ONLY the cadence id + the action; the server resolves the
|
||||
// metric + schedule + course window from the closed catalog and (for
|
||||
// `accept`) mints a `MeasurementReminder` with `origin: COACH`.
|
||||
coachReminderSuggestionActionSchema.meta({
|
||||
const coachReminderSuggestionAction = coachReminderSuggestionActionSchema.meta({
|
||||
id: "CoachReminderSuggestionAction",
|
||||
description:
|
||||
"v1.18.1 — act on a Coach cadence suggestion. `cadenceId` names a closed-catalog preset (e.g. `weight_daily`, `bp_7_2_2`); the client never sends a schedule. `action`: `accept` creates a `MeasurementReminder` (origin: COACH) through the same engine the Vorsorge surface uses; `dismiss` records dismissal memory; `stop` suppresses all future cadence suggestions. Strict: unknown keys 422.",
|
||||
@@ -130,7 +127,7 @@ export const coachReminderSuggestionPaths: NonNullable<
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: coachReminderSuggestionActionSchema },
|
||||
"application/json": { schema: coachReminderSuggestionAction },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -264,24 +261,6 @@ const insightsLayoutResult = insightsLayoutSchema
|
||||
"Resolved Insights layout plus the optimistic-concurrency `updatedAt` token.",
|
||||
});
|
||||
|
||||
// v1.7.0 — health-record export selection. Strict shape: unknown keys
|
||||
// (including any attempt to smuggle a userId) 422 via returnAllZodIssues.
|
||||
// v1.11.0 — clinician share-link create payload. Strict; no `userId` field
|
||||
// (the owner is always narrowed from the session/Bearer). `expiresAt` is
|
||||
// required and capped at SHARE_LINK_MAX_DAYS; the scope columns are frozen
|
||||
// write-once at creation.
|
||||
createShareLinkSchema.meta({
|
||||
id: "CreateShareLinkRequest",
|
||||
description:
|
||||
"v1.11.0 — owner request to mint a clinician share link to their own health record. `expiresAt` is required (absolute ISO instant) and capped at 90 days. `rangeStart`/`rangeEnd` freeze the reporting window (rangeEnd null = rolling). `sections` freezes which record domains the link may serve. A share link serves the rendered page and the documents frozen onto it — there is no FHIR or other machine-readable face behind a share token. Strict: unknown keys 422.",
|
||||
});
|
||||
|
||||
exportSelectionSchema.meta({
|
||||
id: "HealthRecordExportRequest",
|
||||
description:
|
||||
"v1.7.0 — health-record / doctor-handover export selection. `format` picks PDF, FHIR R4 document Bundle, or a combined zip package. Grouped `sections` toggles drive which domains are read (mood is opt-in, off by default). No `userId` field — the user is always narrowed from the session/Bearer. The route is strict: unknown keys 422.",
|
||||
});
|
||||
|
||||
// ── Coach facts (v1.11.1) ────────────────────────────────────────────
|
||||
// Read + delete surface for the durable facts the Coach extracts. Facts
|
||||
// are server-extracted, not user-authored, so there is no create/update
|
||||
@@ -396,17 +375,21 @@ const coachPlanDeletedResponse = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
coachPlanPatchSchema.meta({
|
||||
const coachPlanPatchRequest = coachPlanPatchSchema.meta({
|
||||
id: "CoachPlanPatchRequest",
|
||||
description:
|
||||
"v1.21.3 — confirm or update a Coach plan's lifecycle. `status` moves a `proposed` plan to `active` (confirm), or to `met` / `abandoned`. `reviewDate` pins (ISO instant) or clears (null) a check-in checkpoint. At least one field is required. Strict: unknown keys 422 — the body can never carry the metric, the encrypted text, or a userId.",
|
||||
});
|
||||
|
||||
coachPlansListQuerySchema.meta({
|
||||
id: "CoachPlansListQuery",
|
||||
description:
|
||||
"Optional `?status=` filter for the plans list (a single lifecycle status), or `?scope=` for a named group (open = proposed + active + review_due, past = met + abandoned + reviewed, all = every non-deleted plan). Mutually exclusive. Both omitted returns the non-terminal set (proposed + active).",
|
||||
});
|
||||
// `GET /api/coach/plans` publishes its two query parameters by hand below,
|
||||
// not through `requestParams.query`: the runtime schema carries a cross-field
|
||||
// `.refine()` (status and scope are mutually exclusive) that the expansion
|
||||
// cannot express, and the hand-written pair carries per-parameter prose it
|
||||
// would drop. A `CoachPlansListQuery` annotation sat here claiming a component
|
||||
// id that nothing referenced and that a query object could never reach anyway
|
||||
// — the emitter inlines query objects into `parameters`. Every clause of it is
|
||||
// already published on the operation and on both parameters, so it is gone
|
||||
// rather than left registering nothing.
|
||||
|
||||
// ── Coach episodic reminders (v1.22 B2/B6) ──────────────────────────────
|
||||
const coachReminderItem = z
|
||||
@@ -727,7 +710,7 @@ const coachAttachmentsResponseSchema = z
|
||||
})
|
||||
.meta({ id: "CoachAttachments" });
|
||||
|
||||
coachChatRequestSchema.meta({
|
||||
const coachChatRequest = coachChatRequestSchema.meta({
|
||||
id: "CoachChatRequest",
|
||||
description:
|
||||
"Inbound Coach turn. `message` is the user's turn (1–4 000 chars). `conversationId` is omitted to start a new conversation (the server mints a title from the first message) and supplied to continue one. `scope` narrows which metrics the snapshot ships and which window the timeline covers; omitted fields fall back to server defaults. `locale` picks the reply language. `guidedQuestion` carries the clarifying question a message answers (client-side bubble, never persisted). No `userId` field — the owner is narrowed from the session / Bearer.",
|
||||
@@ -804,7 +787,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: coachChatRequestSchema },
|
||||
"application/json": { schema: coachChatRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -1468,7 +1451,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
emergencyProfileDtoSchema,
|
||||
emergencyProfile,
|
||||
"GetEmergencyProfileResponse",
|
||||
),
|
||||
},
|
||||
@@ -1485,7 +1468,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: emergencyProfileUpdateSchema },
|
||||
"application/json": { schema: updateEmergencyProfileRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -1494,7 +1477,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
emergencyProfileDtoSchema,
|
||||
emergencyProfile,
|
||||
"UpdateEmergencyProfileResponse",
|
||||
),
|
||||
},
|
||||
@@ -1675,7 +1658,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: coachPlanPatchSchema },
|
||||
"application/json": { schema: coachPlanPatchRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
||||
@@ -26,18 +26,23 @@ import { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
consentKindEnum,
|
||||
consentPostBody,
|
||||
webConsentGrantBody,
|
||||
consentPostBody as consentPostBodyBase,
|
||||
webConsentGrantBody as webConsentGrantBodyBase,
|
||||
} from "@/lib/validations/consent";
|
||||
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
|
||||
consentPostBody.meta({
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document. The imports are aliased so the annotated clone can keep the
|
||||
// name every use site below already spells.
|
||||
const consentPostBody = consentPostBodyBase.meta({
|
||||
id: "ConsentPostBody",
|
||||
description:
|
||||
"Explicit AI-consent grant. `artefact` is an opaque signed receipt (base64 PDF or JWT, ≤ 64 KB UTF-8 bytes); `signedAt` is an ISO-8601 instant. Always appends a fresh row — re-granting after a revoke mints a new receipt.",
|
||||
});
|
||||
|
||||
webConsentGrantBody.meta({
|
||||
const webConsentGrantBody = webConsentGrantBodyBase.meta({
|
||||
id: "WebConsentGrantBody",
|
||||
description:
|
||||
'Optional. Omitting the body (or sending `intent: "heal"`) is the AI-settings mount heal: it mints only for an account with no `ai_full` consent history at all, so a standing revocation is never resurrected. `intent: "affirmative"` is the user\'s own grant action and may supersede an earlier revocation, exactly as a first grant would.',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -52,52 +52,56 @@ const cyclePhaseEnumOpenapi = z
|
||||
.enum(["MENSTRUAL", "FOLLICULAR", "OVULATORY", "LUTEAL"])
|
||||
.meta({ id: "CyclePhase" });
|
||||
|
||||
flowLevelEnum.meta({
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document.
|
||||
const flowLevelEnumOpenapi = flowLevelEnum.meta({
|
||||
id: "FlowLevel",
|
||||
description: "Menstrual-flow intensity.",
|
||||
});
|
||||
ovulationTestEnum.meta({
|
||||
const ovulationTestEnumOpenapi = ovulationTestEnum.meta({
|
||||
id: "OvulationTest",
|
||||
description: "Ovulation predictor-kit (OPK) reading.",
|
||||
});
|
||||
cervicalMucusEnum.meta({
|
||||
const cervicalMucusEnumOpenapi = cervicalMucusEnum.meta({
|
||||
id: "CervicalMucus",
|
||||
description: "Cervical-mucus quality.",
|
||||
});
|
||||
homeTestResultEnum.meta({
|
||||
const homeTestResultEnumOpenapi = homeTestResultEnum.meta({
|
||||
id: "HomeTestResult",
|
||||
description: "At-home test result (pregnancy / progesterone).",
|
||||
});
|
||||
cycleTrackingGoalEnum.meta({
|
||||
const cycleTrackingGoalEnumOpenapi = cycleTrackingGoalEnum.meta({
|
||||
id: "CycleTrackingGoal",
|
||||
description: "Drives cycle copy + fertile-window gating.",
|
||||
});
|
||||
|
||||
cycleDayLogInputSchema.meta({
|
||||
const cycleDayLogInput = cycleDayLogInputSchema.meta({
|
||||
id: "CycleDayLogInput",
|
||||
description:
|
||||
"One day's cycle capture. `note` is encrypted at rest; every other field is queryable plaintext. UPSERT key: `(userId, source, externalId)` when externalId present, else `(userId, date)`. Shared by the single POST, the bulk drain, and the period shortcut.",
|
||||
});
|
||||
|
||||
cycleBulkSchema.meta({
|
||||
const cycleDayLogBulkRequest = cycleBulkSchema.meta({
|
||||
id: "CycleDayLogBulkRequest",
|
||||
description:
|
||||
"Outbox / HealthKit drain. Up to 500 entries per call; wrapped in `withIdempotency`; rate-limited 60/min. Each entry upserts per the day-log key.",
|
||||
});
|
||||
|
||||
cyclePeriodSchema.meta({
|
||||
const cyclePeriodRequest = cyclePeriodSchema.meta({
|
||||
id: "CyclePeriodRequest",
|
||||
description:
|
||||
"One-tap period boundary. `start` opens a new cycle (closing the prior), `end` stamps the current cycle's periodEndDate; both write a boundary day-log.",
|
||||
});
|
||||
|
||||
cyclePrefsSchema.meta({
|
||||
const cyclePrefsRequest = cyclePrefsSchema.meta({
|
||||
id: "CyclePrefsRequest",
|
||||
description:
|
||||
"Partial cycle-preferences deep-merge. `enabled` flips the feature gate (`cycleTrackingEnabled`). Omitted fields are left untouched.",
|
||||
});
|
||||
|
||||
cycleDayLogPatchSchema.meta({
|
||||
const cycleDayLogPatchRequest = cycleDayLogPatchSchema.meta({
|
||||
id: "CycleDayLogPatchRequest",
|
||||
description:
|
||||
"Partial day-log edit. Every field optional; `note` re-encrypts (explicit null clears it). `date` / `source` / `externalId` are immutable on update.",
|
||||
@@ -113,19 +117,19 @@ const cycleDayLogDto = z
|
||||
id: z.string(),
|
||||
date: z.string(),
|
||||
cycleId: z.string().nullable(),
|
||||
flow: flowLevelEnum.nullable(),
|
||||
flow: flowLevelEnumOpenapi.nullable(),
|
||||
intermenstrualBleeding: z.boolean(),
|
||||
basalBodyTempC: z.number().nullable(),
|
||||
temperatureExcluded: z.boolean(),
|
||||
ovulationTest: ovulationTestEnum.nullable(),
|
||||
cervicalMucus: cervicalMucusEnum.nullable(),
|
||||
ovulationTest: ovulationTestEnumOpenapi.nullable(),
|
||||
cervicalMucus: cervicalMucusEnumOpenapi.nullable(),
|
||||
cervixPosition: cervixPositionEnum.nullable(),
|
||||
cervixFirmness: cervixFirmnessEnum.nullable(),
|
||||
cervixOpening: cervixOpeningEnum.nullable(),
|
||||
sexualActivity: z.boolean(),
|
||||
protectedSex: z.boolean().nullable(),
|
||||
pregnancyTest: homeTestResultEnum.nullable(),
|
||||
progesteroneTest: homeTestResultEnum.nullable(),
|
||||
pregnancyTest: homeTestResultEnumOpenapi.nullable(),
|
||||
progesteroneTest: homeTestResultEnumOpenapi.nullable(),
|
||||
contraceptive: z.string().nullable(),
|
||||
symptoms: z.array(cycleSymptomDto),
|
||||
note: z.string().nullable(),
|
||||
@@ -191,7 +195,7 @@ const cycleCalendarDayDto = z.object({
|
||||
description:
|
||||
"Whether a logged cycle opens on this day. Deleting this day's log removes that cycle start with it.",
|
||||
}),
|
||||
flow: flowLevelEnum.nullable(),
|
||||
flow: flowLevelEnumOpenapi.nullable(),
|
||||
hasSymptoms: z.boolean(),
|
||||
confidence: z.number(),
|
||||
// v1.15.0 — logged basal-body-temperature + fertility-sign markers, surfaced
|
||||
@@ -199,8 +203,8 @@ const cycleCalendarDayDto = z.object({
|
||||
// loaded server-side for the symptothermal layer; no extra query).
|
||||
basalBodyTempC: z.number().nullable(),
|
||||
temperatureExcluded: z.boolean(),
|
||||
ovulationTest: ovulationTestEnum.nullable(),
|
||||
cervicalMucus: cervicalMucusEnum.nullable(),
|
||||
ovulationTest: ovulationTestEnumOpenapi.nullable(),
|
||||
cervicalMucus: cervicalMucusEnumOpenapi.nullable(),
|
||||
cervixPosition: cervixPositionEnum.nullable(),
|
||||
cervixFirmness: cervixFirmnessEnum.nullable(),
|
||||
cervixOpening: cervixOpeningEnum.nullable(),
|
||||
@@ -247,7 +251,7 @@ const cycleVerdictDto = z
|
||||
|
||||
const cycleProfileDto = z
|
||||
.object({
|
||||
goal: cycleTrackingGoalEnum,
|
||||
goal: cycleTrackingGoalEnumOpenapi,
|
||||
cycleTrackingEnabled: z.boolean(),
|
||||
secondarySymptom: secondarySymptomEnum,
|
||||
rawChartMode: z.boolean(),
|
||||
@@ -266,7 +270,7 @@ const cycleProfileDto = z
|
||||
|
||||
const cycleCalendarResponse = z.object({
|
||||
profile: z.object({
|
||||
goal: cycleTrackingGoalEnum,
|
||||
goal: cycleTrackingGoalEnumOpenapi,
|
||||
rawChartMode: z.boolean(),
|
||||
predictionEnabled: z.boolean(),
|
||||
cyclesObserved: z.number().int(),
|
||||
@@ -444,10 +448,6 @@ const cycleDisabledOnADelegableRoute = recordRefusal(
|
||||
// rest and never reaches a wide event or an audit excerpt — only the icon and
|
||||
// the minted key do.
|
||||
|
||||
// `.meta()` RETURNS a new schema in Zod 4 rather than annotating in place, so
|
||||
// the result has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing, and the component id it names never reaches
|
||||
// the document.
|
||||
const createCycleCustomSymptomRequest = createCustomSymptomSchema.meta({
|
||||
id: "CreateCycleCustomSymptomRequest",
|
||||
description:
|
||||
@@ -682,7 +682,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: cycleDayLogInputSchema } },
|
||||
content: { "application/json": { schema: cycleDayLogInput } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -719,7 +719,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: { path: z.object({ id: z.string() }) },
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: cycleDayLogPatchSchema } },
|
||||
content: { "application/json": { schema: cycleDayLogPatchRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -764,7 +764,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: cycleBulkSchema } },
|
||||
content: { "application/json": { schema: cycleDayLogBulkRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -790,7 +790,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: cyclePeriodSchema } },
|
||||
content: { "application/json": { schema: cyclePeriodRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -979,7 +979,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"Deep-merges the supplied fields. `enabled` flips `cycleTrackingEnabled`. Returns the merged CycleProfileDTO. NOT gated.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: cyclePrefsSchema } },
|
||||
content: { "application/json": { schema: cyclePrefsRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -41,49 +41,53 @@ import {
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
encounterCreateSchema.meta({
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document.
|
||||
const createEncounterRequest = encounterCreateSchema.meta({
|
||||
id: "CreateEncounterRequest",
|
||||
description:
|
||||
"File a visit, or book one. `occurredAt` is the only required field — a visit saves with a date and nothing else. A future instant with `status: PLANNED` books an appointment and mints exactly one reminder for it. `reason` and `outcome` are encrypted at rest. The three id arrays pre-link documents, lab results and condition episodes; an id naming nothing the caller owns is dropped rather than refused, so a link never blocks a save.",
|
||||
});
|
||||
|
||||
encounterUpdateSchema.meta({
|
||||
const updateEncounterRequest = encounterUpdateSchema.meta({
|
||||
id: "UpdateEncounterRequest",
|
||||
description:
|
||||
"Partial edit of a visit; an omitted key leaves the column untouched, and a body naming nothing is a 422. Moving `occurredAt` re-anchors the appointment reminder the visit already owns and never mints a second. Moving `status` to DONE closes the checkup named by `reminderId`, on the transition only. CANCELLED and NO_SHOW stop the reminder without deleting the row. A present id array replaces that link family, empty array included.",
|
||||
});
|
||||
|
||||
encounterListQuerySchema.meta({
|
||||
const listEncountersQuery = encounterListQuerySchema.meta({
|
||||
id: "ListEncountersQuery",
|
||||
description:
|
||||
"Window and filters for the visit list: `from` / `to` (ISO-8601 instants), `status`, `practitionerId`, `episodeId` (only visits filed against that condition episode — the condition side of the link, and the only direction it is offered in), and `limit` (1–200, default 100).",
|
||||
});
|
||||
|
||||
encounterSuggestQuerySchema.meta({
|
||||
const suggestEncounterQuery = encounterSuggestQuerySchema.meta({
|
||||
id: "SuggestEncounterQuery",
|
||||
description:
|
||||
"The date to resolve around, as an ISO-8601 instant with an offset. A document uses `reportDate ?? documentDate`; a lab panel uses `takenAt`.",
|
||||
});
|
||||
|
||||
encounterLinkSchema.meta({
|
||||
const encounterLinkRequest = encounterLinkSchema.meta({
|
||||
id: "EncounterLinkRequest",
|
||||
description:
|
||||
"Targets to file against, or unfile from, a visit. Idempotent in both directions and capped at 100 ids. Ids naming nothing the caller owns come back in `unknown` rather than failing the request.",
|
||||
});
|
||||
|
||||
practitionerCreateSchema.meta({
|
||||
const createPractitionerRequest = practitionerCreateSchema.meta({
|
||||
id: "CreatePractitionerRequest",
|
||||
description:
|
||||
"Add a doctor or practice to the caller's own address book. `name` is the only required field and is stored as queryable plaintext so the picker can search and sort it; `note` is encrypted at rest. `specialty` is free text rather than an enum. Nothing is unique across accounts.",
|
||||
});
|
||||
|
||||
practitionerUpdateSchema.meta({
|
||||
const updatePractitionerRequest = practitionerUpdateSchema.meta({
|
||||
id: "UpdatePractitionerRequest",
|
||||
description:
|
||||
"Partial edit; an omitted key leaves the column untouched, an explicit null clears it, and a body naming nothing is a 422.",
|
||||
});
|
||||
|
||||
practitionerListQuerySchema.meta({
|
||||
const listPractitionersQuery = practitionerListQuerySchema.meta({
|
||||
id: "ListPractitionersQuery",
|
||||
description:
|
||||
"Query params for the address book: `q` (case-insensitive substring of the name OR the practice — a person looks one up by whichever of the two they remember) and `limit` (1–200, default 100). Name-ordered.",
|
||||
@@ -233,7 +237,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
summary: "List visits",
|
||||
description:
|
||||
"Returns the caller's live visits, split into `upcoming` and `past`. Bounded and windowed; soft-deleted visits are excluded.",
|
||||
requestParams: { query: encounterListQuerySchema },
|
||||
requestParams: { query: listEncountersQuery },
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
"200": {
|
||||
@@ -255,7 +259,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"Creates one visit. A future instant with `status: PLANNED` books an appointment and mints its reminder; a past instant with `status: DONE` and a `reminderId` closes that checkup. Claiming both is a 422 — a visit that has not happened cannot have closed anything. Honours `Idempotency-Key`. Audits as `encounter.visit.create`.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: encounterCreateSchema } },
|
||||
content: { "application/json": { schema: createEncounterRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -278,7 +282,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
summary: "Which visit does a record dated around this day belong to?",
|
||||
description:
|
||||
"Resolves the caller's live DONE or PLANNED visits within ±7 days of `anchor` into a verdict, so the three capture moments that ask the question — a document arriving, a lab panel committed from an extraction, a lab panel typed by hand — all read the same answer. `one` means pre-select it visibly with one undo; `many` means offer a picker with NOTHING pre-selected, because pre-selecting the first of two teaches a person to stop reading suggestions; `none` means offer nothing at all. Cancelled visits and no-shows are never candidates: neither happened, so neither produced the record being filed. No AI provider is involved and none is required.",
|
||||
requestParams: { query: encounterSuggestQuerySchema },
|
||||
requestParams: { query: suggestEncounterQuery },
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
"200": {
|
||||
@@ -325,7 +329,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: idPath,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: encounterUpdateSchema } },
|
||||
content: { "application/json": { schema: updateEncounterRequest } },
|
||||
},
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
@@ -396,7 +400,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: idPath,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: encounterLinkSchema } },
|
||||
content: { "application/json": { schema: encounterLinkRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -425,7 +429,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: idPath,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: encounterLinkSchema } },
|
||||
content: { "application/json": { schema: encounterLinkRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -453,7 +457,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
summary: "List practitioners",
|
||||
description:
|
||||
"Returns the caller's address book, name-ordered, with an optional case-insensitive name search.",
|
||||
requestParams: { query: practitionerListQuerySchema },
|
||||
requestParams: { query: listPractitionersQuery },
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
"200": {
|
||||
@@ -478,7 +482,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"Creates one address-book entry. Honours `Idempotency-Key`. Audits as `practitioner.contact.create`.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: practitionerCreateSchema } },
|
||||
content: { "application/json": { schema: createPractitionerRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
@@ -524,7 +528,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: idPath,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: practitionerUpdateSchema } },
|
||||
content: { "application/json": { schema: updatePractitionerRequest } },
|
||||
},
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -19,6 +19,32 @@ import {
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
// The two request bodies this module publishes. `.meta()` CLONES in Zod 4
|
||||
// rather than annotating in place, so the returned schema has to be captured
|
||||
// and referenced: a bare `schema.meta({...})` statement registers nothing and
|
||||
// the component id it names never reaches the emitted document. Both
|
||||
// annotations used to sit in `./coach`, which imported the schemas for no
|
||||
// other reason and never referenced them; the two operations that DO send
|
||||
// these bodies are both here.
|
||||
//
|
||||
// v1.7.0 — health-record export selection. Strict shape: unknown keys
|
||||
// (including any attempt to smuggle a userId) 422 via returnAllZodIssues.
|
||||
// v1.11.0 — clinician share-link create payload. Strict; no `userId` field
|
||||
// (the owner is always narrowed from the session/Bearer). `expiresAt` is
|
||||
// required and capped at SHARE_LINK_MAX_DAYS; the scope columns are frozen
|
||||
// write-once at creation.
|
||||
const createShareLinkRequest = createShareLinkSchema.meta({
|
||||
id: "CreateShareLinkRequest",
|
||||
description:
|
||||
"v1.11.0 — owner request to mint a clinician share link to their own health record. `expiresAt` is required (absolute ISO instant) and capped at 90 days. `rangeStart`/`rangeEnd` freeze the reporting window (rangeEnd null = rolling). `selection` freezes which record leaves the link may serve, and omitting it means an empty scope rather than a default one. `documentIds` freezes the documents the link carries; `documentOnly` mints a share that serves those documents and no record scope at all. A share link serves the rendered page and the documents frozen onto it — there is no FHIR or other machine-readable face behind a share token. Strict: unknown keys 422.",
|
||||
});
|
||||
|
||||
const healthRecordExportRequest = exportSelectionSchema.meta({
|
||||
id: "HealthRecordExportRequest",
|
||||
description:
|
||||
"v1.7.0 — health-record / doctor-handover export selection. `format` picks PDF, FHIR R4 document Bundle, or a combined zip package. `selection` is REQUIRED and carries the leaf inclusion list — membership is inclusion, absence is exclusion, and there is no server-side default; the retired grouped `sections` shape is a 422. No `userId` field — the user is always narrowed from the session/Bearer. The route is strict: unknown keys 422.",
|
||||
});
|
||||
|
||||
// v1.11.0 — clinician share-link owner-facing summary (never the raw token).
|
||||
const shareLinkSummary = z.object({
|
||||
id: z.string(),
|
||||
@@ -91,7 +117,7 @@ export const healthRecordPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: exportSelectionSchema },
|
||||
"application/json": { schema: healthRecordExportRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -124,7 +150,7 @@ export const healthRecordPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: createShareLinkSchema },
|
||||
"application/json": { schema: createShareLinkRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
glucoseContextEnum,
|
||||
measurementTypeEnum,
|
||||
} from "@/lib/validations/measurement";
|
||||
import { glucoseContextEnum } from "@/lib/validations/measurement";
|
||||
import { moodLevelEnum } from "@/lib/validations/mood";
|
||||
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
import {
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
measurementTypeEnum,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
const csvImportRowResult = z
|
||||
.object({
|
||||
|
||||
@@ -72,7 +72,7 @@ import {
|
||||
} from "./medications";
|
||||
import { mcpPaths } from "./mcp";
|
||||
import { metaPaths } from "./meta";
|
||||
import { moodPaths } from "./mood";
|
||||
import { moodPaths, moodTagLayout } from "./mood";
|
||||
import { notificationTransportPaths } from "./notifications-transport";
|
||||
import { notificationChannelPaths } from "./notifications";
|
||||
import { nutrientPaths } from "./nutrients";
|
||||
@@ -82,7 +82,7 @@ import { recordSettingsPaths } from "./record-settings";
|
||||
import { settingsPaths } from "./settings";
|
||||
import { syncPaths } from "./sync";
|
||||
import { workoutPaths } from "./workouts";
|
||||
import { coachPrefsSchema } from "@/lib/validations/coach-prefs";
|
||||
import { coachPrefsSchema } from "./shared";
|
||||
|
||||
export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
...cyclePaths,
|
||||
@@ -183,6 +183,11 @@ export const openApiComponents: NonNullable<ZodOpenApiObject["components"]> = {
|
||||
// this forced registration the schema the description points to would be
|
||||
// silently absent from the published spec.
|
||||
//
|
||||
// `MoodTagLayout` joins them for exactly the same reason: its annotation is
|
||||
// in the assigning form and correct, but both consumers `.extend()` it, so
|
||||
// the base name is never referenced and the description the mood surface
|
||||
// relies on would go unpublished.
|
||||
//
|
||||
// `CoachPrefs` is the same shape: the PUT `/api/auth/me/coach-prefs`
|
||||
// description tells the reader "Body is validated against `CoachPrefs`",
|
||||
// but every actual consumer is a `.extend()` of it (`PutCoachPrefsRequest`,
|
||||
@@ -193,6 +198,7 @@ export const openApiComponents: NonNullable<ZodOpenApiObject["components"]> = {
|
||||
MedicationDoseHistoryImportFatalReason:
|
||||
medicationDoseHistoryImportFatalReasonEnum,
|
||||
CoachPrefs: coachPrefsSchema,
|
||||
MoodTagLayout: moodTagLayout,
|
||||
},
|
||||
// v1.36.0 — the per-request account selector, defined in the sharing route
|
||||
// module so that `src/__tests__/acting-account-boundary-guard.test.ts` keeps
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
* runtime request parsing, so the wire contract stays single-source.
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
measurementTypeEnum,
|
||||
measurementSourceEnum,
|
||||
} from "@/lib/validations/measurement";
|
||||
import { measurementSourceEnum, measurementTypeEnum } from "../shared";
|
||||
import { METRIC_STATUS_IDS } from "@/lib/insights/metric-status-registry";
|
||||
import {
|
||||
DERIVED_METRIC_IDS,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -7,21 +7,19 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import {
|
||||
createMeasurementSchema,
|
||||
listMeasurementsSchema,
|
||||
measurementTypeEnum,
|
||||
updateMeasurementSchema,
|
||||
measurementSourceEnum,
|
||||
} from "@/lib/validations/measurement";
|
||||
import { updateMeasurementSchema } from "@/lib/validations/measurement";
|
||||
import { seriesBatchQuerySchema } from "@/lib/validations/series-batch";
|
||||
import { deviceTypeEnum } from "@/lib/validations/source-priority";
|
||||
import {
|
||||
MODULE_DISABLED_DESCRIPTION,
|
||||
createMeasurementSchema,
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
idempotencyKeyParameter,
|
||||
idempotentWrite,
|
||||
listMeasurementsSchema,
|
||||
measurementSourceEnum,
|
||||
measurementTypeEnum,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,15 @@ 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({
|
||||
/**
|
||||
* Exported for the forced registration in `./index.ts`.
|
||||
*
|
||||
* Both consumers are an `.extend()` of this base, and `.extend()` builds a new
|
||||
* object rather than a reference, so the name never carries forward on its own
|
||||
* even though the annotation is in the assigning form. Same shape as
|
||||
* `CoachPrefs` and `Medication`, and the same remedy.
|
||||
*/
|
||||
export 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 +229,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 +1358,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 +1385,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 +1438,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 +1469,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 +1496,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": {
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
ocrCommitSchema.meta({
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced. A bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document.
|
||||
const ocrCommitRequest = ocrCommitSchema.meta({
|
||||
id: "OcrCommitRequest",
|
||||
description:
|
||||
"The rows a human confirmed on the Lab-OCR review screen. Each row is EITHER numeric (`value` + `unit`, optional reference bounds) OR qualitative (`valueText`) — exactly one. `analyte` drives a resolve-or-mint of the user-scoped biomarker; `takenAt` is a backdatable ISO instant. No `userId` field — it is narrowed from the session. 1..100 rows. The route skips a row that duplicates a live reading (same analyte + day + value). The optional `encounterId` files the panel against a visit the caller owns — ONE link per written result row, so a marker re-run on a different day belongs to its own visit; always optional, and a link that could not be made never fails the commit.",
|
||||
@@ -209,7 +213,7 @@ export const ocrPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
'Writes ONLY the rows the human confirmed on the review screen. Each row resolves-or-mints a user-scoped biomarker and creates a `LabResult` with `source: "OCR"`; a row that duplicates a live reading is skipped. Idempotent (Idempotency-Key). Audits as `labs.ocr.commit`. `userId` is narrowed from the session, never a body field.',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: ocrCommitSchema } },
|
||||
content: { "application/json": { schema: ocrCommitRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { coachPrefsSchema } from "@/lib/validations/coach-prefs";
|
||||
import { notificationPrefsSchema } from "@/lib/validations/notification-prefs";
|
||||
import { sourcePrioritySchema } from "@/lib/validations/source-priority";
|
||||
import { modulePrefsPatchSchema } from "@/lib/validations/modules";
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
healthProfileFactKindSchema,
|
||||
} from "@/lib/validations/health-profile-facts";
|
||||
import {
|
||||
coachPrefsSchema,
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
stdResponses,
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
createMeasurementSchema,
|
||||
listMeasurementsSchema,
|
||||
measurementTypeEnum,
|
||||
measurementSourceEnum,
|
||||
createMeasurementSchema as createMeasurementSchemaBase,
|
||||
listMeasurementsSchema as listMeasurementsSchemaBase,
|
||||
measurementTypeEnum as measurementTypeEnumBase,
|
||||
measurementSourceEnum as measurementSourceEnumBase,
|
||||
} from "@/lib/validations/measurement";
|
||||
import { loginPasswordSchema } from "@/lib/validations/auth";
|
||||
import { coachPrefsSchema } from "@/lib/validations/coach-prefs";
|
||||
import { createBatchWorkoutSchema } from "@/lib/validations/workout";
|
||||
import { loginPasswordSchema as loginPasswordSchemaBase } from "@/lib/validations/auth";
|
||||
import { coachPrefsSchema as coachPrefsSchemaBase } from "@/lib/validations/coach-prefs";
|
||||
import { createBatchWorkoutSchema as createBatchWorkoutSchemaBase } from "@/lib/validations/workout";
|
||||
|
||||
/**
|
||||
* Common envelopes — every HealthLog API response wraps payload in
|
||||
@@ -48,44 +48,53 @@ export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
}
|
||||
|
||||
// ── Schemas — annotated for spec emission ────────────────────────────
|
||||
//
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
// schema has to be captured and referenced: a bare `schema.meta({...})`
|
||||
// statement registers nothing and the component id it names never reaches the
|
||||
// emitted document. These seven are shared across route modules, so the
|
||||
// annotated clone is exported from here under the name the use sites already
|
||||
// spell and the base import is aliased. A module that wants the published
|
||||
// form imports it from `./shared`; importing the raw schema from
|
||||
// `@/lib/validations/*` instead is what inlined these anonymously.
|
||||
|
||||
measurementTypeEnum.meta({
|
||||
export const measurementTypeEnum = measurementTypeEnumBase.meta({
|
||||
id: "MeasurementType",
|
||||
description:
|
||||
"DB-stored measurement category. v1.4.23 added 7 Apple Health values (HRV, resting HR, active energy, flights, walking/running distance, VO2 max, body temperature).",
|
||||
});
|
||||
|
||||
measurementSourceEnum.meta({
|
||||
export const measurementSourceEnum = measurementSourceEnumBase.meta({
|
||||
id: "MeasurementSource",
|
||||
description:
|
||||
"Origin of the measurement. v1.4.23 added APPLE_HEALTH for the iOS HealthKit batch ingest path.",
|
||||
});
|
||||
|
||||
loginPasswordSchema.meta({
|
||||
export const loginPasswordSchema = loginPasswordSchemaBase.meta({
|
||||
id: "LoginPasswordRequest",
|
||||
description:
|
||||
"Email-or-username login. The native-client flow returns a paired access + refresh token when X-Client-Type: native or the iOS UA prefix is present.",
|
||||
});
|
||||
|
||||
createMeasurementSchema.meta({
|
||||
export const createMeasurementSchema = createMeasurementSchemaBase.meta({
|
||||
id: "CreateMeasurementRequest",
|
||||
description:
|
||||
"Single-measurement ingest body. Plausibility-range guard runs server-side; out-of-range values fail 422. `glucoseContext` stays REQUIRED on a `BLOOD_GLUCOSE` row here: this is the hand-entry surface, where the person taking the reading knows whether it was fasting or after a meal. The bulk ingest paths (CSV import, JSON import, device sync) accept a contextless reading, because a sensor export classifies nothing per sample.",
|
||||
});
|
||||
|
||||
listMeasurementsSchema.meta({
|
||||
export const listMeasurementsSchema = listMeasurementsSchemaBase.meta({
|
||||
id: "ListMeasurementsQuery",
|
||||
description:
|
||||
"Query params for the measurements list endpoint. `limit` capped at 500.",
|
||||
});
|
||||
|
||||
coachPrefsSchema.meta({
|
||||
export const coachPrefsSchema = coachPrefsSchemaBase.meta({
|
||||
id: "CoachPrefs",
|
||||
description:
|
||||
"Per-user Coach prompt-tuning preferences (v1.4.23 H4). All fields default to the legacy v1.4.22 behaviour when omitted.",
|
||||
});
|
||||
|
||||
createBatchWorkoutSchema.meta({
|
||||
export const createBatchWorkoutSchema = createBatchWorkoutSchemaBase.meta({
|
||||
id: "CreateBatchWorkoutRequest",
|
||||
description:
|
||||
"Typed workout batch ingest. Each entry is an HKWorkout-aligned record with an optional nested GeoJSON LineString route AND an optional route-independent per-workout heart-rate series (`samples`: `[{ t, hr?, speedMs?, power?, cadence? }]`, up to 30 000 points). The `samples` series is the strain-engine input for indoor workouts that have no GPS route. Up to 100 workouts per call; nested route geometry capped at 20 000 points. Withings server-to-server callers pass source: WITHINGS and ship no route (Withings reports aggregates only).",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { measurementSourceEnum } from "@/lib/validations/measurement";
|
||||
import { createBatchWorkoutSchema } from "@/lib/validations/workout";
|
||||
import {
|
||||
MODULE_DISABLED_DESCRIPTION,
|
||||
createBatchWorkoutSchema,
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
idempotencyKeyParameter,
|
||||
idempotentWrite,
|
||||
measurementSourceEnum,
|
||||
moduleDisabledResponse,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
|
||||
Reference in new issue
Block a user