mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(nutrients): evict the dashboard snapshot on a landed batch row
POST /api/nutrients/batch performed zero cache invalidation, so an Apple-Health-synced water/nutrient total kept the dashboard's tile serving the pre-sync figure until the analytics fresh TTL lapsed (~3 min) — the manual water POST already hard-evicts on its own write. Calls invalidateUserDashboardSnapshot(user.id) once at least one row inserted or updated, mirroring the manual route's own posture so a synced total reflects on the very next dashboard read.
This commit is contained in:
1 parent
56a32c528e
commit
1d559d7ed0
2 files changed
+46
No files matched your search
@@ -46,6 +46,9 @@ vi.mock("@/lib/idempotency", () => ({
|
||||
fn(...args),
|
||||
}));
|
||||
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
|
||||
vi.mock("@/lib/cache/invalidate", () => ({
|
||||
invalidateUserDashboardSnapshot: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
@@ -65,6 +68,7 @@ import { getSession } from "@/lib/auth/session";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
@@ -315,6 +319,40 @@ describe("POST /api/nutrients/batch — write semantics", () => {
|
||||
expect(prisma.nutrientIntakeDay.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// v1.30.3 (QA F9) — a landed row must evict the dashboard snapshot so
|
||||
// the water/nutrient tile reflects the new total on the very next read,
|
||||
// matching the manual water POST's own posture (`nutrients/water/route.ts`).
|
||||
it("evicts the dashboard snapshot when a row lands", async () => {
|
||||
const day = recentDay();
|
||||
const res = await POST(
|
||||
postReq({
|
||||
entries: [{ day, nutrient: "vitamin_d", unit: "ug", amount: 22.5 }],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(invalidateUserDashboardSnapshot).toHaveBeenCalledTimes(1);
|
||||
expect(invalidateUserDashboardSnapshot).toHaveBeenCalledWith("user-1");
|
||||
});
|
||||
|
||||
it("does NOT evict the dashboard snapshot when nothing lands (every entry skipped)", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
entries: [
|
||||
{
|
||||
day: recentDay(),
|
||||
nutrient: "vitamin_d",
|
||||
unit: "mg", // wrong unit vs the catalog's canonical "ug" — skipped
|
||||
amount: 22.5,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const body = (await res.json()) as BatchResponse;
|
||||
expect(body.data.inserted).toBe(0);
|
||||
expect(body.data.updated).toBe(0);
|
||||
expect(invalidateUserDashboardSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports updated (never duplicate) when the composite key already exists", async () => {
|
||||
const day = recentDay();
|
||||
vi.mocked(prisma.nutrientIntakeDay.findMany).mockResolvedValue([
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate";
|
||||
import { NUTRIENT_CATALOG } from "@/lib/nutrients/catalog";
|
||||
import {
|
||||
MAX_NUTRIENT_ENTRIES_PER_BATCH,
|
||||
@@ -324,6 +325,13 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
skipped: skipped.length,
|
||||
},
|
||||
});
|
||||
// v1.30.3 (QA F9) — a landed row changes the dashboard's water/nutrient
|
||||
// tile; without this the snapshot kept serving the pre-sync figure
|
||||
// until the analytics fresh TTL lapsed (~3 min). Mirrors the manual
|
||||
// water POST's own hard-evict (`nutrients/water/route.ts`) so an
|
||||
// Apple-Health-synced total reflects on the very next dashboard read,
|
||||
// same as a manually logged one.
|
||||
invalidateUserDashboardSnapshot(user.id);
|
||||
}
|
||||
|
||||
annotate({
|
||||
|
||||
Reference in new issue
Block a user