mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(measurements): refine drill-down limit to 1000 in the validator
The /api/measurements per-day drill-down branch clamped `limit` to `Math.min(limit, 1000)` after the request reached the route. Callers asking for more saw no response signal — the schema-declared 5000 ceiling silently capped at 1000. Move the cap into the validator's .refine on (limit, dayKey) so the boundary returns 422 with an actionable message. The route now reads `limit` straight through because the validator has already clamped it.
This commit is contained in:
1 parent
1eb0038914
commit
1440c77396
3 files changed
+54
-4
No files matched your search
@@ -100,9 +100,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
measuredAt: { gte: dayStart, lt: dayEnd },
|
||||
},
|
||||
orderBy: { measuredAt: sortDir },
|
||||
// Cap the drill-down — a phone-only stepCount day can hit a few
|
||||
// hundred rows; the route ceiling keeps a pathological day bounded.
|
||||
take: Math.min(limit, 1000),
|
||||
// v1.4.38 — the 1000-row cap now lives on the validator (refine
|
||||
// on `(limit, dayKey)` returning 422 when a caller asks for
|
||||
// more). The route reads `limit` straight through because the
|
||||
// validator has already clamped it.
|
||||
take: limit,
|
||||
});
|
||||
annotate({
|
||||
action: { name: "measurement.list" },
|
||||
|
||||
@@ -235,4 +235,40 @@ describe("measurement validation", () => {
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// v1.4.38 — the per-day drill-down branch always returns at most
|
||||
// 1000 rows (one pathological phone-only stepCount day). Push the
|
||||
// cap into the validator so a caller asking for more sees a 422
|
||||
// instead of a silent server-side Math.min clamp.
|
||||
describe("listMeasurementsSchema — v1.4.38 dayKey limit ceiling", () => {
|
||||
it("accepts limit=1000 alongside a dayKey", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({
|
||||
type: "ACTIVITY_STEPS",
|
||||
dayKey: "2026-05-15",
|
||||
limit: 1000,
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects limit=1001 when dayKey is set (drill-down hard cap)", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({
|
||||
type: "ACTIVITY_STEPS",
|
||||
dayKey: "2026-05-15",
|
||||
limit: 1001,
|
||||
});
|
||||
expect(parsed.success).toBe(false);
|
||||
if (!parsed.success) {
|
||||
expect(parsed.error.issues[0].message).toMatch(/<=\s*1000/);
|
||||
expect(parsed.error.issues[0].path).toEqual(["limit"]);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts limit=5000 when dayKey is omitted (legacy ceiling)", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({
|
||||
type: "ACTIVITY_STEPS",
|
||||
limit: 5000,
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -325,7 +325,19 @@ export const listMeasurementsSchema = z
|
||||
"offset is not supported with groupBy=day or dayKey; use a smaller window or omit offset",
|
||||
path: ["offset"],
|
||||
},
|
||||
);
|
||||
)
|
||||
// v1.4.38 — surface the drill-down cap on the validator so a caller
|
||||
// asking for more than 1000 rows on a per-day branch sees a 422
|
||||
// instead of a silent `Math.min(limit, 1000)` clamp inside the
|
||||
// route. The drill-down returns at most one phone-only stepCount
|
||||
// day's worth of per-sample chunks; 1000 covers the pathological
|
||||
// case with room to spare. Callers that want a wider window should
|
||||
// omit `dayKey` and use `from` / `to` instead.
|
||||
.refine(({ limit, dayKey }) => !(dayKey != null && limit > 1000), {
|
||||
message:
|
||||
"limit must be <= 1000 when dayKey is set; drill-down responses are capped at 1000 rows",
|
||||
path: ["limit"],
|
||||
});
|
||||
|
||||
export const createBatchMeasurementSchema = z.object({
|
||||
measurements: z.array(createMeasurementSchema).min(1).max(5),
|
||||
|
||||
Reference in new issue
Block a user