Files
HealthLog/src/lib/openapi/registry.ts
T
Marc-André BombeckandClaude Opus 4.8 5f485ca780 feat(cycle): add cycle-tracking API surface + offline sync
Add the `/api/cycle/*` capture, calendar, history, and settings routes
plus the offline-sync delta extension for the v1.15 cycle-tracking
feature, all against the locked iOS contract.

- Enable flag: add nullable `cycleTrackingEnabled` to CycleProfile
  (migration 0132). A null toggle derives from gender (FEMALE → on).
  `isCycleEnabled()` is the single gate resolver; every `/api/cycle/*`
  route 403s `{ errorCode: "cycle.disabled" }` when off. The resolved
  flag surfaces additively on GET /api/auth/me.
- Routes (apiHandler + Zod + envelope + gate + annotate + auditLog):
  POST/PATCH/DELETE day-logs (single), POST day-logs/bulk (idempotent,
  capped 500, rate-limited 60/min, per-entry inserted/updated/duplicate/
  skipped), POST period boundary, GET calendar (runs the deterministic
  engine, debounced stale-while-revalidate cache, goal-gated fertile
  window), GET cycles (history + median/MAD stats), GET profile,
  DELETE cycles, PATCH /api/auth/me/cycle-prefs (deep-merge).
- Day-log note encrypts at rest (notesEncrypted); intent fields stay
  queryable plaintext. Upsert keys on (userId, source, externalId) when
  externalId present, else (userId, date).
- Offline sync: extend GET /api/sync/changes + the cursor with the
  `cycleDays` + `cycles` domains (CURSOR_VERSION → 2). cycleDays
  tombstones key on externalId else id; cycles on id.
- OpenAPI: register every route + DTO; regenerate the committed spec.
- i18n: cycle prediction disclaimer across all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 08:57:12 +02:00

90 lines
3.7 KiB
TypeScript

/**
* OpenAPI 3.1 base registry (v1.4.23 / Wave 4 F5a).
*
* Per the W1 stream-3 decision: `zod-openapi` (samchungy) reads Zod v4's
* native `.meta()` metadata, so we annotate schemas in their original
* validation files and register the route table here. The committed
* spec at `docs/api/openapi.yaml` is regenerated via
* `pnpm openapi:generate`; the CI gate in `.github/workflows/security.yml`
* runs `pnpm openapi:check` to detect drift.
*
* The v1.4.23 baseline covers the 6-8 routes the v1.5 iOS app touches
* (auth/login, auth/passkey/login-verify, auth/refresh, measurements
* GET + POST + batch, devices POST, insights/comprehensive). Future
* waves expand coverage organically as routes are touched. Once the
* registry catches the rest of the hand-maintained spec, the CI gate
* flips from `continue-on-error: true` to hard-fail.
*
* The registry is intentionally split:
* - `openApiBase` carries the static document scaffolding (info,
* servers, tags).
* - `buildOpenApiDocument` assembles the full spec by importing the
* registered route table from `./routes`. Routes live in their own
* file so additions don't touch the base.
*/
import { createDocument, type ZodOpenApiObject } from "zod-openapi";
import { openApiPaths, openApiComponents } from "./routes";
import { version as packageVersion } from "../../../package.json";
const openApiBase: Pick<
ZodOpenApiObject,
"openapi" | "info" | "servers" | "tags"
> = {
openapi: "3.1.0",
info: {
title: "HealthLog API",
// v1.5.1 — pulled from package.json so the spec's info.version
// tracks the release line automatically. The CI drift gate
// (`pnpm openapi:check`) catches the case where the registry
// and the committed spec disagree.
version: packageVersion,
description:
"Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.\n\n" +
"## Date-time contract\n\n" +
"Every field marked `format: date-time` is an ISO-8601 instant. The schema pattern accepts both `Z` (UTC) and `±HH:MM` (offset) suffixes; clients must accept either. " +
"Server responses since v1.4.25 emit timestamps with the requesting user's display-timezone offset when a user context exists (exports, doctor-report PDF). Background-job-authored payloads (backups, admin tools) emit `Z`. " +
"Native iOS clients should always parse via a Foundation ISO-8601 formatter with the `withInternetDateTime` + `withTimeZone` options enabled.",
license: {
name: "AGPL-3.0",
url: "https://github.com/MBombeck/HealthLog/blob/main/LICENSE",
},
contact: { name: "HealthLog", url: "https://healthlog.dev" },
},
servers: [
{ url: "https://healthlog.bombeck.io", description: "Production" },
{ url: "http://localhost:3000", description: "Local dev" },
],
tags: [
{ name: "Auth" },
{ name: "Measurements" },
{ name: "Medications" },
{ name: "Mood" },
{ name: "Analytics" },
{ name: "Insights" },
{ name: "Dashboard" },
{ name: "Notifications" },
{ name: "Devices" },
{ name: "Export" },
{ name: "Sync" },
{ name: "Cycle" },
{ name: "Admin" },
{ name: "Meta" },
],
};
/**
* Assemble the full OpenAPI 3.1 document. Routes live in their own
* file so additions don't touch the base scaffolding. The static
* import keeps typecheck-narrowing and tree-shaking intact (W6 S-02
* removed the lazy-`require` form which had no real consumer and was
* costing an `eslint-disable` + an `as` cast for no benefit).
*/
export function buildOpenApiDocument(): ReturnType<typeof createDocument> {
return createDocument({
...openApiBase,
paths: openApiPaths,
components: openApiComponents,
});
}