Skip to content

feat(patients): patient data export (right of access) as a complete, machine-readable bundle - #155

Merged
afeezorobsco-cyber merged 4 commits into
DocForum:mainfrom
zainabwahab-eth:feat/40-patient-data-export
Oct 19, 2026
Merged

afeezorobsco-cyber merged 4 commits into
DocForum:mainfrom
zainabwahab-eth:feat/40-patient-data-export

Conversation

@zainabwahab-eth

Copy link
Copy Markdown
Contributor

Closes #40

What changed

Endpoints (patient-only, self-only. Doctors and admins get 403; another patient's export id returns 404, the same as an id that doesn't exist):

  • POST /patients/me/export returns 202 with a status object and a Location header. If the patient already has an export pending or processing, it returns that one instead of queuing a second (the PatientProfile row is locked FOR UPDATE, so a double-click can't create two jobs).
  • GET /patients/me/export/:id returns status, expiresAt, a machine-readable failure, and downloadUrl once ready.
  • GET /patients/me/export/:id/download streams the JSON bundle. It returns 409 EXPORT_NOT_READY until the export is ready, and 410 EXPORT_EXPIRED at or after expiresAt, even if cleanup hasn't run yet.

The download is a third route rather than a different response shape on GET /:id, so the status endpoint always returns JSON.

Exporter registry (ARCHITECTURE.md §3.1). Each module contributes its own section through its own services/. patients never queries another module's tables:

Section Owner Contents
account auth email, phone, role, emailVerifiedAt (never the password hash)
profile patients full name, DOB
appointments appointments status/origin, slot times via availabilityService.getSlotTimesByIds, doctor name via doctorsService.getDoctorSummariesByIds
payments payments order ref, status, amount (string), facility, escrow id, stellarTxHash, contract id

Sections are listed explicitly in src/export-sections.ts and registered by createApp(). To add a module later (referrals, prescriptions, ...), export a PatientExportSection from its service and add one line there. Every section uses an explicit field allowlist, so a column added later doesn't leak into exports by default.

Bundle: { format: "docforum.patient-export", schemaVersion: 1, exportId, generatedAt, sections: { <name>: { version, items } } }.

Bounded memory. Sections page with keyset pagination (PATIENT_EXPORT_PAGE_SIZE, default 200). The writer flushes about 256 KiB chunks into PatientExportChunk as it goes, and the download reads one chunk at a time with backpressure. Memory is about one page plus one chunk, however long the history is.

Lifecycle and integrity. pending → processing → ready → expired, or → failed. Every transition is a conditional UPDATE. Each claim bumps an attempt fencing token: chunks are keyed by it, and ready is conditional on it, so two racing workers generate a job exactly once and a stale or crashed claim can't publish a partial bundle. If any section throws, the whole export fails (SECTION_FAILED plus the section name, never the error message), and its partial chunks are deleted. Claims older than 15 minutes are requeued, up to 3 attempts.

Jobs (src/jobs/patient-exports.ts, started from server.ts only). Plain intervals, per ARCHITECTURE_ESSENTIALS. The worker is also woken immediately by a new request. Cleanup expires ready exports past their TTL (PATIENT_EXPORT_TTL_HOURS, default 24), deletes their chunks, and sweeps any chunk that can't be served. It touches only this module's export rows.

Schema changes (proposed per AGENTS.md)

  • New PatientExport + PatientExportChunk and enum PatientExportStatus. Migration 20261017062203_add_patient_export adds tables only; no existing table is altered.
  • Storage decision: bundles are kept in Postgres rather than object storage. No object-storage provider exists yet, and a filesystem store would break on Render's ephemeral disk and on any second instance. Recorded as ARCHITECTURE.md §6.3.4. Only patients/repositories touches the chunks, so moving to S3 later swaps that seam, not the design.

Docs

ARCHITECTURE.md §3.1 (registry, and how to register), §4.10 (models), §6.3.4 (storage trade-off); ARCHITECTURE_ESSENTIALS.md (model map, jobs); docs/api/README.md (endpoints, status object, bundle schema, env knobs); backend/src/jobs/README.md; backend/.env.example; ROADMAP.md (Phase 8 item and changelog).

Tests

  • tests/unit/patient-export.test.ts (12): bundle format and version, chunk-size bound, paging before later pages are fetched, section failure keeps only the section name, multi-byte byte accounting, registry duplicate and name rules, keyset pagination.
  • tests/integration/patient-export.test.mts (11, own embedded Postgres on port 5438):
    • in-flight dedupe under concurrent POSTs
    • doctor, admin and anonymous callers refused on all three routes; a patientProfileId in the body is ignored
    • cross-patient 404
    • full bundle contents, including tx hash and BigInt strings, with no password hash, no sentinel platform secrets, and none of another patient's rows
    • multi-chunk streaming and size accounting
    • two workers racing for one job
    • failing section: job failed, no artifact, no error text stored
    • stale-claim requeue, fencing, and the max-attempts failure
    • expiry refusal before and after cleanup; cleanup leaves unexpired exports alone
    • real job wiring wakes on a request
    • booking rows unchanged

Mutation check: dropping the owner scope from findExportForPatient makes the cross-patient test fail.

npm run typecheck        → tsc --noEmit (clean)
npm test                 → Test Files 7 passed (7) · Tests 42 passed (42)
npm run test:integration → Test Files 5 passed (5) · Tests 33 passed (33)
npm run build            → clean

These were run in Ubuntu (WSL) on Node 20 to match CI, with --hookTimeout raised locally. The existing suites' beforeAll takes more than 10s on this machine; the new suite sets its own 180s hook timeout.

Notes for reviewers

🤖 Generated with Claude Code

…machine-readable bundle

Adds POST /patients/me/export, GET /patients/me/export/:id and
GET /patients/me/export/:id/download. Owner-only: the subject is always
the authenticated patient, doctors/admins get 403, and another patient's
export id is a 404.

- Exporter registry: each module contributes its own section through its
  service (account/auth, profile/patients, appointments, payments), wired
  explicitly in src/export-sections.ts. Documented in ARCHITECTURE.md §3.1.
- Versioned JSON bundle (format, schemaVersion, exportId, generatedAt,
  sections). Payments include tx hash / escrow id; field allowlists keep
  password hashes and platform secrets out.
- Bounded memory: sections page via keyset pagination, the bundle is
  flushed as ~256 KiB chunks into PatientExportChunk and streamed back one
  chunk at a time with backpressure.
- Lifecycle pending -> processing -> ready -> expired / failed, moved only
  by conditional UPDATEs with an attempt fencing token. One in-flight job
  per patient. A failing section fails the whole export and leaves no
  artifact.
- Interval worker + cleanup in src/jobs/patient-exports.ts; downloads are
  refused at expiresAt, cleanup deletes the stored chunks.

Migration: 20261017062203_add_patient_export.

Closes DocForum#40

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@zainabwahab-eth Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

fix conflicts

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

CI is failing on this PR. main itself is green, so the cause is in this branch:

Typecheck fails: src/app.ts(18,3): Cannot find name 'registerPatientExportSections'. app.ts calls it, but the import from ./export-sections is missing, probably dropped when you merged main in. Add the import.

Please push a fix and CI will re-run. Before pushing, run npm run prisma:generate && npm run typecheck && npm test && npm run test:integration in backend/.

zainabwahab-eth and others added 2 commits October 19, 2026 00:57
Brings in DocForum#158 (AuditEvent), DocForum#147 (access-control matrix) and DocForum#150
(field-level encryption), and fixes what broke CI on the PR merge ref:

- Conflict resolution keeps both sides in app.ts, appointments, payments
  and doctors services/repositories, using DocForum#43's tx-aware
  updateVerificationStatus/markEscrowed/markStatus.
- The patient-export suite moved from port 5438 to 5442. DocForum#147's
  access-control suite also uses 5438, and the parallel integration
  files collided.
- The three /patients/me/export routes are added to DocForum#147's
  access-control matrix (KNOWN_ROUTES + expected status per actor).
- ARCHITECTURE: the export model section is renumbered to §4.11
  (§4.10 is now the audit trail).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… resolution

The web merge of main took main's side of every conflict, which dropped
DocForum#40's imports and repository functions (the CI typecheck errors). This
branch's tree already contains main plus a correct resolution, so it is
kept as-is.

@afeezorobsco-cyber afeezorobsco-cyber left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed: export is patient-only and self-only, foreign ids 404, every section is an explicit field allowlist (no password hash / throttle state), generation is paged + chunked with conditional claims, and the download streams with backpressure. CI green. LGTM.

@afeezorobsco-cyber
afeezorobsco-cyber merged commit 90d90c4 into DocForum:main Oct 19, 2026
1 check passed
Oluwaferanmi18 added a commit to Oluwaferanmi18/docforum-core that referenced this pull request Oct 19, 2026
…ng-and-facility-location

Brings in upstream DocForum#151 (Prisma 7.10.1 + driver adapter), DocForum#155 (patient
data export), DocForum#150 (field-level encryption), DocForum#147 (access-control tests).

Conflicts resolved in backend/src/app.ts (kept /facilities and
/pharmacy-orders mounts alongside /patients) and docs/api/README.md
(kept the new Facilities/Pharmacy-orders tables; facilities and
pharmacy-orders come out of the "no routes yet" list since this branch
gives them routes).

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 8] Patient data export (right of access) as a complete, machine-readable bundle

2 participants