From a59597e1331d5bf111f42df5624eff856ab68da6 Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Fri, 28 Aug 2026 19:57:22 +0300 Subject: [PATCH] add group purge docs --- SUMMARY.md | 1 + docs/api/bulk-api/README.md | 3 + docs/api/bulk-api/group-purge.md | 248 +++++++++++++++++++++++++++++++ docs/api/bulk-api/purge.md | 2 + docs/overview/release-notes.md | 1 + 5 files changed, 255 insertions(+) create mode 100644 docs/api/bulk-api/group-purge.md diff --git a/SUMMARY.md b/SUMMARY.md index 367025ff4..366496b22 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -201,6 +201,7 @@ * [$load and /fhir/$load](api/bulk-api/load-and-fhir-load.md) * [$import and /fhir/$import](api/bulk-api/import-and-fhir-import.md) * [$purge](api/bulk-api/purge.md) + * [Group $purge](api/bulk-api/group-purge.md) * [Aidbox.bulk data import](api/bulk-api/aidbox-bulk-data-import.md) * [Bulk import from an S3 bucket](api/bulk-api/bulk-import-from-an-s3-bucket.md) * [Batch/Transaction](api/batch-transaction.md) diff --git a/docs/api/bulk-api/README.md b/docs/api/bulk-api/README.md index a966ed1de..29fc807b9 100644 --- a/docs/api/bulk-api/README.md +++ b/docs/api/bulk-api/README.md @@ -24,6 +24,9 @@ Bulk import FHIR resources asynchronously with progress monitoring. {% card icon="trash" title="$purge" href="purge.md" %} Permanently delete a Patient and all resources in their compartment. {% endcard %} +{% card icon="trash" title="Group $purge" href="group-purge.md" %} +Permanently delete every Patient member of a Group and their compartments. +{% endcard %} {% endcards %} ## Read more diff --git a/docs/api/bulk-api/group-purge.md b/docs/api/bulk-api/group-purge.md new file mode 100644 index 000000000..432940bf2 --- /dev/null +++ b/docs/api/bulk-api/group-purge.md @@ -0,0 +1,248 @@ +--- +description: Permanently delete every Patient member of a Group and all resources in their compartments using the $purge operation. +--- + +# Group $purge + +{% hint style="info" %} +Available since version 2608. +{% endhint %} + +The `$purge` operation on `Group` permanently deletes every Patient member of the group and all resources in those patients' compartments, including all historical versions. It is the group-level counterpart of [`$purge` on Patient](purge.md) and applies exactly the same per-patient deletion. + +This operation implements the [FHIR Group Purge](https://build.fhir.org/group-operation-purge.html) specification. By default it uses the system default compartment for Patient, or a custom compartment can be passed as a parameter. + +{% hint style="danger" %} +**This deletes Group resources as well.** `Group` is part of the Patient compartment through `Group.member`, so purging a patient deletes every Group that lists them — the group you targeted, and any **other** group that happens to share a member. See [Groups are deleted too](#groups-are-deleted-too). +{% endhint %} + +## Endpoint + +```http +POST /fhir/Group//$purge +``` + +## Which members are purged + +| Member | Purged | +|------------------------------------------------------------------------------------|-----------| +| Literal reference to a Patient — `{"reference": "Patient/"}` | Yes | +| Member with `"inactive": true` | No | +| Non-Patient member — Practitioner, Device, and so on | No | +| A nested `Group` | See below | +| Logical reference — `{"type": "Patient", "identifier": {...}}` with no `reference` | No | +| `display` only, or a reference Aidbox cannot resolve to `/` | No | + +The last row covers bare ids such as `{"reference": "pt-1"}` and absolute URLs such as `{"reference": "http://example.org/fhir/Patient/pt-1"}`. Neither is a resolvable relative FHIR reference, so both are skipped. + +{% hint style="warning" %} +**Skipped members are not reported.** A successful response states that the group was purged; it does not list which members were left out. A group whose members are all logical references purges nothing and still returns 200. Verify membership before purging if that distinction matters to you. +{% endhint %} + +When no member is purgeable, nothing is deleted and the operation returns 200: + +```json +{ + "resourceType": "OperationOutcome", + "id": "informational", + "issue": [ + { + "severity": "fatal", + "code": "informational", + "diagnostics": "Group grp-1 doesn't have any purgeable members" + } + ] +} +``` + +## Groups are deleted too + +The standard Patient CompartmentDefinition includes `Group` with the search parameter `member`. Purging a patient therefore deletes every Group resource that lists that patient — including the group named in the request. + +It also deletes **unrelated groups that share a member**: + +``` +Group/cohort-a members: pt-1 +Group/cohort-b members: pt-1, pt-2 + +POST /fhir/Group/cohort-a/$purge + +pt-1 deleted +cohort-a deleted +cohort-b deleted ← never named in the request +pt-2 kept ← its group is gone, its data is not purged +``` + +`cohort-b` disappears because `pt-1` was one of its members, while `pt-2` — who was never in `cohort-a` — keeps all their data. Only the Group resources are affected; no extra patient is purged. + +Because the group itself is deleted, re-running `$purge` on the same group returns **404 Not Found**. + +### Keeping Group resources + +To purge the members without deleting any Group, pass a `compartmentDefinition` that omits the `Group` entry. Read the server's Patient CompartmentDefinition, remove the entry whose `code` is `Group`, and send the rest: + +```json +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "compartmentDefinition", + "resource": { + "resourceType": "CompartmentDefinition", + "url": "http://example.com/patient-without-group", + "name": "PatientCompartmentWithoutGroup", + "code": "Patient", + "status": "active", + "search": true, + "resource": [ + { "code": "Condition", "param": ["subject"] }, + { "code": "Observation", "param": ["subject"] } + ] + } + } + ] +} +``` + +Patients and their compartment resources are still purged, and every Group survives — including the purged one, which is left holding member references to patients that no longer exist. + +{% hint style="warning" %} +A custom compartment definition is a copy of the server's, frozen at the time you wrote it. If a later Aidbox version adds resource types to the Patient compartment, your copy will not purge them. +{% endhint %} + +## Authorization + +Authorization is checked for **every** member before anything is deleted. The first member that fails aborts the whole operation with **403 Forbidden**, and no member is purged — including the ones that were permitted. + +Two checks run per member: + +* `$purge` on `Patient/` — always. +* Search access to each resource type in the compartment — only when [`fhir.search.authorize-inline-requests`](../../reference/all-settings.md#fhir.search.authorize-inline-requests) is enabled. + +```json +{ + "resourceType": "OperationOutcome", + "id": "forbidden", + "issue": [ + { + "severity": "fatal", + "code": "forbidden", + "diagnostics": "Failed to purge Patient/pt-2: $purge on Patient is forbidden" + } + ] +} +``` + +Only the first denial is reported. With several unauthorized members you will see them one at a time as you grant access. + +## Parameters + +The request body is optional. When provided, it must be a FHIR `Parameters` resource. A body that is not a `Parameters` resource is rejected with **422**. + +| Parameter | Type | Description | +|-------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `compartmentDefinition` | resource (CompartmentDefinition) | Defines which resource types and search parameters identify compartment resources. If omitted, the standard server Patient CompartmentDefinition is used. | + +The definition must have `code` equal to `Patient` and at least one resource entry with a non-empty `param` list. + +## Basic usage + +{% tabs %} +{% tab title="Request" %} +```http +POST /fhir/Group/grp-1/$purge +Content-Type: application/json +``` +{% endtab %} + +{% tab title="Response" %} +**Status** + +200 OK + +**Body** + +```json +{ + "resourceType": "OperationOutcome", + "id": "informational", + "issue": [ + { + "severity": "fatal", + "code": "informational", + "diagnostics": "Purged all Patients in Group/grp-1" + } + ] +} +``` +{% endtab %} +{% endtabs %} + +The operation stops at the first failure. If a member cannot be purged, the request fails with **500** and the members already processed stay deleted — there is no partial-success report. + +## Async mode + +{% hint style="info" %} +Use async mode for large groups. A synchronous purge deletes every member in one request, so a group with many members can exceed your HTTP timeout. +{% endhint %} + +Include the `Prefer: respond-async` header. Members are purged in background tasks, and the response carries a `Content-Location` header pointing at the operation status. + +{% tabs %} +{% tab title="Request" %} +```http +POST /fhir/Group/grp-1/$purge +Content-Type: application/json +Prefer: respond-async +``` +{% endtab %} + +{% tab title="Response" %} +**Status** + +202 Accepted + +**Headers** + +* `Content-Location` — URL to check purge status (e.g. `/fhir/$async/`) + +**Body** + +```json +{ + "resourceType": "OperationOutcome", + "id": "informational", + "issue": [ + { + "severity": "fatal", + "code": "informational", + "diagnostics": "Purge for Group/grp-1 accepted for async processing" + } + ] +} +``` +{% endtab %} +{% endtabs %} + +Authorization is checked before any task is scheduled, so `Prefer: respond-async` does not bypass it — an unauthorized member returns 403 and no operation is created. + +Members are split across background tasks. If one task fails, the operation is marked failed and its remaining tasks are cancelled; members already purged by completed tasks stay deleted. Check the status URL as described in [Check async status](purge.md#check-async-status), and cancel with `DELETE /fhir/$async/`. + +{% hint style="info" %} +The number of concurrent async worker threads is controlled by the [`BOX_SCHEDULER_EXECUTORS`](../../reference/all-settings.md#scheduler-executors) setting (default: 4). +{% endhint %} + +## Responses + +| Status | When | +|--------|-------------------------------------------------------------------------------------------| +| 200 | Members purged, or the group has no purgeable members | +| 202 | Accepted for async processing | +| 403 | A member is not authorized to be purged — nothing is deleted | +| 404 | The group does not exist, or was already deleted by an earlier purge | +| 422 | The request body is not a `Parameters` resource, or the compartment definition is invalid | +| 500 | A member failed to purge — earlier members stay deleted | + +## Audit logging + +Both outcomes are audited with `action: "E"` (Execute), `subtype: "$purge"`, and the Group as the entity. A successful purge records `outcome: "0"`; a purge refused with 403 records `outcome: "4"`. See [How to configure audit log](../../tutorials/security-access-control-tutorials/how-to-configure-audit-log.md) for setup instructions. diff --git a/docs/api/bulk-api/purge.md b/docs/api/bulk-api/purge.md index bc080b262..e4da4190d 100644 --- a/docs/api/bulk-api/purge.md +++ b/docs/api/bulk-api/purge.md @@ -10,6 +10,8 @@ Available since version 2602. The `$purge` operation permanently deletes a Patient resource and all resources in that patient's compartment, including all historical versions. +To purge every Patient member of a group in one request, see [Group $purge](group-purge.md). + This operation implements the [FHIR Patient Purge](https://build.fhir.org/patient-operation-purge.html) specification. By default, this operation uses the system default compartment for Patient, or a custom compartment can be passed as a parameter. {% hint style="info" %} diff --git a/docs/overview/release-notes.md b/docs/overview/release-notes.md index 3ae691132..1cd97a55a 100644 --- a/docs/overview/release-notes.md +++ b/docs/overview/release-notes.md @@ -15,6 +15,7 @@ description: >- * **[Cloud SQL Java Connector support](../tutorials/other-tutorials/how-to-run-aidbox-with-cloud-sql-java-connector.md)** — connect Aidbox to Google Cloud SQL for PostgreSQL through the Cloud SQL JDBC socket factory, with IAM database authentication instead of a stored database password. * **[Correlation id in topic-based subscription notifications](../modules/topic-based-subscriptions/aidbox-topic-based-subscriptions.md#correlation-id)** — send a correlation id on a request, and Aidbox copies it into `AidboxSubscriptionStatus.notificationEvent.correlationId` on the resulting topic-based subscription notification. Configure the request header name with the `module.topics.correlation-id-header` setting. Every sender supports it, and NATS destinations also carry it as a native message header. * **[Organization $purge](../access-control/authorization/scoped-api/organization-based-hierarchical-access-control/organization-purge.md)** — delete select or all data belonging to an organization and its nested organizations. + * **[Group $purge](../api/bulk-api/group-purge.md)** — the `$purge` operation on `Group` deletes every Patient member of a group along with their compartments, synchronously or asynchronously. Authorization is checked for every member up front, so a single denial leaves the group untouched. ## July 2026 _`latest, 2607`_