mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(api): say when a list was cut at its ceiling
A list that stops at its own take and says nothing lets the last row read as the oldest there is. Capped lists now fetch one row past the limit and answer meta.truncated and meta.limit on the success envelope, so data keeps its shape. Applied to the capped lists without a total count: rhythm events, ECG recordings, sharing grants and activity, illness episodes, side effects, personal records, security activity, cycles, family history, allergies and mental-health assessments. The activity feed no longer reports a list that is exactly full as cut.
This commit is contained in:
1 parent
415da581f8
commit
97d1052dd1
28 files changed
+396
-174
No files matched your search
@@ -44,7 +44,12 @@ import {
|
||||
} from "@/lib/validations/managed-profile-handover";
|
||||
import { onboardingStateResource } from "./onboarding";
|
||||
import { aiAccountBlock, moduleAccessMap } from "./profile";
|
||||
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
import {
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
/**
|
||||
@@ -877,7 +882,7 @@ export const accountSharingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The activity, and how far back it can see.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
recordActivityResponse,
|
||||
"RecordActivityEnvelope",
|
||||
),
|
||||
@@ -900,7 +905,7 @@ export const accountSharingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Grants given and received.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
grantListResponse,
|
||||
"AccountGrantListEnvelope",
|
||||
),
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
@@ -97,7 +98,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The caller's allergy records.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(z.array(allergy), "ListAllergiesEnvelope"),
|
||||
schema: listEnvelope(z.array(allergy), "ListAllergiesEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
errorEnvelope,
|
||||
loginPasswordSchema,
|
||||
validationIssue,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { passkeyLoginOptionsSchema } from "@/lib/validations/auth";
|
||||
import {
|
||||
@@ -1724,7 +1725,7 @@ export const authPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Recent security events for the caller.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
securityActivityResponse,
|
||||
"SecurityActivityEnvelope",
|
||||
),
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
measurementTypeEnum,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
@@ -190,7 +191,7 @@ export const awardsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The record's personal bests, newest first.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.array(personalRecord),
|
||||
"ListPersonalRecordsEnvelope",
|
||||
),
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
@@ -954,7 +955,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Cycle history.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
cycleHistoryResponse,
|
||||
"CycleHistoryEnvelope",
|
||||
),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
|
||||
@@ -88,7 +89,7 @@ export const familyHistoryPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The caller's family-history entries.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.array(familyHistoryEntry),
|
||||
"ListFamilyHistoryEnvelope",
|
||||
),
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
|
||||
@@ -298,7 +299,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The caller's illness episodes.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.array(illnessEpisode),
|
||||
"ListIllnessEpisodesEnvelope",
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "../shared";
|
||||
import {
|
||||
aiCheckFailedResponse,
|
||||
@@ -892,7 +893,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The ECG recording list (possibly empty).",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(ecgListResponse, "EcgListResponseEnvelope"),
|
||||
schema: listEnvelope(ecgListResponse, "EcgListResponseEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -985,7 +986,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The device-flagged event timeline (possibly empty).",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
rhythmEventsResponse,
|
||||
"RhythmEventsResponseEnvelope",
|
||||
),
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "../shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
@@ -596,7 +597,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"The entries, plus a `meta.total` that counts THIS PAGE rather than the whole log — it is the returned array's length, so it can never exceed `limit`.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.object({
|
||||
items: z.array(medicationSideEffect),
|
||||
meta: z.object({ total: z.number().int().nonnegative() }),
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
|
||||
@@ -121,7 +122,7 @@ export const mentalHealthPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Screener history.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
listResponse,
|
||||
"ListMentalHealthAssessmentsEnvelope",
|
||||
),
|
||||
|
||||
@@ -131,6 +131,42 @@ export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
.meta({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
* The success envelope of a list the server cuts at a ceiling (see
|
||||
* `src/lib/api/list-cap.ts`). `data` is the route's own payload, unchanged;
|
||||
* `meta.truncated` says whether rows past `meta.limit` exist.
|
||||
*/
|
||||
export const listCapMeta = z
|
||||
.object({
|
||||
requestId: z.string().optional(),
|
||||
truncated: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"True when more rows exist than this response carries: the list was cut at `limit`, newest or most relevant first as the operation states. False means the list is complete. Show the person that the list is partial rather than letting the last row read as the oldest there is.",
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(10_000)
|
||||
.describe("The most rows this response could carry."),
|
||||
})
|
||||
.meta({
|
||||
id: "ListCapMeta",
|
||||
description:
|
||||
"How a capped list was cut. Present on every response of an operation that declares it.",
|
||||
});
|
||||
|
||||
export function listEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
return z
|
||||
.object({
|
||||
data: payload,
|
||||
error: z.null(),
|
||||
meta: listCapMeta,
|
||||
})
|
||||
.meta({ id });
|
||||
}
|
||||
|
||||
// ── Schemas — annotated for spec emission ────────────────────────────
|
||||
//
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
|
||||
Reference in new issue
Block a user