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:
Marc-André Bombeck committed 2026-05-17 17:18:11 +02:00
1 parent 1eb0038914
commit 1440c77396
3 files changed
+54 -4

No files matched your search

+5 -3
View File
@@ -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);
});
});
});
+13 -1
View File
@@ -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),