feat(api): DELETE /api/measurements/by-external-ids for iOS deletion-sync

HealthKit's API does not surface a stream of deleted UUIDs, so the iOS
HealthLog client performs a periodic 30-day reconciliation: it diffs
HealthKit's current UUID set against the server's externalIds and
posts the surplus here. This endpoint deletes the user's matching
rows and never touches another user's data (Prisma's deleteMany with
the userId predicate is the cross-user 404 guard).

Contract:
  - Body: { externalIds: string[] } with min 0, max 500 entries.
  - Auth: requireAuth() — session cookie or bearer.
  - Empty array: returns { deletedCount: 0 } with 200 (no-op, not a
    failure — matches the ingest path's symmetric tolerance).
  - Over-cap: 422 with errorCode "measurement.delete.too_large" so
    the iOS client can surface a specific diagnostic.

Integration coverage: happy path, cross-user 404 guard, empty array,
and batch-cap rejection. The cap (500) mirrors POST /measurements/batch
so the two surfaces share one operational ceiling.

OpenAPI: registered as DELETE on /api/measurements/by-external-ids
with the Measurements tag; req/res schemas reuse the same z.array cap
the runtime validator enforces. openapi.yaml regenerated.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 13:07:05 +02:00
1 parent 57bd4453ba
commit 959adec899
4 files changed
+411

No files matched your search

+67
View File
@@ -538,6 +538,49 @@ components:
- measuredAt
- source
type: object
MeasurementsDeleteByExternalIdsRequest:
description:
iOS deletion-sync. Up to 500 externalIds per call; matching rows owned by other users are silently skipped
(cross-user 404 guard).
properties:
externalIds:
items:
maxLength: 120
minLength: 1
type: string
maxItems: 500
minItems: 0
type: array
required:
- externalIds
type: object
MeasurementsDeleteByExternalIdsResponse:
additionalProperties: false
properties:
data:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResult"
error:
type: "null"
meta:
additionalProperties: false
properties:
requestId:
type: string
type: object
required:
- data
- error
type: object
MeasurementsDeleteByExternalIdsResult:
additionalProperties: false
properties:
deletedCount:
maximum: 9007199254740991
minimum: 0
type: integer
required:
- deletedCount
type: object
PasskeyLoginVerifyRequest:
description: Passkey assertion verification. Same native-client token issuance as the password path.
properties:
@@ -1152,6 +1195,30 @@ paths:
summary: Apple Health batch ingest
tags:
- Measurements
/api/measurements/by-external-ids:
delete:
description:
Removes the user's measurement rows whose externalId is in the request list. Rows owned by another user are
silently skipped (cross-user 404 guard). Up to 500 externalIds per call.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsRequest"
required: true
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/MeasurementsDeleteByExternalIdsResponse"
description: Delete batch processed.
"401": *a1
"422": *a2
"429": *a3
summary: Delete measurements by external ID (iOS deletion-sync)
tags:
- Measurements
servers:
- description: Production
url: https://healthlog.bombeck.io