mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
feat(api): add bulk soft-delete for measurements and mood
Add `POST /api/measurements/bulk-delete` and `POST /api/mood-entries/bulk-delete`,
mirroring the bulk-insert conventions (`withIdempotency`, per-user rate
limit, response envelope). Body is `{ ids: string[] }`, capped at 200.
Soft-delete only, ownership-scoped: a single `updateMany` pins `userId` +
`deletedAt: null` and sets `deletedAt` + bumps `syncVersion`, matching the
single-DELETE iOS tombstone contract. A forged or foreign id is a silent
no-op — the returned `deleted` count reflects owned rows only, never a 404
existence leak.
Side effects fan out efficiently: measurements collapse the deleted rows to
the unique `(type, day)` set before recomputing rollups (one recompute per
type-day, not per row) and invalidate the affected status-insight caches;
mood collapses to the unique day set for its rollup recompute. Both bust the
per-user read caches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0d69611bbf
commit
ca262d9839
7 files changed
+984
No files matched your search
@@ -579,6 +579,26 @@ const deleteByExternalIdsResponse = z
|
||||
})
|
||||
.meta({ id: "MeasurementsDeleteByExternalIdsResult" });
|
||||
|
||||
// v1.15.13 — multi-select bulk soft-delete for the measurements
|
||||
// management list. Ids are scoped to the caller; forged / foreign ids
|
||||
// are silent no-ops (no existence leak). Soft-delete (tombstone), so the
|
||||
// rows surface in the `/api/sync/changes` delta feed.
|
||||
const bulkDeleteMeasurementsRequest = z
|
||||
.object({
|
||||
ids: z.array(z.string().min(1)).min(1).max(200),
|
||||
})
|
||||
.meta({
|
||||
id: "MeasurementsBulkDeleteRequest",
|
||||
description:
|
||||
"1..200 measurement ids, scoped to the caller. Forged / foreign ids are silently skipped (no existence leak).",
|
||||
});
|
||||
|
||||
const bulkDeleteMeasurementsResponse = z
|
||||
.object({
|
||||
deleted: z.number().int().nonnegative(),
|
||||
})
|
||||
.meta({ id: "MeasurementsBulkDeleteResult" });
|
||||
|
||||
const measurementResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
@@ -3444,6 +3464,34 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/measurements/bulk-delete": {
|
||||
post: {
|
||||
tags: ["Measurements"],
|
||||
summary: "Bulk soft-delete measurements",
|
||||
description:
|
||||
"Soft-deletes (tombstones) up to 200 of the caller's measurement rows in one call, mirroring the single-DELETE contract. Idempotent via the `Idempotency-Key` header; rate-limited `measurements:bulk-delete:<userId>`. Forged / foreign / already-deleted ids are silently skipped — `deleted` is the count of rows actually tombstoned. The deletions surface in `/api/sync/changes` as tombstones.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: bulkDeleteMeasurementsRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Bulk delete processed.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
bulkDeleteMeasurementsResponse,
|
||||
"MeasurementsBulkDeleteResponse",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/sleep/night": {
|
||||
get: {
|
||||
tags: ["Measurements"],
|
||||
|
||||
Reference in new issue
Block a user