Publish the nineteen routes the drift check could not see

`openapi:check` compares the registry against the committed YAML, so a route
that was never registered produced no drift and no failure. These nineteen
had shipped without a contract: the health probe, the dashboard analytics
aggregate, the profile summary, personal records, the badge grid, the whole
environmental-context module, both onboarding writes, the Apple Health import
pair, the external medication ingest, the self-context adopt, the AI-provider
update and the AI connection probe.

Each entry is read off the route rather than inferred from a neighbour, so it
carries what a client cannot guess: that `/api/health` answers a bare object
and not the envelope, that `/api/analytics` records a score row and mints
pattern ids on a GET, that `/api/ai/test` reports a provider failure as 200
with `ok: false` because a 5xx comes back as somebody's HTML error page, that
the ingest route replays with 200 and creates with 201, and that a home
location re-stamps its effective-from instant on every write.

Three route-local request schemas move to `src/lib/validations/` so the
published shape and the runtime parse are one object rather than two copies —
a route module may only export handlers, which is why they could not be
imported where they were. The AI-provider PATCH has no schema to move: it
hand-rolls its parse field by field, so its entry says so and spells out that
a wrongly-typed key is skipped in silence.

The generated YAML is deliberately not in this commit.
This commit is contained in:
Marc-André Bombeck committed 2026-08-22 12:54:43 +02:00
1 parent 1c2e6c5c80
commit 49da15feaa
15 files changed
+2097 -68

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
/**
* Request schema for `POST /api/ai/test` — the AI connection probe.
*
* Lives outside the route file so the OpenAPI registry can import it (a route
* module may only export handlers plus the Next.js route config), which keeps
* the published contract and the runtime parser one object.
*
* The body carries UNSAVED credentials on purpose: the settings surface tests
* a selection before it is persisted. Nothing here is written to the user row
* — `resolveProviderForTest` layers the override over the stored config for
* the duration of the one probe and drops it.
*/
import { z } from "zod/v4";
/** The five provider kinds a user may select. */
export const AI_PROVIDER_KINDS = [
"OPENAI",
"ANTHROPIC",
"LOCAL",
"OPENAI_COMPATIBLE",
"CHATGPT_OAUTH",
] as const;
export const aiProviderKindSchema = z.enum(AI_PROVIDER_KINDS);
export const aiTestOverrideSchema = z
.object({
provider: aiProviderKindSchema.optional().nullable(),
model: z.string().min(1).max(120).optional().nullable(),
baseUrl: z.string().url().max(2048).optional().nullable(),
anthropicKey: z.string().min(1).max(500).optional().nullable(),
localKey: z.string().min(1).max(500).optional().nullable(),
openaiKey: z.string().min(1).max(500).optional().nullable(),
// The OpenAI-compatible gateway's own fields, kept separate from
// `baseUrl` / `openaiKey` so testing an unsaved gateway config can never
// reach the pinned OpenAI arm and vice versa.
compatBaseUrl: z.string().url().max(2048).optional().nullable(),
compatKey: z.string().min(1).max(500).optional().nullable(),
compatModel: z.string().min(1).max(120).optional().nullable(),
})
.strict();
export type AiTestOverrideInput = z.infer<typeof aiTestOverrideSchema>;
+56
View File
@@ -0,0 +1,56 @@
/**
* Request schemas for the two onboarding write endpoints.
*
* They live outside the route files for the same reason
* `src/lib/validations/about-me.ts` does: a route module may only export
* handlers plus the Next.js route config, so the OpenAPI registry cannot
* import a schema declared inside one. Keeping the shapes here lets the
* published contract and the runtime parser be the same object rather than
* two hand-kept copies.
*
* Prisma-free by construction — `@/lib/onboarding/goals` is pure data — so
* the generator script can pull them in without dragging the server graph
* along.
*/
import { z } from "zod/v4";
import { ONBOARDING_GOAL_SLUGS } from "@/lib/onboarding/goals";
/**
* `POST /api/onboarding/step` — the step-by-step wizard checkpoint.
*
* `step` is the step being COMPLETED and must equal the stored step plus one;
* the route refuses anything else with 409 rather than clamping. `goals`
* rides the step-2 submit and is validated against the closed slug set, so an
* unknown slug fails the whole request instead of being silently dropped.
*/
export const onboardingStepSchema = z.object({
step: z.number().int().min(1).max(4),
goals: z
.array(z.enum(ONBOARDING_GOAL_SLUGS))
.max(ONBOARDING_GOAL_SLUGS.length)
.optional(),
});
export type OnboardingStepInput = z.infer<typeof onboardingStepSchema>;
/**
* `POST /api/onboarding/complete` — the legacy single-shot completion path.
*
* Every field is optional: the endpoint's job is the completion stamp, and
* the profile fields are whatever the wizard happened to collect. A field
* that parses but is falsy (an empty `displayName`, `heightCm: 0`) is not
* written — the route only assigns truthy values.
*/
export const onboardingCompleteSchema = z.object({
displayName: z.string().trim().min(1).max(50).optional(),
heightCm: z.number().min(50).max(300).optional(),
dateOfBirth: z.string().optional(),
// The same three values the profile schema stores. A narrower enum here
// rejected the whole onboarding submission over a field the account is
// allowed to hold, and left the value unrecordable until the person found
// the setting again afterwards.
gender: z.enum(["MALE", "FEMALE", "OTHER"]).optional(),
});
export type OnboardingCompleteInput = z.infer<typeof onboardingCompleteSchema>;