mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
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.
99 lines
4.8 KiB
SQL
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;
|