Measurement and workout batch ingest now fire a pr-detection job
after each successful insert. Batches above 50 entries propagate
silent=true so a multi-year Apple Health or HKWorkout backfill
writes the records without spamming hundreds of pushes. Both
enqueue calls are wrapped in audit-log entries and a swallow-and-
annotate failure path so a flaky queue cannot 500 an ingest call —
the 30-minute fallback cron picks the user up in any case.
Unit + integration coverage for POST /api/workouts/batch.
Unit (database-free, mocks the Prisma layer):
- 401 on unauthenticated calls
- 413 when Content-Length exceeds the 5 MB ceiling, with the
documented workout.batch.payload_too_large error code
- 429 when the rate-limit gate blocks the call
- 400 when the workouts array exceeds the 100-entry cap
- 400 when the Zod schema rejects the payload
- 400 when a route exceeds the 20 000-point cap
- Fresh entries marked inserted
- Pre-existing entries flagged duplicate without re-inserting
- Race-reconciliation downgrades inserted statuses to duplicate
when createMany.count is below the attempted-row count
- WorkoutRoute rows attached by FK after the workout createMany
Integration (real Postgres in a testcontainer):
- Happy-path single workout with nested route lands as a Workout
row + WorkoutRoute row + 1:1 FK + computed durationSec
- 100-workout batch with mixed routes lands all 100 workouts and
exactly the expected number of route rows
- Over-cap batch (101 workouts) returns 400 with
workout.batch.too_large
- Over-cap route (20 001 points) returns 400 with
workout.batch.invalid
- Re-posted batch surfaces as duplicates without inserting twice
- Idempotency-Key replays the cached envelope and sets the
X-Idempotent-Replay header
- Pre-seeded rate-limit row triggers 429 and writes nothing
- Narrow-scope Bearer token admitted on a route that declares no
required permission (the W10 fix-C correctness fix the iOS
narrow-scope sync depends on)
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.