From 97920f0f59a11d25db463b0868b75b556c4b0be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 11:45:26 +0200 Subject: [PATCH] fix(measurements-route,series,timeline): exclude soft-deleted in list reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GET /api/measurements` has five distinct read shapes (per-day drill-down, group-by-day, rollup-source coverage-fallback probe + fold re-read, live `date_trunc` aggregate, default findMany + count). Each relied on a shared `where` builder or a local `findMany` clause; none filtered `deletedAt`. The `GET /api/measurements/series` adapter for iOS read BP-SYS / BP-DIA / single-type series without the filter too. Pin `deletedAt: null` on the shared `where` so the count + raw findMany + group-by-day mode all inherit it; add the filter to the dayKey drill-down findMany, the coverage-fallback day-count probe, and the live `date_trunc` aggregate; thread the filter through the three series findMany calls. The `source=rollup` branch reads from `measurement_rollups` so the writer-side filter covers it. No `measurements/timeline` route exists yet — the file-set name was prospective. --- src/app/api/measurements/route.ts | 9 +++++++++ src/app/api/measurements/series/route.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/api/measurements/route.ts b/src/app/api/measurements/route.ts index 2229d21ba..21c72df3f 100644 --- a/src/app/api/measurements/route.ts +++ b/src/app/api/measurements/route.ts @@ -64,6 +64,12 @@ export const GET = apiHandler(async (request: NextRequest) => { const where = { userId: user.id, + // v1.4.40 W-DELETED — soft-deleted readings are invisible to every + // list / count / aggregate read. The dedicated `sync/state` route + // keeps surfacing them via its own filter so iOS can reconcile + // tombstones; every other consumer (list view, analytics fan-out, + // dashboard read paths) must never include them. + deletedAt: null, ...(type && { type: type as MeasurementType }), ...(from || to ? { @@ -99,6 +105,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userId: user.id, type: type as MeasurementType, measuredAt: { gte: dayStart, lt: dayEnd }, + deletedAt: null, }, orderBy: { measuredAt: sortDir }, // v1.4.38 — the 1000-row cap now lives on the validator (refine @@ -347,6 +354,7 @@ export const GET = apiHandler(async (request: NextRequest) => { AND m."type" = ${type}::measurement_type AND m."measured_at" >= ${from} AND m."measured_at" <= ${to} + AND m."deleted_at" IS NULL `; const liveDays = Number(liveDayCountRows[0]?.days ?? BigInt(0)); if (liveDays > rollupRows.length) { @@ -484,6 +492,7 @@ export const GET = apiHandler(async (request: NextRequest) => { WHERE m."user_id" = ${user.id} AND m."measured_at" >= ${from} AND m."measured_at" <= ${to} + AND m."deleted_at" IS NULL ${type ? Prisma.sql`AND m."type" = ${type}::measurement_type` : Prisma.empty} GROUP BY m."type", bucket_start ORDER BY bucket_start ASC diff --git a/src/app/api/measurements/series/route.ts b/src/app/api/measurements/series/route.ts index b570404c4..a91a6382f 100644 --- a/src/app/api/measurements/series/route.ts +++ b/src/app/api/measurements/series/route.ts @@ -84,6 +84,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userId: user.id, type: "BLOOD_PRESSURE_SYS", measuredAt: { gte: since }, + deletedAt: null, }, orderBy: { measuredAt: "asc" }, select: { id: true, value: true, measuredAt: true }, @@ -93,6 +94,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userId: user.id, type: "BLOOD_PRESSURE_DIA", measuredAt: { gte: since }, + deletedAt: null, }, orderBy: { measuredAt: "asc" }, select: { id: true, value: true, measuredAt: true }, @@ -120,7 +122,12 @@ export const GET = apiHandler(async (request: NextRequest) => { } else { const type = KIND_TO_TYPE[kind]; const rows = await prisma.measurement.findMany({ - where: { userId: user.id, type, measuredAt: { gte: since } }, + where: { + userId: user.id, + type, + measuredAt: { gte: since }, + deletedAt: null, + }, orderBy: { measuredAt: "asc" }, select: { id: true, value: true, measuredAt: true }, });