Files
HealthLog/prisma/migrations/0218_v125_inbound_documents/migration.sql
T
Marc-André Bombeck 5280c183bd feat(documents): add inbound clinical-document data model (migration 0218)
Store an uploaded doctor report / discharge letter and stage extracted
structured facts for review. Two models:

- InboundDocument: the uploaded document, raw bytes encrypted at rest via
  the shared AES-256-GCM Bytes codec (content_encrypted), with queryable
  metadata (kind, mime, size, status, report date).
- ExtractedFact: the review-then-confirm staging row — fact type, status,
  per-field provenance, a confidence score, and a needs-review gate, plus
  the FHIR-staged payload (stated codes/status only) as JSONB.

Wire content_encrypted into the encryption-key rotation registry and the
rotation script so a key drop never strands a stored document.
2026-06-28 16:56:54 +02:00

131 lines
4.8 KiB
SQL

-- v1.25 (W-DOCS-IN) — inbound clinical documents + extracted-fact staging.
--
-- A self-hoster uploads a doctor report / discharge letter they received.
-- The raw document is stored encrypted at rest (BYTEA, the AES-256-GCM
-- `*_encrypted` convention shared with the illness / mood / lab note
-- columns). The dedicated OCR/vision provider transcribes STRUCTURED FACTS
-- into a review-then-confirm staging area (`extracted_facts`). The app
-- reproduces what the clinician wrote — it never interprets, never
-- range-flags, never diagnoses, never links a condition to a medication. A
-- fact reaches the structured stores (labs / conditions / medications) only
-- on explicit user approval; low-confidence facts fail closed.
--
-- `mime_type`, `byte_size`, `provider_type`, `report_date` stay queryable
-- plaintext metadata; `content_encrypted` holds the sensitive original. The
-- per-fact `data_json` / `provenance_json` carry the FHIR-staged fields
-- (stated codes only) and the source-span provenance.
--
-- Additive; no existing row touched. Idempotent guards (`IF NOT EXISTS` /
-- `DO $$`) so reruns are safe on prod.
--
-- Reversibility (down):
-- DROP TABLE IF EXISTS "extracted_facts";
-- DROP TABLE IF EXISTS "inbound_documents";
-- DROP TYPE IF EXISTS "extracted_fact_status";
-- DROP TYPE IF EXISTS "extracted_fact_type";
-- DROP TYPE IF EXISTS "inbound_document_status";
-- DROP TYPE IF EXISTS "inbound_document_kind";
-- A roll-back drops the inbound-document store wholesale — facts already
-- approved into labs / conditions / medications are independent rows and are
-- NOT affected (the staging link is one-way).
-- CreateEnum
DO $$ BEGIN
CREATE TYPE "inbound_document_kind" AS ENUM ('DOCTOR_REPORT', 'DISCHARGE_LETTER', 'OTHER');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- CreateEnum
DO $$ BEGIN
CREATE TYPE "inbound_document_status" AS ENUM ('EXTRACTING', 'EXTRACTED', 'FAILED', 'CONFIRMED', 'DISCARDED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- CreateEnum
DO $$ BEGIN
CREATE TYPE "extracted_fact_type" AS ENUM ('CONDITION', 'OBSERVATION', 'MEDICATION_STATEMENT');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- CreateEnum
DO $$ BEGIN
CREATE TYPE "extracted_fact_status" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- CreateTable
CREATE TABLE IF NOT EXISTS "inbound_documents" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"kind" "inbound_document_kind" NOT NULL DEFAULT 'OTHER',
"filename" TEXT,
"mime_type" TEXT NOT NULL,
"byte_size" INTEGER NOT NULL,
"content_encrypted" BYTEA NOT NULL,
"status" "inbound_document_status" NOT NULL DEFAULT 'EXTRACTING',
"provider_type" TEXT,
"report_date" TIMESTAMP(3),
"error_reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "inbound_documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE IF NOT EXISTS "extracted_facts" (
"id" TEXT NOT NULL,
"document_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"fact_type" "extracted_fact_type" NOT NULL,
"status" "extracted_fact_status" NOT NULL DEFAULT 'PENDING',
"confidence" DOUBLE PRECISION NOT NULL DEFAULT 0,
"needs_review" BOOLEAN NOT NULL DEFAULT true,
"data_json" JSONB NOT NULL,
"provenance_json" JSONB NOT NULL,
"committed_record_id" TEXT,
"committed_record_type" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "extracted_facts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX IF NOT EXISTS "inbound_documents_user_id_created_at_idx" ON "inbound_documents"("user_id", "created_at");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "inbound_documents_user_id_deleted_at_idx" ON "inbound_documents"("user_id", "deleted_at");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "extracted_facts_document_id_idx" ON "extracted_facts"("document_id");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "extracted_facts_user_id_idx" ON "extracted_facts"("user_id");
-- AddForeignKey
DO $$ BEGIN
ALTER TABLE "inbound_documents" ADD CONSTRAINT "inbound_documents_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- AddForeignKey
DO $$ BEGIN
ALTER TABLE "extracted_facts" ADD CONSTRAINT "extracted_facts_document_id_fkey" FOREIGN KEY ("document_id") REFERENCES "inbound_documents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- AddForeignKey
DO $$ BEGIN
ALTER TABLE "extracted_facts" ADD CONSTRAINT "extracted_facts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;