mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(measurements): bound the per-night sleep list to a year ending at to
The sleep list capped its read to the last year only when no `from` was sent. An explicit early `from` walked every per-stage row back to that date, on a multi-source account several rows per night for years. The read window is now at most a year ending at `to` (or now). `from` narrows it but never widens it, older nights are reached by moving `to` back, and `meta.windowFrom` names the instant a cut window started, so a client can tell a cut from an empty history.
This commit is contained in:
1 parent
730e9e7015
commit
5f1eb05445
4 files changed
+89
-5
No files matched your search
@@ -56,7 +56,7 @@ interface ListEnvelope {
|
||||
napCount?: number;
|
||||
awakenings?: number;
|
||||
}>;
|
||||
meta: { total: number; groupBy?: string };
|
||||
meta: { total: number; groupBy?: string; windowFrom?: string };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,4 +160,61 @@ describe("GET /api/measurements?type=SLEEP_DURATION", () => {
|
||||
const stages = body.data.measurements.map((m) => m.sleepStage).sort();
|
||||
expect(stages).toEqual(["CORE", "DEEP", "REM"]);
|
||||
});
|
||||
|
||||
it("reads at most a year ending at `to`, however early `from` is", async () => {
|
||||
// An explicit early `from` used to walk every per-stage row back to it.
|
||||
const prisma = getPrismaClient();
|
||||
const user = await seedSession("sleep-window-user");
|
||||
const day = 86_400_000;
|
||||
const morning = new Date();
|
||||
morning.setUTCHours(4, 0, 0, 0);
|
||||
// One night of a single CORE stage every day for two years.
|
||||
await prisma.measurement.createMany({
|
||||
data: Array.from({ length: 730 }, (_, i) => ({
|
||||
userId: user.id,
|
||||
type: "SLEEP_DURATION" as const,
|
||||
value: 420,
|
||||
unit: "minutes",
|
||||
source: "APPLE_HEALTH" as const,
|
||||
measuredAt: new Date(morning.getTime() - i * day),
|
||||
externalId: `uuid-night-${i}`,
|
||||
sleepStage: "CORE" as const,
|
||||
})),
|
||||
});
|
||||
const spy = vi.spyOn(prisma.measurement, "findMany");
|
||||
const from = new Date(morning.getTime() - 800 * day).toISOString();
|
||||
const res = await callList(
|
||||
`type=SLEEP_DURATION&from=${encodeURIComponent(from)}&limit=1000`,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as ListEnvelope;
|
||||
expect(body.data.meta.total).toBeLessThanOrEqual(366);
|
||||
expect(body.data.meta.total).toBeGreaterThanOrEqual(364);
|
||||
expect(body.data.meta.windowFrom).toBeDefined();
|
||||
const windowFrom = Date.parse(body.data.meta.windowFrom!);
|
||||
expect(Date.now() - windowFrom).toBeLessThan(366 * day);
|
||||
expect(Date.now() - windowFrom).toBeGreaterThan(364 * day);
|
||||
// The read itself was bounded, not just the answer.
|
||||
const read = spy.mock.calls
|
||||
.map(([args]) => args as { where?: { measuredAt?: { gte?: Date } } })
|
||||
.find((args) => args?.where?.measuredAt?.gte);
|
||||
expect(read?.where?.measuredAt?.gte?.getTime()).toBe(windowFrom);
|
||||
|
||||
// Older nights are reached by moving `to` back.
|
||||
const to = new Date(morning.getTime() - 400 * day).toISOString();
|
||||
const older = await callList(
|
||||
`type=SLEEP_DURATION&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&limit=1000`,
|
||||
);
|
||||
const olderBody = (await older.json()) as ListEnvelope;
|
||||
expect(olderBody.data.meta.total).toBeGreaterThanOrEqual(329);
|
||||
expect(olderBody.data.meta.total).toBeLessThanOrEqual(331);
|
||||
|
||||
// A window inside the year is untouched and carries no marker.
|
||||
const recent = await callList(
|
||||
`type=SLEEP_DURATION&from=${encodeURIComponent(new Date(morning.getTime() - 10.5 * day).toISOString())}`,
|
||||
);
|
||||
const recentBody = (await recent.json()) as ListEnvelope;
|
||||
expect(recentBody.data.meta.total).toBe(11);
|
||||
expect(recentBody.data.meta.windowFrom).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user