Files
HealthLog/src/lib/openapi/routes/hydration.ts
T
Marc-André Bombeck 31c35baec2 feat: hydration logging and daily goal ring
Add a WATER_INTAKE measurement type and a per-user daily goal so the
Insights overview can track water intake against a goal ring. Logging
rides the existing measurements create path (type WATER_INTAKE, unit ml,
summed cumulatively); GET/PATCH /api/hydration read the day total and own
the goal. The goal-ring math (empty / partial / met / exceeded) lives in
a pure helper, free of Prisma and React, and is unit-tested in isolation.

Wire the new type through the measurement enum, value ranges, cumulative
HealthKit handling, categories, display transform, list metadata, record
direction, and the chartable-token allowlist. The HealthKit DietaryWater
import stays on the nutrition hold, so the type carries no import mapping
by design.

Document GET/PATCH /api/hydration in the OpenAPI contract.
2026-06-28 03:20:28 +02:00

103 lines
3.1 KiB
TypeScript

/**
* OpenAPI route module — hydration daily-goal surface (v1.25).
*
* `GET /api/hydration` reads today's summed water intake vs the user's goal;
* `PATCH /api/hydration` sets the per-user daily goal. Logging rides the
* measurements create path (POST /api/measurements, type WATER_INTAKE), so it
* is documented there, not here.
*
* Part of the OpenAPI route table; aggregated in `./index.ts`.
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { dataEnvelope, stdResponses } from "./shared";
const hydrationEntry = z.object({
id: z.string(),
value: z.number().describe("Logged amount in millilitres."),
measuredAt: z.string().describe("ISO-8601 instant the entry was logged."),
});
const hydrationTodayResponse = z
.object({
date: z.string().describe("ISO-8601 start of the user's local day."),
totalMl: z.number().describe("Today's summed intake in ml."),
goalMl: z.number().describe("Effective daily goal in ml."),
percent: z.number().describe("Progress toward the goal, capped at 100."),
rawPercent: z.number().describe("Uncapped progress; can exceed 100."),
met: z.boolean().describe("Whether the day's total reached the goal."),
remainingMl: z.number().describe("Remaining ml to the goal (0 once met)."),
entries: z.array(hydrationEntry),
})
.meta({
id: "HydrationToday",
description: "Today's water-intake total vs the user's daily goal.",
});
const hydrationGoalRequest = z
.object({
goalMl: z
.number()
.int()
.min(250)
.max(8000)
.describe("Daily hydration goal in millilitres."),
})
.meta({
id: "HydrationGoalRequest",
description: "Set the per-user daily hydration goal (ml).",
});
const hydrationGoalResponse = z
.object({ goalMl: z.number() })
.meta({ id: "HydrationGoalResponse" });
export const hydrationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/hydration": {
get: {
tags: ["Hydration"],
summary: "Today's hydration total vs goal",
description:
"Sums the calling user's WATER_INTAKE measurements for the current local day and compares them against the per-user goal (default 2000 ml).",
responses: {
"200": {
description: "Today's hydration summary.",
content: {
"application/json": {
schema: dataEnvelope(
hydrationTodayResponse,
"HydrationTodayEnvelope",
),
},
},
},
...stdResponses,
},
},
patch: {
tags: ["Hydration"],
summary: "Set the daily hydration goal",
description: "Updates the per-user daily hydration goal in millilitres.",
requestBody: {
required: true,
content: { "application/json": { schema: hydrationGoalRequest } },
},
responses: {
"200": {
description: "Goal updated.",
content: {
"application/json": {
schema: dataEnvelope(
hydrationGoalResponse,
"HydrationGoalEnvelope",
),
},
},
},
...stdResponses,
},
},
},
};