Files
HealthLog/prisma/migrations/0111_v1110_whoop_integration/migration.sql
T
Marc-André Bombeck 6db3bef413 feat(measurements): add WHOOP source, native score types, and source-priority ladders
Add the WHOOP integration data layer (migration 0111): seven new
MeasurementType values (HRV_RMSSD, DAY_STRAIN, WORKOUT_STRAIN,
SLEEP_PERFORMANCE, SLEEP_EFFICIENCY, SLEEP_CONSISTENCY, SLEEP_NEED,
ENERGY_EXPENDITURE_KJ), a WHOOP MeasurementSource, the WhoopConnection
and WhoopOAuthState models, and the per-user encrypted BYO-key columns
on User. Day-strain takes DAY_STRAIN (not the COMPUTED STRAIN_SCORE) and
RMSSD takes HRV_RMSSD (not the SDNN HEART_RATE_VARIABILITY) so a native
value never shares a bucket with a derived proxy.

Carry the new types through the completeness walls (units, value ranges,
chart tokens, PR direction, categories, list-meta colours/icons/labels,
HealthKit-mapping exclusions) and feed the cross-source picker: recovery
ladder (WHOOP native above the COMPUTED proxy), WHOOP inserted into the
overlapping vital ladders and the workout ladder, plus skinTemperature
and respiratoryRate metric keys.
2026-06-04 00:49:31 +02:00

99 lines
4.8 KiB
SQL

-- v1.11.0 — WHOOP integration (schema + data layer).
--
-- Purely-additive: two enum-extension batches, two new tables, and two new
-- nullable columns on `users`. No backfill, no existing row touched.
--
-- 1. `measurement_type` += seven WHOOP-native score classes. WHOOP day-strain
-- lands in the NEW `DAY_STRAIN` (NOT the existing COMPUTED `STRAIN_SCORE`,
-- the v1.10.3 TRIMP engine) so the native value and the derived proxy
-- never share a bucket. The RMSSD HRV gets its own `HRV_RMSSD` for the
-- same reason (distinct estimator from the SDNN `HEART_RATE_VARIABILITY`).
-- WHOOP-native Recovery reuses the existing `RECOVERY_SCORE`, distinguished
-- only by `source = WHOOP` vs `COMPUTED`.
-- 2. `measurement_source` += `WHOOP` for the server-side native ingest.
-- 3. `whoop_connections` — per-user encrypted token row (1:1 with users),
-- mirroring `withings_connections` plus `whoop_user_id`,
-- `backfill_completed_at`, and `max_heart_rate`.
-- 4. `whoop_oauth_states` — short-lived `(nonce → userId)` CSRF ledger,
-- identical in shape to `withings_oauth_states`.
-- 5. `users` += `whoop_client_id_encrypted` / `whoop_client_secret_encrypted`
-- — per-user BYO-keys (the per-app authorized-user cap makes a single
-- shared WHOOP app unworkable for a multi-operator product).
--
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
--
-- Reversibility: Postgres cannot remove an enum value, so the eight new
-- members stay; with no rows carrying them they are inert. The two tables drop
-- with `DROP TABLE IF EXISTS`, the two columns with `DROP COLUMN IF EXISTS`.
-- ── 1. measurement_type — append the seven WHOOP-native score classes ──
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'HRV_RMSSD';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'DAY_STRAIN';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'WORKOUT_STRAIN';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'SLEEP_PERFORMANCE';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'SLEEP_EFFICIENCY';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'SLEEP_CONSISTENCY';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'SLEEP_NEED';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'ENERGY_EXPENDITURE_KJ';
-- ── 2. measurement_source — append the WHOOP server-owned source ───────
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'WHOOP';
-- ── 3. whoop_connections ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS "whoop_connections" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"whoop_user_id" TEXT NOT NULL,
"access_token" TEXT NOT NULL,
"refresh_token" TEXT NOT NULL,
"token_expires_at" TIMESTAMP(3) NOT NULL,
"scope" TEXT,
"last_synced_at" TIMESTAMP(3),
"backfill_completed_at" TIMESTAMP(3),
"max_heart_rate" INTEGER,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "whoop_connections_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "whoop_connections_user_id_key"
ON "whoop_connections" ("user_id");
DO $$ BEGIN
ALTER TABLE "whoop_connections"
ADD CONSTRAINT "whoop_connections_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
-- ── 4. whoop_oauth_states ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS "whoop_oauth_states" (
"nonce" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "whoop_oauth_states_pkey" PRIMARY KEY ("nonce")
);
CREATE INDEX IF NOT EXISTS "whoop_oauth_states_expires_at_idx"
ON "whoop_oauth_states" ("expires_at");
DO $$ BEGIN
ALTER TABLE "whoop_oauth_states"
ADD CONSTRAINT "whoop_oauth_states_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
-- ── 5. users — per-user WHOOP BYO-key columns (encrypted at app level) ─
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "whoop_client_id_encrypted" TEXT;
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "whoop_client_secret_encrypted" TEXT;