mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
feat(import): add ImportJob model for Apple Health export ingest
Additive Prisma model mirroring a `pg-boss` `apple-health-import` job's lifecycle. The polling endpoint reads `progress` and `result` live from this row rather than the one-shot terminal `output` column on `pg_boss.job` (pg-boss v12 only populates `output` on completion). Carries `pgBossJobId` for the worker's reverse lookup, an upload sha-256 for content-hash idempotency on re-upload, and per-phase status (`queued | unpacking | parsing | upserting | done | failed`).
This commit is contained in:
1 parent
2a3fb0e913
commit
3d309bb91b
2 files changed
+105
No files matched your search
@@ -0,0 +1,45 @@
|
||||
-- v1.4.34 — Apple Health export.zip import jobs
|
||||
--
|
||||
-- Additive-only — one new table mirroring the lifecycle of a pg-boss
|
||||
-- `apple-health-import` job. The polling endpoint reads from this
|
||||
-- table so the operator sees live per-phase progress rather than the
|
||||
-- one-shot terminal `output` column on `pg_boss.job` (v12 contract).
|
||||
--
|
||||
-- `pg_boss_job_id` is nullable so the `INSERT` can land before the
|
||||
-- kick-off route gets the boss id back from `boss.send`. The unique
|
||||
-- index covers the populated rows; NULL values are distinct under
|
||||
-- the default Postgres semantics so two pre-send rows never collide.
|
||||
--
|
||||
-- Locks per `.planning/research/v1434-r-1-xml-import.md` §5.2.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "import_jobs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"triggered_by_admin_id" TEXT,
|
||||
"pg_boss_job_id" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'queued',
|
||||
"failure_reason" TEXT,
|
||||
"upload_bytes" INTEGER NOT NULL,
|
||||
"upload_sha256" TEXT,
|
||||
"exported_at" TIMESTAMP(3),
|
||||
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"progress" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"result" JSONB,
|
||||
|
||||
CONSTRAINT "import_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "import_jobs_pg_boss_job_id_key"
|
||||
ON "import_jobs" ("pg_boss_job_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "import_jobs_user_id_started_at_idx"
|
||||
ON "import_jobs" ("user_id", "started_at");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "import_jobs_status_idx"
|
||||
ON "import_jobs" ("status");
|
||||
|
||||
ALTER TABLE "import_jobs"
|
||||
ADD CONSTRAINT "import_jobs_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -246,6 +246,10 @@ model User {
|
||||
// capture (not per-shot) so delayed-onset symptoms map to their
|
||||
// actual occurrence time.
|
||||
medicationSideEffects MedicationSideEffect[]
|
||||
// v1.4.34 — Apple Health `export.zip` import jobs. Mirrors the
|
||||
// pg-boss job state so the polling endpoint can read live per-
|
||||
// phase progress without round-tripping through pg-boss internals.
|
||||
importJobs ImportJob[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -1382,6 +1386,62 @@ model DataBackup {
|
||||
@@map("data_backups")
|
||||
}
|
||||
|
||||
// ─── Apple Health export.zip import (v1.4.34) ───────────────
|
||||
//
|
||||
// Mirrors a `pg-boss` `apple-health-import` job's lifecycle so the
|
||||
// polling endpoint can read live per-phase progress without
|
||||
// round-tripping through pg-boss internals (`output` only populates
|
||||
// on completion in v12). The `pgBossJobId` link lets the worker
|
||||
// reconcile a row by the boss id it just received from `boss.send`.
|
||||
//
|
||||
// Status state machine:
|
||||
// queued → unpacking → parsing → upserting → done
|
||||
// ↘ failed
|
||||
//
|
||||
// `progress` carries the live snapshot the worker writes every
|
||||
// 1 000 records (see `ImportJobProgress` in
|
||||
// `src/lib/measurements/import-apple-health-export.ts`). `result`
|
||||
// carries the terminal `ImportJobResult` envelope.
|
||||
model ImportJob {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
/// Admin who triggered the import on behalf of `userId` (admin
|
||||
/// variant only). NULL when the user kicked off their own import.
|
||||
triggeredByAdminId String? @map("triggered_by_admin_id")
|
||||
/// `pg-boss` job id assigned by `boss.send`. Allows the worker to
|
||||
/// look up its mirror row by the boss id rather than passing the
|
||||
/// HealthLog id through the payload.
|
||||
pgBossJobId String? @unique @map("pg_boss_job_id")
|
||||
/// `queued | unpacking | parsing | upserting | done | failed`.
|
||||
status String @default("queued")
|
||||
/// Free-text reason on `failed`; null otherwise.
|
||||
failureReason String? @map("failure_reason")
|
||||
/// Bytes uploaded — surfaced to the operator audit trail.
|
||||
uploadBytes Int @map("upload_bytes")
|
||||
/// SHA-256 hex digest of the uploaded `export.zip`. Lets the
|
||||
/// kick-off endpoint short-circuit a re-upload of the exact same
|
||||
/// file back to the existing job id without re-queueing.
|
||||
uploadSha256 String? @map("upload_sha256")
|
||||
/// Apple's `ExportDate` attribute string-formatted, parsed during
|
||||
/// the unpack phase. NULL until the parser sees it.
|
||||
exportedAt DateTime? @map("exported_at")
|
||||
startedAt DateTime @default(now()) @map("started_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
/// JSON envelope mirroring `ImportJobProgress` (`src/lib/measurements/
|
||||
/// import-apple-health-export.ts`). Updated mid-run.
|
||||
progress Json @default("{}")
|
||||
/// JSON envelope mirroring `ImportJobResult`. NULL until terminal
|
||||
/// state. Carries per-type counts, workout counts, clinical /
|
||||
/// deferred / unknown buckets.
|
||||
result Json?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, startedAt])
|
||||
@@index([status])
|
||||
@@map("import_jobs")
|
||||
}
|
||||
|
||||
// ─── Rate Limiting ──────────────────────────────────────────
|
||||
|
||||
model RateLimit {
|
||||
|
||||
Reference in new issue
Block a user