Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The FHIR facade

The FHIR side of FerroBRIDGE is a REST facade in front of an openEHR CDR. A FHIR client sees one FHIR server; behind it, every request becomes CDR operations driven by FHIRconnect mappings. The facade stores no clinical data of its own.

The facade runs. It is off until [facade] enabled turns it on, and a disabled facade mounts no route, so a request answers 404 rather than 403. What this page describes is what the server answers today: conformance, create, read, vread, update, transaction and $validate. Search and batch are later work, and the CapabilityStatement leaves them out rather than claiming them.

Why a facade

FHIRconnect defines the mapping language and leaves the server surface to the implementer. Two shapes exist in the prior art: publish a facade, or leave both the facade and the CDR to the integrator. No standard prefers either. FerroBRIDGE publishes the facade so that a FHIR client needs to know about one server and one base URL, and so that the mapping direction, the terminology calls, and the CDR commit happen in one place that can refuse a request as a whole.

The surface

FHIR R4 (4.0.1), because that is the only version the FHIRconnect schemas admit for spec.version. The service base is /fhir.

RequestWhat it does
GET /fhir/metadataThe CapabilityStatement of the loaded mapping set
POST /fhir/{type}Create, conditional through If-None-Exist
POST /fhir/{type}/$validateThe dry run that commits nothing
GET /fhir/{type}/{id}Read one resource back out of its composition
GET /fhir/{type}/{id}/_history/{vid}Vread: read the version a write’s Location named
PUT /fhir/{type}/{id}Update, with If-Match
POST /fhirA transaction Bundle, all or nothing

A type no loaded program maps is absent from the CapabilityStatement and answers 404 with a not-supported OperationOutcome on the wire. A batch Bundle answers 422 with not-supported. Search has no route (issue #98 lands it), and a _count that is not a non-negative integer is a 400 with invalid, so a client never reads a page it did not ask for.

The CapabilityStatement names exactly the resource types the loaded programs map, with create, read, vread and update per type, transaction at system level, the $validate operation, updateCreate: false, conditionalCreate and conditionalUpdate true, fhirVersion: 4.0.1, and no search parameter. When the FHIRconnect operations lane is served under the same base, rest.operation also names $tofhir and $toopenehr; their direct forms are never declared (The FHIRconnect operations says why).

Media types and bodies

The facade reads application/fhir+json and application/json, which R4 names as the JSON media type and its alias, and answers 415 for anything else. Every response carries application/fhir+json. An Accept header that admits neither is a 406.

A body is parsed through the strict codec of the generated fhir-types crate, so an unknown property is a 400 with structure rather than a value that is silently dropped. Prefer: return=minimal, return=representation and return=OperationOutcome are honoured on a write, and the default is the resource itself.

Everything the facade authors is an OperationOutcome with an issue.code from the R4 value set. An openEHR error body never reaches the wire as its own document: the CDR’s message and every validationErrors entry travel verbatim inside issue.diagnostics, which keeps one error vocabulary on the wire while losing nothing the CDR said.

Program selection

A request is mapped by the program whose context claims the profiles the resource declares in meta.profile. Every declared profile is considered, as set membership, and the templateId pin of the FHIRconnect operations surface selects among candidates when a deployment sends one.

Two refusals are distinct on the wire, and both are 422. When no program claims the resource, the outcome names the profiles it saw and answers not-supported. When several programs claim it and nothing pins the template, the outcome names the candidates, so the fix is a pin rather than a guess.

Identity

A FHIR resource id has to be stable across composition versions, because R4 fixes that “once assigned, this value never changes”. The facade derives it from the entry’s LOCATABLE.uid when the entry carries one that the R4 id grammar admits, and otherwise from a SHA-256 over the version container, the entry path and the split occurrence, rendered as 52 lowercase base32 characters. meta.versionId carries the openEHR version_tree_id and never enters the derivation, so a new version of a composition reads back under the same id with a new meta.versionId. meta.source names the openEHR version uid the answer was read from.

The identity map is a redb file with seven tables: a patient identifier to its ehr_id and each ehr_id back to the first person recorded for it, an external resource id to the internal one, an internal id to the composition version container with the entry path and split occurrence, and the source resource id with its meta.versionId to the mapping that consumed them, beside the contribution each source was committed in and each Resource.identifier of a committed resource. The map wins once written, so a derivation change never renames a resource a client already holds. The store keeps identifiers and nothing else: no clinical content is written into it, and a test greps the file for a mapped value to prove it.

That last table is what makes create idempotent. A resource re-sent with the same id and meta.versionId commits nothing and answers 200 OK with the composition it already produced. A resource with a known id and a new meta.versionId commits a later version of that composition. A resource with a known id and no meta.versionId is a replay when it maps to the content the composition holds now, and a later version otherwise. A transaction entry with a known id and a new meta.versionId commits that later version inside the Bundle’s contribution. The Operate page on failure and identity states the full rule.

A read is a valid instance

Every resource the facade answers, on a read, a vread or a write under Prefer: return=representation, is a valid R4 instance: each element the R4 element table marks min 1 is present, at the top of the resource and inside every element and contained resource it carries. A mapping often has no outbound row for the subject, because the ingest reads the subject to find the EHR and the composition does not hold it. When the rendered resource has no subject (or patient, on a type that names its subject so), the facade writes it from the identity map: the person the map recorded for the composition’s EHR, as a literal reference when the person was keyed by one in subject_namespace, and otherwise as a logical reference with type: Patient and the person’s identifier. That is the form a create reads back into the same EHR, so you can send a read body back as an update unchanged. A subject the mapping wrote is kept. The log line of the request counts the elements the facade filled and names their paths, never a value. When a required element stays absent, the facade answers 500 exception with the element path in issue.location rather than an invalid resource. No specification governs the fill: it is FerroBRIDGE’s own design.

Provenance

Every composition the facade commits carries a FEEDER_AUDIT, which is the reference model’s own element for data transformed into openEHR form: the source resource’s id and type as an originating_system_item_ids entry, its meta.versionId as originating_system_audit.version_id, and the configured system_id naming the bridge. A source resource that carries no id is recorded as unknown, never given one. So a reader of the CDR can tell which FHIR resource a composition came from without asking the bridge.

Writes

A create resolves the subject to an EHR: the identity map first, then the CDR by subject id and namespace, and only then the configured policy. With ehr_policy = "existing" an unknown subject is refused; with create_on_first_write the facade creates the EHR and records it.

If-None-Exist behaves as R4 defines it: no match creates, one match answers 200 with the resource that already exists, and several matches answer 412. The search it runs is over the identity map, so the parameters it answers are _id and identifier, each in the comma-separated OR form. identifier matches the Resource.identifier of every resource the facade committed, in the token forms [code], |[code] and [system]|[code]; the [system]| form is refused. Any other parameter is refused, because a silently narrowed search would turn a duplicate into a second composition.

A create answers 201 with Location naming the FHIR resource under the configured base URL and ETag carrying the version.

That Location is [base]/{type}/{id}/_history/{vid}, and a GET of it is the R4 vread. The {vid} is the version tree id of the composition version the write committed, so the facade reads that exact version from the CDR and answers it with its ETag. A {vid} the CDR does not hold answers 404, and a version the CDR reports deleted answers 410.

An update needs If-Match when the map knows the id. Without one, the facade reads the CDR’s current ETag and answers 412 with it on a mismatch rather than overwriting a version the client never saw. Send back the ETag a read or write answered, If-Match: W/"1": the facade completes that versionId to the current composition version and answers 412 with the current ETag when it is stale. A value that names no version is 400. A PUT to an id the map does not know is a 404: this milestone does not upsert, and the CapabilityStatement says updateCreate: false.

A transaction Bundle maps every entry first and commits the lot through one ITS-REST contribution so the CDR makes it atomic. The facade uses fullUrl in two places. It names each entry by its fullUrl in every refusal, or by its position when it has none. And a subject or patient reference whose value is another entry’s fullUrl is read through that entry: the person is that entry’s first identifier with a value. The facade resolves no other reference. A reference is mapped as the resource carries it, and the facade never fetches a referenced resource from anywhere. Any failure answers one OperationOutcome naming every failing entry, and nothing is committed.

A committed transaction answers 200 with a transaction-response Bundle, one entry per request entry in the same order. Each entry’s response carries 201 Created, the location [base]/[type]/[id]/_history/[vid] and the etag W/"[vid]", the values a single create answers. Send the same Bundle again and nothing is committed: each entry answers 200 OK with the location of the resource the first delivery created. A Bundle only some of whose entries an earlier delivery consumed is refused with 409, naming those entries, and a Bundle that carries one resource twice is refused with 422. A delivery that arrives while another delivery of the same Bundle or resource is still in flight is refused with 409 and commits nothing; retry it after the first one answers. The redelivery rule is on the failure and identity page.

An HL7 v2 message that the HL7 v2 face receives goes through this same path as a transaction of its Bundle’s entries, so the same identity and replay rules apply to it. Its entries are keyed by the message’s MSH-10 and message type and their position in the Bundle, so a message sent again commits nothing and is answered AA, as its first delivery was. A message whose delivery overlaps another of the same message is refused and commits nothing. Each composition’s FEEDER_AUDIT names the message where a resource would be named here. A subject reference to another entry of the Bundle is read through that entry: the person is the referenced resource’s first identifier, the same pair a subject.identifier names.

$validate is a dry run of this server

POST /fhir/{type}/$validate runs the whole inbound path up to the commit and commits nothing, which a test proves by counting EHRs and versions before and after. It answers 200 for both verdicts: information issues when the resource would commit, and an error issue carrying the validator’s message verbatim when it would not. The operation-level statuses mirror the create path, so a 415, a 422 for an unmapped profile or a 404 for an unknown type reads the same on both routes.

The validator is this server, not the CDR. openEHR ITS-REST 1.1.0 documents no composition-validation route, and a commit the bridge never finalises would still write a version, so what runs is the mapping engine, the Web Template validation the composition build performs, and the strict RM reader. Every outcome says so in issue.details, so you can read what a verdict covers. A CDR that refuses a composition this validator accepts still answers 422 on the create.

Status mapping

No specification governs what a CDR answer becomes on the FHIR wire, so the mapping is FerroBRIDGE’s own design. It lives in one table, each row citing both sides, and one wire test asserts each row.

The CDR answersThe facade answers
422, template validation422 with validationErrors verbatim in issue.diagnostics
412412 carrying the current ETag
204 on a read, the composition is deleted410
404404
401401, propagating WWW-Authenticate, never 403
400, 405 or 415500: the bridge chose a call the CDR does not offer
any 5xx502 carrying the upstream status
no answer: a connect failure or a timeout502

Paths are written as well as read

The engine is a bidirectional path model over FHIR JSON, because a mapping has to construct a resource as well as read one. The model is guided by the element table of the generated fhir-types crate, so it knows which elements repeat, which are choice types, and which alternatives a choice admits. The three read-side FHIRPath forms the published mappings use (ofType() and as(), extension(url), resolve()) are path-model operations; no FHIRPath evaluator is embedded. FHIRconnect’s ^ parent operator and $fhirRoot are not FHIRPath, and the path model resolves them when the mapping is compiled.

Terminology

The facade calls a configured FHIR terminology server for three operations: $lookup to resolve a code’s display, $translate for a conceptmap reference, and $validate-code. The $lookup case is the common one, because openEHR requires DV_CODED_TEXT.value while FHIR leaves display optional. A terminology failure is an error, never a coding with a missing display.

The facade does not call the terminology server yet. The call site is the inbound engine seam, and it runs the calls before anything is built, so a terminology refusal will cost no CDR write. It arrives with the registry of mapping functions in issue #95; until then a mapping that needs a display resolved refuses the unit rather than writing a coding without one.

Search is FerroBRIDGE’s own design

FHIRconnect states that it “does not focus specifically on AQL and FHIRsearch”, so no specification governs FHIR search here. The design is FerroBRIDGE’s own, and it is labelled as such wherever it appears:

  • An AQL projection per resource type, declared beside the context mapping.
  • Execution over POST /query/aql on the CDR.
  • A CapabilityStatement that names exactly which search parameters each resource supports.
  • An OperationOutcome refusing every parameter outside that list, rather than ignoring it.

A refusal is deliberate. A search server that silently drops an unsupported parameter returns a result set that looks filtered and is not, which is the kind of quiet wrongness this project treats as a defect.

What the facade does not do

It does not materialise a FHIR store, it does not implement Subscriptions, and it does not send demographics to an external endpoint. Each of those is recorded as outside the current design, with its reasoning, on the tracker.