fix(measurements-route,series,timeline): exclude soft-deleted in list reads

`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.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 11:45:26 +02:00
1 parent 1dd1a9a7a5
commit 97920f0f59
2 files changed
+17 -1

No files matched your search

+9
View File
@@ -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
+8 -1
View File
@@ -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 },
});