mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(api): add source filter to measurement and mood lists
Add a source filter to both management lists. Mood gains an optional `source` param (validated against the mood source set); measurements gain `sourceEq` (validated against MeasurementSource) — the `source` key on the measurements list is the rollup-tier opt-in, so the filter rides a distinct param. Both thread into the list `where` alongside the pinned `userId` + `deletedAt: null`. Migration 0136 adds two additive indexes — `(userId, source, measuredAt)` on measurements and `(userId, source, moodLoggedAt)` on mood — so a source filter + recency sort is an index range scan rather than a userId scan plus in-memory sort at 100-200k rows per user. Pure `CREATE INDEX IF NOT EXISTS`, no backfill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1fd875b83c
commit
0d69611bbf
8 files changed
+117
-1
No files matched your search
@@ -97,6 +97,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
sortDir,
|
||||
aggregate,
|
||||
source,
|
||||
sourceEq,
|
||||
groupBy,
|
||||
dayKey,
|
||||
} = parsed.data;
|
||||
@@ -110,6 +111,10 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
// dashboard read paths) must never include them.
|
||||
deletedAt: null,
|
||||
...(type && { type: type as MeasurementType }),
|
||||
// v1.15.13 — management-list source filter. Distinct from the
|
||||
// `source=rollup` opt-in above (which selects the rollup read tier);
|
||||
// `sourceEq` narrows the plain list to one ingest source.
|
||||
...(sourceEq && { source: sourceEq }),
|
||||
...(from || to
|
||||
? {
|
||||
measuredAt: {
|
||||
|
||||
@@ -178,6 +178,38 @@ describe("GET /api/mood-entries — 422 multi-issue (v1.4.43 W6)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/mood-entries — source filter (v1.15.13)", () => {
|
||||
it("threads a valid source into the list where", async () => {
|
||||
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
|
||||
vi.mocked(prisma.moodEntry.count).mockResolvedValue(0 as never);
|
||||
|
||||
const res = await GET(getReq("source=TELEGRAM"));
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const call = vi.mocked(prisma.moodEntry.findMany).mock.calls[0][0];
|
||||
expect(call?.where).toMatchObject({
|
||||
userId: "user-1",
|
||||
deletedAt: null,
|
||||
source: "TELEGRAM",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits source from the where when not supplied", async () => {
|
||||
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
|
||||
vi.mocked(prisma.moodEntry.count).mockResolvedValue(0 as never);
|
||||
|
||||
const res = await GET(getReq(""));
|
||||
expect(res.status).toBe(200);
|
||||
const call = vi.mocked(prisma.moodEntry.findMany).mock.calls[0][0];
|
||||
expect(call?.where).not.toHaveProperty("source");
|
||||
});
|
||||
|
||||
it("422s on an unknown source value", async () => {
|
||||
const res = await GET(getReq("source=BOGUS"));
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/mood-entries — 422 multi-issue (v1.4.43 W6)", () => {
|
||||
it("surfaces TWO simultaneous validation errors", async () => {
|
||||
// Bad `mood` enum + missing `moodLoggedAt`.
|
||||
|
||||
@@ -71,13 +71,16 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
const { mood, from, to, limit, offset, sortBy, sortDir } = parsed.data;
|
||||
const { mood, source, from, to, limit, offset, sortBy, sortDir } =
|
||||
parsed.data;
|
||||
|
||||
const where = {
|
||||
userId: user.id,
|
||||
// v1.7.0 sync — hide soft-deleted (tombstoned) rows from the list.
|
||||
deletedAt: null,
|
||||
...(mood && { mood }),
|
||||
// v1.15.13 — management-list source filter.
|
||||
...(source && { source }),
|
||||
...(from || to
|
||||
? {
|
||||
date: {
|
||||
|
||||
Reference in new issue
Block a user