Merge the health-data route group

Two groups created environment.ts independently; one carried all six
paths, the other only the overview read, so the fuller side stands and
the barrel keeps one import.
This commit is contained in:
Marc-André Bombeck committed 2026-08-22 13:29:59 +02:00
commit 50b483d8d1
18 files changed
+4136 -54

No files matched your search

@@ -9,9 +9,11 @@
* keeps its neutral greeting — never a fabricated opener.
*
* The signal is selected SERVER-SIDE; the client renders the resolved DTO
* and never recomputes it (server-authoritative parity). This is an
* INTERNAL same-origin route — it is deliberately NOT registered in the
* OpenAPI contract the iOS client consumes.
* and never recomputes it (server-authoritative parity). It IS registered in
* the OpenAPI contract. This comment said the opposite until the route was
* published — it shipped as a same-origin web surface and the sentence was
* never revisited. A comment that contradicts the registry is worse than no
* comment, because the next reader believes it instead of checking.
*
* Mirrors the sibling derived routes: `apiHandler` wrapper, `requireAuth`
* (userId narrowed from the session/Bearer, never a body/query field),
+11 -2
View File
@@ -15,7 +15,7 @@
*/
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import { annotate } from "@/lib/logging/context";
import {
@@ -53,7 +53,16 @@ const POST_WINDOW_MS = 60_000;
export const GET = apiHandler(
async (_request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
// The read resolves the RECORD, at the level its siblings use. It did not
// until the route was published, and the asymmetry that produced is the
// reason it is written down here: the POST below has resolved the record
// since v1.37.0, so a manager could add a titration step to a medication
// whose history the GET refused them — may write, may not read, which is
// not a shape any reading of the sharing model produces. `read` rather
// than the POST's `manage` because that is what `/inventory`,
// `/side-effects` and `/cadence` ask for the same medication's data, and
// this read is strictly narrower than the write already permitted.
const { user } = await requireRecordAuth("read", "medications");
const { id } = await params;
// v1.5.5 F-1 C-4 — close the §10 invariant 24 gap. The destructive
+8 -33
View File
@@ -12,7 +12,6 @@
* Updates the named MedicationIntakeEvent and returns the updated row.
*/
import { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import {
apiError,
@@ -46,7 +45,8 @@ import {
restoreForIntake,
} from "@/lib/medications/inventory/consumption";
import {
injectionSiteEnum,
intakeAggregateQuerySchema,
intakeStatusUpdateSchema,
type InjectionSiteValue,
} from "@/lib/validations/medication";
import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync";
@@ -54,40 +54,15 @@ import { dispatchMedicationIntakeWebClear } from "@/lib/notifications/web-push-c
import { notifyDelegatedIntake } from "@/lib/notifications/delegated-intake";
import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses";
const querySchema = z.object({
scope: z.enum(["today", "compliance"]),
days: z.coerce.number().int().min(1).max(365).optional().default(30),
});
const updateSchema = z.object({
intakeId: z.string().min(1),
status: z.enum(["taken", "skipped", "snoozed"]),
takenAt: z.iso
.datetime({ offset: true })
.transform((s) => new Date(s))
.optional(),
snoozedUntil: z.iso
.datetime({ offset: true })
.transform((s) => new Date(s))
.optional(),
// v1.8.5 — optional injection site captured alongside a "taken"
// status toggle. Validated server-side against the medication's
// effective allowed set; ignored for skipped / snoozed.
injectionSite: injectionSiteEnum.optional(),
// v1.15.18 — late-take "attribute anyway" pin. When a "taken" toggle lands
// outside every dose window the UI can offer to pin the take onto a chosen
// real slot ("diesem Slot zuordnen?"); the server validates the instant is
// a real slot of the medication (422 otherwise). Ignored for skip / snooze.
forceSlotInstant: z.iso
.datetime({ offset: true })
.transform((s) => new Date(s))
.optional(),
});
// The query + body schemas moved to `@/lib/validations/medication` when this
// endpoint joined the published contract: the OpenAPI registry generates from
// the validation modules, and a schema that only exists inside a handler is
// one the spec has to restate — which is a second copy to keep in step.
export const GET = apiHandler(async (request: NextRequest) => {
const { user } = await requireRecordAuth("read", "medications");
const parsed = querySchema.safeParse(
const parsed = intakeAggregateQuerySchema.safeParse(
Object.fromEntries(request.nextUrl.searchParams),
);
if (!parsed.success) {
@@ -233,7 +208,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
if (error) return error;
const parsed = updateSchema.safeParse(body);
const parsed = intakeStatusUpdateSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 W6 — intake-event update hot path; multi-issue 422 +
// audit breadcrumb keyed `medications.intake.update.validation-failed`.