Files
HealthLog/src
Marc-André Bombeck 839fa6bd62 feat(api): POST /api/workouts/batch — typed workout ingest with nested route
First write endpoint for the W8d Workout + WorkoutRoute tables.
Mirrors the v1.4.25 W10-hardened /api/measurements/batch in every
cross-cutting concern so the iOS sync engine and the deferred
Withings activity sync (W17b) can re-use the same retry / cursor
plumbing:

- requireAuth() — cookie + Bearer; narrow-scope-token safe per
  the W10 fix-C contract (a route that declares no scope admits
  any authenticated token, wildcard or otherwise).
- withIdempotency — Idempotency-Key replays the cached envelope
  on retry (24h window).
- checkRateLimit at 60 batches/min/user — 100 workouts/batch
  ceiling × 60/min = 6 000 workouts/min headroom, orders of
  magnitude past any healthy sync.
- Content-Length pre-flight at 5 MB — anything larger returns
  413 with a structured error code so the client falls back to
  one-workout-per-call.
- Per-entry status (inserted | duplicate | skipped) so the iOS
  sync cursor can checkpoint accurately.

Idempotency contract:
- HTTP-level via Idempotency-Key.
- Per-entry via @@unique([userId, source, externalId]) — Prisma
  createMany skipDuplicates absorbs duplicate-key conflicts.

Race reconciliation mirrors the v1.4.25 W10 fix for measurements
batch: under contention, createMany.count is the source of truth
for how many rows THIS request wrote, and we downgrade enough
per-entry inserted statuses to duplicate so the envelope sums
stay consistent with the aggregate counts. The iOS sync cursor
advances past both statuses identically.

Two-step write: createMany the workouts, then look up the freshly
written rows by composite key and createMany the routes by FK.
The 1:1 WorkoutRoute.workoutId unique index means route inserts
are also idempotent on re-submit.
2026-05-14 18:08:54 +02:00
..
2026-05-12 22:33:35 +02:00
2026-05-12 22:33:35 +02:00