mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(measurements): value-range filter on the management list
Add an optional numeric value-range filter to the measurement list:
readings whose `value` falls within `[valueMin, valueMax]`, either bound
omittable for an open-ended range. Server-authoritative — both bounds are
parsed on `listMeasurementsSchema` (coerced from the query string), an
inverted `valueMin > valueMax` returns 422, and the bounds thread into the
Prisma `where` as a `value: { gte, lte }` range on the raw per-sample list.
The filter rides a new `FilterBarNumberRange` pill that mirrors the existing
date-range control, hidden for the day-grouped and sleep-night types where
the painted value is a synthesised total rather than a stored reading.
This commit is contained in:
1 parent
8920a35d38
commit
cc89695cbd
14 files changed
+286
-4
No files matched your search
@@ -331,6 +331,68 @@ describe("measurement validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("listMeasurementsSchema — v1.18.5 value-range filter", () => {
|
||||
it("coerces valueMin / valueMax from query strings", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({
|
||||
valueMin: "80",
|
||||
valueMax: "120",
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
if (parsed.success) {
|
||||
expect(parsed.data.valueMin).toBe(80);
|
||||
expect(parsed.data.valueMax).toBe(120);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts an open-ended min-only range", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({ valueMin: "100" });
|
||||
expect(parsed.success).toBe(true);
|
||||
if (parsed.success) {
|
||||
expect(parsed.data.valueMin).toBe(100);
|
||||
expect(parsed.data.valueMax).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts an open-ended max-only range", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({ valueMax: "90" });
|
||||
expect(parsed.success).toBe(true);
|
||||
if (parsed.success) {
|
||||
expect(parsed.data.valueMax).toBe(90);
|
||||
expect(parsed.data.valueMin).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts valueMin == valueMax (exact-value band)", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({
|
||||
valueMin: "72",
|
||||
valueMax: "72",
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects an inverted range (valueMin > valueMax) with a 422-shaped issue", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({
|
||||
valueMin: "120",
|
||||
valueMax: "80",
|
||||
});
|
||||
expect(parsed.success).toBe(false);
|
||||
if (!parsed.success) {
|
||||
expect(parsed.error.issues.some((i) => i.path[0] === "valueMin")).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves both bounds undefined when omitted", () => {
|
||||
const parsed = listMeasurementsSchema.safeParse({});
|
||||
expect(parsed.success).toBe(true);
|
||||
if (parsed.success) {
|
||||
expect(parsed.data.valueMin).toBeUndefined();
|
||||
expect(parsed.data.valueMax).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@@ -651,6 +651,13 @@ export const listMeasurementsSchema = z
|
||||
// threaded into the plain-list `where`. Backed by the `(userId, source,
|
||||
// measuredAt)` index (migration 0136).
|
||||
sourceEq: measurementSourceEnum.optional(),
|
||||
// v1.18.5 — value-range FILTER (backlog G). Narrows the plain list to
|
||||
// readings whose `value` falls within an optional `[valueMin, valueMax]`
|
||||
// band. Either bound may be omitted for an open-ended range. Coerced
|
||||
// from the query string; threaded into the plain-list `where` as a
|
||||
// `value: { gte, lte }` range. Server-authoritative; validated min<=max.
|
||||
valueMin: z.coerce.number().finite().optional(),
|
||||
valueMax: z.coerce.number().finite().optional(),
|
||||
// v1.4.37 W7c — list-view "one row per day" mode for cumulative
|
||||
// types (steps, active energy, distance, flights, daylight). When
|
||||
// `groupBy=day` is set and `type` is a cumulative HK type, the route
|
||||
@@ -715,7 +722,19 @@ export const listMeasurementsSchema = z
|
||||
message:
|
||||
"limit must be <= 1000 when dayKey is set; drill-down responses are capped at 1000 rows",
|
||||
path: ["limit"],
|
||||
});
|
||||
})
|
||||
// v1.18.5 — an inverted value range (`valueMin > valueMax`) is a client
|
||||
// bug, not an empty result set; reject it at the validator with a 422 so
|
||||
// the filter UI can surface the mismatch instead of silently returning
|
||||
// zero rows. Open-ended ranges (one bound omitted) pass through.
|
||||
.refine(
|
||||
({ valueMin, valueMax }) =>
|
||||
valueMin == null || valueMax == null || valueMin <= valueMax,
|
||||
{
|
||||
message: "valueMin must be <= valueMax",
|
||||
path: ["valueMin"],
|
||||
},
|
||||
);
|
||||
|
||||
export const createBatchMeasurementSchema = z.object({
|
||||
measurements: z.array(createMeasurementSchema).min(1).max(5),
|
||||
|
||||
Reference in new issue
Block a user