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:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-07 19:24:16 +02:00
1 parent 0d69611bbf
commit ca262d9839
7 files changed
+984

No files matched your search

+77
View File
@@ -1508,6 +1508,18 @@ paths:
type: string
enum:
- rollup
- in: query
name: sourceEq
schema:
type: string
enum:
- MANUAL
- WITHINGS
- IMPORT
- APPLE_HEALTH
- COMPUTED
- WHOOP
- FITBIT
- in: query
name: groupBy
schema:
@@ -1698,6 +1710,31 @@ paths:
"401": *a1
"422": *a3
"429": *a4
/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:
$ref: "#/components/schemas/MeasurementsBulkDeleteRequest"
responses:
"200":
description: Bulk delete processed.
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsBulkDeleteResponse"
"401": *a1
"422": *a3
"429": *a4
/api/sleep/night:
get:
tags:
@@ -3998,6 +4035,19 @@ components:
- externalIds
description: iOS deletion-sync. Up to 500 externalIds per call; matching rows owned by other users are silently skipped
(cross-user 404 guard).
MeasurementsBulkDeleteRequest:
type: object
properties:
ids:
minItems: 1
maxItems: 200
type: array
items:
type: string
minLength: 1
required:
- ids
description: 1..200 measurement ids, scoped to the caller. Forged / foreign ids are silently skipped (no existence leak).
DeviceRegisterRequest:
type: object
properties:
@@ -7012,6 +7062,33 @@ components:
required:
- deletedCount
additionalProperties: false
MeasurementsBulkDeleteResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MeasurementsBulkDeleteResult"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MeasurementsBulkDeleteResult:
type: object
properties:
deleted:
type: integer
minimum: 0
maximum: 9007199254740991
required:
- deleted
additionalProperties: false
SleepNightResponse:
type: object
properties: