mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(personal-records): clamp findMany result count via ?limit pagination
The GET route was an unbounded findMany. Today the detection worker isn't shipped so every user sees an empty array, but a power user with multi-year Apple Health history could accumulate 50+ PRs per metric × 14 PR-trackable metrics once the worker lands. Default 100, max 500 — matches `listMeasurementsSchema`'s ceiling so every ingest-and-read endpoint on the route surface shares one upper bound. Garbage values silently clamp to the default rather than 400-ing, keeping the same defence-in-depth posture the `metricType` parse uses. The v1.5 iOS app's query path is now bounded from day one.
This commit is contained in:
1 parent
cf5d6c433a
commit
eb49d8ab1b
1 file changed
+26
@@ -9,6 +9,7 @@
|
||||
*
|
||||
* Query params:
|
||||
* - metricType: optional MeasurementType filter (e.g. ?metricType=VO2_MAX)
|
||||
* - limit: optional pagination cap (default 100, max 500)
|
||||
*
|
||||
* Response envelope (matches the project-wide `apiSuccess` contract):
|
||||
* { data: PersonalRecord[], error: null }
|
||||
@@ -23,12 +24,24 @@ import type { MeasurementType } from "@/generated/prisma/client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// v1.4.25 W10 reconcile (Sr-M4) — pagination bounds. Matches the
|
||||
// `listMeasurementsSchema` ceiling (max 500) so every ingest-and-read
|
||||
// endpoint on the route surface shares the same upper bound. Today
|
||||
// the worker hasn't populated any rows so even an uncapped findMany is
|
||||
// harmless, but a power user with multi-year Apple Health history could
|
||||
// accumulate 50+ PRs per metric × 14 PR-trackable metrics once the
|
||||
// worker lands. Add the cap now so the v1.5 iOS app's query path is
|
||||
// bounded from day one.
|
||||
const DEFAULT_LIMIT = 100;
|
||||
const MAX_LIMIT = 500;
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "personalRecords.list" } });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const metricTypeParam = searchParams.get("metricType");
|
||||
const limitParam = searchParams.get("limit");
|
||||
|
||||
// Defensive parse — drop unknown values rather than 400 so the
|
||||
// caller's loosely-typed filter doesn't take the page down.
|
||||
@@ -37,12 +50,25 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
? (metricTypeParam as MeasurementType)
|
||||
: null;
|
||||
|
||||
// Limit clamp: parse → clamp [1, MAX_LIMIT] → default fallback. A
|
||||
// garbage value (`?limit=abc`, `?limit=-1`, `?limit=999999`) silently
|
||||
// clamps to the default rather than 400-ing — same defence-in-depth
|
||||
// stance the metricType parse takes.
|
||||
let limit = DEFAULT_LIMIT;
|
||||
if (limitParam != null) {
|
||||
const parsed = Number(limitParam);
|
||||
if (Number.isInteger(parsed) && parsed > 0) {
|
||||
limit = Math.min(parsed, MAX_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
const records = await prisma.personalRecord.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(metricType ? { metricType } : {}),
|
||||
},
|
||||
orderBy: { achievedAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return apiSuccess(records);
|
||||
|
||||
Reference in new issue
Block a user