Failure and identity behaviour
A bridge carries clinical data between systems, so a silently dropped element or a swallowed upstream error becomes a wrong record in the receiving system. FerroBRIDGE fails loudly instead. The FHIRconnect engine chapter states its guidance as recommendations, so each rule below is pinned as FerroBRIDGE’s own decision, and this page is the operator-facing statement of it.
- All or nothing per unit
- Type coercion is strict
- An upstream failure stays a failure
- Unmapped OMOP codes are recorded, never dropped
- Identity
- A re-sent transaction commits once
- A re-sent single create commits once
- An HL7 v2 sender and receiver become MessageHeader endpoints
- Version mismatch is refused at load time
- Programmed mappings are compiled in
All or nothing per unit
A transaction Bundle that cannot be mapped in full is refused with an
OperationOutcome naming every failing entry, and nothing is committed. A
batch Bundle answers per entry and counts its failures, because FHIR R4 defines
batch that way. There is no best-effort mode. On the OMOP side the unit is one
composition’s whole record graph: a composition becomes many linked rows, and
they commit together or not at all, so no FACT_RELATIONSHIP row ever points
at a record that was never written.
Type coercion is strict
A value that does not parse as the RM leaf type the Web Template resolved is a refusal. FerroBRIDGE does not round a number, truncate a string, or guess a date format to make a value fit.
An upstream failure stays a failure
A refused CDR call, a failed terminology lookup, or a timeout is reported with
the upstream status. It never becomes an empty value, a default, or a silently
missing element. When a display cannot be resolved through $lookup, you get
an error that says so, not a resource with a coding missing its display.
Unmapped OMOP codes are recorded, never dropped
A source code with no standard concept lands as concept_id = 0 with the
source value kept in its source column. That is the CDM’s own answer for “no
matching concept”, and it keeps the row auditable. A row is never discarded
because its code did not resolve, and every run reports how many codes landed
there, per mapping, because the reference implementation’s own authors measured
8.65% of primary concepts doing so and called that an underestimate.
Identity
On the FHIR side an exported resource id is derived deterministically, as the
specification recommends, from the composition’s stable versioned_object_uid,
the entry path and the split occurrence, so it stays the same across
composition versions, which FHIR requires of a logical id; meta.versionId
carries the openEHR version. FerroBRIDGE also keeps a persistent id-map: the
patient identifier to ehr_id relation, the external to internal resource id
relation, and the resource id to composition relation, so a PUT resolves and
a re-sent Bundle is recognised. It also records each Resource.identifier a
committed resource carries against its resource id, which is what a
conditional create’s identifier search reads.
A re-sent transaction commits once
Every entry the facade commits records the source it consumed. A resource is
keyed by its resourceType, id and meta.versionId, the same key a single
create uses. A message is keyed by its type, its control id and the entry’s
position in the Bundle, so a redelivered message is recognised even when it is
rebuilt with fresh resource ids. When every entry of a Bundle was consumed
before, the Bundle commits nothing and answers the resources the first delivery
created. When only some were, the Bundle is refused with 409 and nothing is
committed. An entry without an id has no key, so its Bundle commits again
each time it is sent. These rules are FerroBRIDGE’s own design.
The CDR does not say in which order it lists the versions of a contribution.
After the commit, FerroBRIDGE reads each version back and matches it to the
entry it came from by the FEEDER_AUDIT the engine wrote: the item’s type,
its id and its version. Where several entries name the same item, as the
entries of one message do, the composition content decides. That comparison
masks what two mappings of one entry can differ in without the source
changing: the composition uid the CDR assigns, and every time the engine
filled from its clock because no mapping wrote it. The engine reads the clock
once per resource and hands that instant to the builder as the ctx/time
default, which fills EVENT_CONTEXT.start_time, HISTORY.origin,
EVENT.time and ACTION.time where the mapping left them empty (ITS-REST
Simplified Formats, master06 §time). Each DV_DATE_TIME whose value is that
instant is left out of the comparison, so a Bundle mapped again at a later
instant still matches the compositions its first delivery stored. A version that
matches no entry or more than one refuses the answer with a 500 that names
the contribution, and no binding is recorded.
The commit is two steps. First the facade commits the contribution with
Prefer: return=representation and records the contribution uid the CDR
answers against every keyed entry of the Bundle, before it binds any of them.
Then it binds each entry from the versions the answer lists. A CDR that
ignores the header answers an empty 201 that still names the contribution,
and the facade then reads the contribution back with
GET /ehr/{ehr_id}/contribution/{contribution_uid} and binds from its
versions, with the same FEEDER_AUDIT check. A contribution that cannot be
read back or bound is a 500 naming the contribution, and the recorded uid
stays. When you send the same Bundle again, the facade finds that uid,
commits nothing, reads the contribution back and binds from it, and answers
200 OK for each entry as for any re-sent Bundle.
A single create shares that rule. When you POST a resource whose id and
meta.versionId a transaction committed and did not finish binding, the
facade reads the recorded contribution back, finds the version whose
FEEDER_AUDIT names the resource, binds it, commits nothing, and answers
200 OK with the composition the first delivery produced. The other versions
of that contribution belong to the transaction’s other entries and are passed
over.
A re-sent single create commits once
A single create (POST [base]/{type}) of a resource whose id the identity
map already consumed follows the same rule as a re-sent transaction, keyed by
the same resourceType, id and meta.versionId. FHIR R4 says nothing about
a repeated create with a client-assigned id, so these rules are FerroBRIDGE’s
own design.
- The same
idand the samemeta.versionId: the create is a replay. The facade commits nothing and answers200 OKwith theLocation, theETagand the body of the composition the first delivery produced, at the version it stands at now. A re-sent Bundle gets the same answer per entry. - The same
idand anothermeta.versionId: a changedmeta.versionIdmeans the sender changed the resource, so the create commits a later version of the composition the first delivery produced and answers200 OK. That is the update aPUTperforms, following the version the CDR holds now. - The same
idand nometa.versionId: the key has no version to compare, so the facade maps the resource and compares the result with the composition as the CDR holds it now. The comparison masks the same fields as the transaction path: the compositionuidand every time the engine filled from its clock. The same content is a replay, answered as above. Other content is the later version. - An
idthe map binds to more than one composition: the facade cannot tell which one the create revises, so it answers409 Conflictwith aconflictissue and commits nothing.
A transaction entry follows the same lookup. An entry whose exact key the map
consumed, or a transaction committed, is recognised as above. An entry whose
id the map consumed at another meta.versionId commits a later version of
that composition inside the Bundle’s contribution: its UpdateVersion names
the composition’s latest version as preceding_version_uid and its audit
states a modification, beside the creations of the other entries. The entry
answers 200 OK in the transaction-response, keeps its resource id, and its
Location names the new version. The commit is recorded before the binding
and matched by FEEDER_AUDIT as for any entry, so a retry after a failed
binding reads the contribution back and commits nothing. An entry the map
binds to more than one composition, or to a composition in another EHR,
refuses the Bundle with 409, and two entries that revise one composition
refuse it with 422. PUT is unchanged.
Two deliveries of one source that overlap commit once. Before a delivery reads
the identity map, it claims the key of every entry it carries, and it holds
those claims until it answers, whether it commits, is refused, or fails. A
second delivery that finds any of its keys claimed commits nothing and answers
409 Conflict with an OperationOutcome whose duplicate issues name each
entry another delivery holds. The rule is the same for a transaction and for a
single create of a resource with an id, and a single create also claims the
resourceType and id alone, so two creates of one id at different
versions do not run at once. Retry after the first delivery has
answered: the retry is then recognised as a re-sent Bundle or resource, as
described above. The claims live in the server process, one set per identity
store, so they order deliveries that reach the same FerroBRIDGE instance.
On the OMOP side every CDM v5.4 primary key is a 32-bit integer, so ids come
from database sequences and a bridge-owned side table maps each source record
to its row. That table is what makes a re-run replace rather than duplicate,
and it keeps the openEHR identity in the *_source_value columns.
The FHIR scheme has a failure mode worth knowing before you run it: a re-sent Bundle that omits one resource reads as a different mapping. It is documented rather than hidden.
An HL7 v2 sender and receiver become MessageHeader endpoints
A FHIR R4 message Bundle opens with a MessageHeader
(https://hl7.org/fhir/R4/bundle.html#invs, bdl-12), whose
source.endpoint and destination.endpoint are required url values
(https://hl7.org/fhir/R4/messageheader.html). The v2-to-FHIR guide writes
MSH-3 (Sending Application) and MSH-5 (Receiving Application) there, and both
are HD values. FerroBRIDGE builds the endpoint from the HD in this order:
- When HD.3 (the universal ID type) is
ISO,UUID,DNSorURIand HD.2 (the universal ID) is valued, the endpoint is the form the guide’sHDendpoint map assigns:urn:oid:,urn:uuid:,urn:dns:orurn:uri:followed by HD.2. - Otherwise, or when that form is no valid
url, the endpoint is derived:urn:ferrobridge:hl7v2-hd:followed by HD.1 (the namespace ID), and, when HD.2 or HD.3 is valued,:HD.2:HD.3. Every byte outside the RFC 3986pcharset is percent-encoded, and so is:inside a component, soNorth Lab Appbecomesurn:ferrobridge:hl7v2-hd:North%20Lab%20App.
HD.1, when valued, is also written to source.name or destination.name, so
the application name stays readable. No specification governs the derived
form: it is FerroBRIDGE’s own design. The guide leaves an HD without a
typed universal ID to the implementer, a v2 application name often holds
spaces a url cannot carry, and the ferrobridge prefix marks the value as
derived so nobody reads it as an identifier the sender assigned. The same
HD always yields the same endpoint.
When MSH-3 and MSH-24 (Sending Network Address) are both empty and MSH-4
(Sending Facility) is valued, FerroBRIDGE builds source.endpoint and
source.name from the MSH-4 HD by the same two rules. The guide still
writes MSH-4 into MessageHeader.sender as an Organization, and that stays.
The destination side works the same way: with MSH-5 and MSH-25 (Receiving
Network Address) empty, MSH-6 (Receiving Facility) gives
destination.endpoint and destination.name. Each fallback is counted as a
facility-endpoint outcome naming the facility field and the element it
filled. The guide’s MSH-3 and MSH-24 rows leave a message valuing neither to
the implementer, so the fallback is FerroBRIDGE’s own design.
A message that names its sender in none of MSH-3, MSH-24 and MSH-4 is
mapped. The guide’s MSH-24 row writes the data-absent-reason extension with
the code unknown into source.endpoint for it, and FHIR R4 JSON carries a
primitive with an extension and no value as the _endpoint property alone
(https://hl7.org/fhir/R4/json.html#primitive), which satisfies the required
endpoint. When MSH-4 is valued, its endpoint replaces the extension as above.
A run that still completes no MessageHeader is refused with an error naming
the element it lacks, so no header-less message Bundle is ever produced. A
message valuing MSH-24 is one: the guide’s row into source has two
qualified HD data type maps and no single one to run.
Version mismatch is refused at load time
The mapping version, the grammar version, the archetype revision, the template
sem_ver, and the profile version all have to agree. A mismatch is refused when
the mapping loads, not when a request arrives, so a bad configuration cannot sit
waiting for the first patient record to expose it.
Programmed mappings are compiled in
mappingCode in FHIRconnect and CustomMapping in OMOCL name a function.
FerroBRIDGE registers those as named Rust functions at build time. There is no
runtime plugin loading, so what a deployment can execute is fixed by the binary
you audited.