Skip to content

feat(operations): add Patient $purge for GDPR erasure - #106

Merged
tnamli merged 1 commit into
masterfrom
patient-purge
Oct 30, 2026
Merged

tnamli merged 1 commit into
masterfrom
patient-purge

Conversation

@postaci

@postaci postaci commented Oct 30, 2026

Copy link
Copy Markdown

Summary

Adds the FHIR Patient $purge operation (Patient-purge, FHIR 6.0.0-ballot3) for R4 and R5, to serve GDPR erasure requests. A FHIR delete keeps the data in history; $purge removes it for good.

POST [base]/Patient/[id]/$purge permanently deletes every current and historical version of the resources in the patient's compartment, then the Patient itself, and returns 200 with an OperationOutcome giving the number of removed versions per resource type.

Also opens 4.1.0-SNAPSHOT: a new operation is additive, so this is a minor release.

Behaviour

  • Hard delete: nothing goes to history and no resource events are published. It cannot be undone.
  • Kept:
    • AuditEvent, to preserve the audit trail;
    • Group, since a shared cohort would otherwise be removed for every member.
  • Patient matched by id only: another Patient record that points at this one through link stays.
  • Retryable: the Patient is purged last, so if a run fails partway the same call can be repeated.
  • Checked up front: if a compartment search parameter isn't supported, the call answers 501 naming it and removes nothing. A server without the Patient compartment answers 405.
  • Synchronous: the asynchronous pattern is not offered.

Safety against wiping a collection

  • ResourceManager.purgeResources refuses parameters that don't resolve to a single Mongo query.
  • DocumentManager.removeDocuments refuses any query that trivially matches the whole collection ({}, {$and: []}, an $or with an empty branch, or a nesting of these). This is the last check before deleteMany.
  • A test builds the actual query for every resource type in the R4 Patient compartment and asserts that it is restricted to the patient.

Enabling it (opt-in)

Neither base definition set contains this operation. To enable it, a deployment must:

  1. copy repofyr-operations/src/main/resources/patient-purge.json into its fhir.initialization.operations-path;
  2. declare the operation on Patient in its CapabilityStatement;
  3. declare the Patient compartment under rest.compartment.

The default R5 CapabilityStatement declares no compartment, so a stock R5 setup answers 405 until it does. docker/sample-setup deliberately leaves it out.

Tests

  • FHIRPurgeOperationTest (R4 endpoint, 13 tests) checks that:
    • current and historical versions are gone, including a resource deleted beforehand;
    • a patient deleted the FHIR way can still be purged;
    • Condition and Appointment (via actor) are purged, and so is an Appointment shared with another patient;
    • the linked Patient and the other patient's data are kept;
    • a repeat call answers 404;
    • the guards against wiping a collection hold.
  • PatientPurgeOperationHandlerTest: the supported-parameter check (4 tests).
  • SmartAuthorizerTest now builds its configuration with the operations path and the default operation table, as FhirConfigurationManager does.
  • R5: no endpoint test. The module boots one server on the packaged CapabilityStatement, which R5ServerBootTest pins.

Full reactor: 351 tests, BUILD SUCCESS (repofyr-server-r4 179).

Reviewer notes

  • patient-purge.json copies HL7's ballot definition (version: 6.0.0-ballot3, status: draft). One difference: affectsState is true rather than HL7's false, so the operation can only be called with POST.
  • Severity in the OperationOutcome is information, because success exists only from FHIR 6.
  • The CHANGELOG has a new 4.1.0 - Unreleased section, and the module README documents the operation.

A FHIR delete keeps the data in history, so it cannot serve an erasure
request. Patient $purge (FHIR 6.0.0-ballot3) is now served for R4 and R5:
it hard-deletes every current and historical version of the Patient
compartment and then of the Patient, publishes no events, and returns an
OperationOutcome with the removed counts.

AuditEvent and Group are kept. The Patient is matched by id, not through
'link', and is removed last so that a failed run can be retried. Before
deleting anything the operation checks that the compartment search
parameters are supported (501 if not). A server without the Patient
compartment answers 405.

ResourceManager.purgeResources removes documents from both collections and
refuses an empty query. DocumentManager.removeDocuments now takes the
target collection and refuses any query that trivially matches the whole
collection, as a last check before deleteMany. The operation is opt-in:
repofyr-operations ships patient-purge.json, and a deployment adds it to
its operations-path and CapabilityStatement.
@postaci
postaci requested a review from tnamli October 30, 2026 07:36
@tnamli
tnamli merged commit 298db68 into master Oct 30, 2026
3 checks passed
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.

2 participants