mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
feat(lists): read and show the list cap on every capped list
Twelve routes answer a capped list with meta.truncated / meta.limit on the envelope, and the web client read only data, so no surface said a list had stopped short. apiGetList / apiPostList are now the one client reader of the cap and ListCapNotice the one line that shows it; every capped list on the web reads its cap and shows the notice under the list, the paged readers (side effects, illness episodes) stop on the route's answer instead of a full-page guess, and the ECG list moves its five readers onto one hook. The record activity feed sent its cap twice, in data and on the envelope; it rides the envelope alone now. A guard derives the capped routes from the tree and binds each to a client that reads its cap.
This commit is contained in:
1 parent
be79603d53
commit
dc5120b59a
49 files changed
+574
-204
No files matched your search
@@ -56,6 +56,7 @@ import {
|
||||
validateResponseContext,
|
||||
withRecordFenceHeaders,
|
||||
} from "./record-fence";
|
||||
import { readListCap, type ListCap } from "@/lib/api/list-cap-shape";
|
||||
|
||||
/** HTTP-level failure from an `/api/...` route — non-OK status. */
|
||||
export class ApiError extends Error {
|
||||
@@ -214,6 +215,35 @@ function withJsonBody(
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET a capped list — resolves with `data` and the envelope's list cap
|
||||
* (`meta.truncated` / `meta.limit`), null when the route sent none. The one
|
||||
* client reader of the cap; pair it with `ListCapNotice`.
|
||||
*/
|
||||
export async function apiGetList<T = unknown>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<{ data: T; cap: ListCap | null }> {
|
||||
const { data, meta } = await apiFetchEnvelope<T>(path, {
|
||||
...init,
|
||||
method: "GET",
|
||||
});
|
||||
return { data, cap: readListCap(meta) };
|
||||
}
|
||||
|
||||
/** POST variant of {@link apiGetList}, for a list read with a body. */
|
||||
export async function apiPostList<T = unknown>(
|
||||
path: string,
|
||||
body?: unknown,
|
||||
init?: RequestInit,
|
||||
): Promise<{ data: T; cap: ListCap | null }> {
|
||||
const { data, meta } = await apiFetchEnvelope<T>(
|
||||
path,
|
||||
withDefaultIdempotencyKey(withJsonBody("POST", body, init)),
|
||||
);
|
||||
return { data, cap: readListCap(meta) };
|
||||
}
|
||||
|
||||
/** GET — resolves with the unwrapped `data` payload. */
|
||||
export function apiGet<T = unknown>(
|
||||
path: string,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* The wire shape of a list cap, safe for the client bundle.
|
||||
*
|
||||
* A capped list answers with `meta: { truncated, limit }` on the envelope
|
||||
* (`apiListSuccess` in `./list-cap.ts`). That is the one place the signal
|
||||
* rides; `readListCap` is the one place a client reads it.
|
||||
*/
|
||||
export interface ListCap {
|
||||
/** A row past `limit` exists and was not returned. */
|
||||
truncated: boolean;
|
||||
/** The most rows this response could carry. */
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/** The cap an envelope's `meta` carries, or null when it carries none. */
|
||||
export function readListCap(meta: unknown): ListCap | null {
|
||||
if (!meta || typeof meta !== "object") return null;
|
||||
const { truncated, limit } = meta as Record<string, unknown>;
|
||||
if (typeof truncated !== "boolean") return null;
|
||||
if (typeof limit !== "number" || !Number.isFinite(limit)) return null;
|
||||
return { truncated, limit };
|
||||
}
|
||||
@@ -17,16 +17,16 @@
|
||||
* `data` keeps exactly the shape the route always had and a client that never
|
||||
* reads `meta` decodes it unchanged. `truncated` is exact: true only when a
|
||||
* row past the limit exists. The OpenAPI side is `listEnvelope` in
|
||||
* `src/lib/openapi/routes/shared.ts`.
|
||||
* `src/lib/openapi/routes/shared.ts`. A client reads it through `apiGetList`
|
||||
* / `apiPostList` (`src/lib/api/api-fetch.ts`) and shows it with
|
||||
* `ListCapNotice`; `src/__tests__/list-cap-reader-guard.test.ts` binds every
|
||||
* capped route to such a reader.
|
||||
*/
|
||||
import { apiSuccessWithMeta } from "@/lib/api-response";
|
||||
|
||||
export interface ListCap {
|
||||
/** A row past `limit` exists and was not returned. */
|
||||
truncated: boolean;
|
||||
/** The most rows this response could carry. */
|
||||
limit: number;
|
||||
}
|
||||
import type { ListCap } from "./list-cap-shape";
|
||||
|
||||
export type { ListCap } from "./list-cap-shape";
|
||||
|
||||
/** The `take` that lets {@link capRows} tell a full list from a cut one. */
|
||||
export function overfetch(limit: number): number {
|
||||
|
||||
Reference in new issue
Block a user