Repository navigation
feat(patients): patient data export (right of access) as a complete, machine-readable bundle - #155
Conversation
…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>
|
@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! 🚀 |
|
fix conflicts |
|
CI is failing on this PR. Typecheck fails: Please push a fix and CI will re-run. Before pushing, run |
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
left a comment
There was a problem hiding this comment.
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.
…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>
Closes #40
What changed
Endpoints (patient-only, self-only. Doctors and admins get
403; another patient's export id returns404, the same as an id that doesn't exist):POST /patients/me/exportreturns202with a status object and aLocationheader. If the patient already has an export pending or processing, it returns that one instead of queuing a second (thePatientProfilerow is lockedFOR UPDATE, so a double-click can't create two jobs).GET /patients/me/export/:idreturns status,expiresAt, a machine-readablefailure, anddownloadUrlonce ready.GET /patients/me/export/:id/downloadstreams the JSON bundle. It returns409 EXPORT_NOT_READYuntil the export is ready, and410 EXPORT_EXPIREDat or afterexpiresAt, 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/.patientsnever queries another module's tables:accountemailVerifiedAt(never the password hash)profileappointmentsavailabilityService.getSlotTimesByIds, doctor name viadoctorsService.getDoctorSummariesByIdspaymentsstellarTxHash, contract idSections are listed explicitly in
src/export-sections.tsand registered bycreateApp(). To add a module later (referrals, prescriptions, ...), export aPatientExportSectionfrom 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 intoPatientExportChunkas 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 anattemptfencing token: chunks are keyed by it, andreadyis 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_FAILEDplus 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 fromserver.tsonly). Plain intervals, per ARCHITECTURE_ESSENTIALS. The worker is also woken immediately by a new request. Cleanup expiresreadyexports 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)
PatientExport+PatientExportChunkand enumPatientExportStatus. Migration20261017062203_add_patient_exportadds tables only; no existing table is altered.patients/repositoriestouches 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):patientProfileIdin the body is ignored404Mutation check: dropping the owner scope from
findExportForPatientmakes the cross-patient test fail.These were run in Ubuntu (WSL) on Node 20 to match CI, with
--hookTimeoutraised locally. The existing suites'beforeAlltakes more than 10s on this machine; the new suite sets its own 180s hook timeout.Notes for reviewers
ExportSubjectcarriespatientProfileIdexplicitly so it can be added without changing the registry.backend/src/modules/audit/. There's no overlap with this PR.🤖 Generated with Claude Code