medOS ultra

FHIR Transformer Module

Durable reference for the FHIR write/read transformer layer: layer responsibilities, resource coverage, code-system URIs, add-resource checklists.

5 min read diagramsUpdated 2026-05-25docs/architecture/fhir-transformer-module.md

Durable reference for the FHIR write/read transformer layer in services/public-api. Update this doc as resources are added. Companion to the (one-shot) handoff at docs/architecture/fhir-ipd-handoff-2026-05-09.md.

Module location

services/public-api/src/api/publicapi/modules/fhir/

Layer responsibilities

Layer Files What it does What it must NOT do
Capability fhir-capability.controller.ts Hand-maintained CapabilityStatement at GET /fhir/metadata. Lists every advertised resource + interaction. Auto-derive from controllers (it doesn’t, intentionally — keeps profile claims explicit).
Resource controllers resources/*.controller.ts One per FHIR resource type. HTTP route handlers. Fetch from Moleculer, call transformers. Contain mapping logic (delegate to transformers).
Forward transformers transformers/<resource>.transformer.ts medOS document → FHIR R4 resource. Pure functions. Reach into Moleculer / DB. The controller fetches; the transformer maps.
Reverse transformers transformers/fhir-to-medos.transformer.ts (single file) FHIR R4 resource → medOS write payload. Pure functions. Same as above. Single file by convention.
Bundle composers e.g. transformers/surgicalWorkflow.transformer.ts Compose multi-resource document Bundles. Imports forward transformers. Fetch records — caller passes them in.

Resource coverage matrix

Resource GET / search POST / PUT (write) Forward mapper Reverse mapper Tests
Patient patient.transformer.ts fromFhirPatient partial
Encounter encounter.transformer.ts fromFhirEncounter ✅ (14 tests)
Condition condition.transformer.ts fromFhirCondition
Observation observation.transformer.ts fromFhirObservation
ServiceRequest serviceRequest.transformer.ts fromFhirServiceRequest
MedicationRequest medicationRequest.transformer.ts fromFhirMedicationRequest ✅ (9 tests)
AllergyIntolerance allergyIntolerance.transformer.ts fromFhirAllergyIntolerance
DocumentReference ✅ (via surgical bundle) inline in surgicalWorkflow.transformer.ts ✅ via surgical
Procedure partial (via surgical) (via surgical) surgicalWorkflow.transformer.ts ✅ (14 tests)
QuestionnaireResponse (via surgical) surgicalWorkflow.transformer.ts ✅ via surgical
Task (Acknowledgement) partial ack-to-fhir-task.transformer.ts fhirTaskToAck ✅ (pre-existing)
SurgicalWorkflow (custom Bundle) GET /fhir/SurgicalWorkflow/:orId surgicalWorkflow.transformer.ts n/a
EpisodeOfCare
Coverage
Account / Invoice

Update this table when adding a new resource.

Conventions

Code system URIs

medOS concept URI
Standard FHIR diagnosis roles (AD/DD/CC/CM/pre-op/post-op/billing) http://terminology.hl7.org/CodeSystem/diagnosis-role
medOS-specific diagnosis roles (e.g. working) http://medos.health/CodeSystem/diagnosis-role
Standard FHIR location physical types http://terminology.hl7.org/CodeSystem/location-physical-type
HL7 v3 ActCode (Encounter.class) http://terminology.hl7.org/CodeSystem/v3-ActCode

When a medOS concept has no canonical FHIR equivalent, mint a URI under http://medos.health/CodeSystem/<concept> rather than abusing the canonical URL. See encounter.transformer.ts:STANDARD_ROLE_DISPLAY for the pattern.

Reference shapes

Always emit references as { reference: '/<id>' }. Add display only when the medOS document carries a denormalised name. Don’t ever emit a bare string id.

// ✅ Right
{ reference: 'Patient/pat-7' }
{ reference: 'Location/loc-1', display: 'Ward 4B' }

// ❌ Wrong
{ reference: 'pat-7' }
'Patient/pat-7'

Date / period emission

  • For dates already stored as ISO strings: pass through as-is.
  • For dates stored as Date objects (Mongoose default): always wrap with new Date(x).toISOString() to avoid timezone drift between Node hosts.
  • Periods: always emit { start, end } even when one side is undefined — better for client diffing than omitting the wrapper.

_id: false on subdocs

When adding a sub-array to a Mongoose-style schema in packages/platform-api-schema, always set _id: false on the inner schema spec unless you genuinely need a stable id per entry. Example from Encounter.ts:

diagnoses: {
  type: [{
    conditionRef: { type: String, ref: 'Condition' },
    use: String,
    rank: Number,
    _id: false,   // ← required
  }],
  default: undefined,
}

Without it Mongoose auto-mints _id on every entry, breaking idempotent re-saves and bloating the document.

Test patterns

  • One spec file per transformer file. Named <name>.transformer.spec.ts. Lives in __tests__/ (sibling to transformers/).
  • Imports: from '../transformers/<name>'. Tests use only standard jest globals — no testing-library, no nest test harness.
  • Each describe block builds a baseFixture and clones it per test. Don’t share mutable fixtures.
  • Reverse mappers (fromFhir*) get their own consolidated spec at fhir-to-medos.transformer.spec.ts.
  • Bundle composers test the shape (resource types present, partOf links wired) rather than every field of every entry — those are covered by the per-resource specs.

Adding a new resource — checklist

  1. Forward mapper: create transformers/<resource>.transformer.ts. Export toFhir(doc).
  2. Reverse mapper (if write is supported): add fromFhir(fhir) to transformers/fhir-to-medos.transformer.ts.
  3. Controller: create resources/fhir-<resource>.controller.ts. Wire to a Moleculer service.
  4. CapabilityStatement: add an entry to rest[0].resource[] in fhir-capability.controller.ts (interactions, profile, searchParams).
  5. Tests: create __tests__/<resource>.transformer.spec.ts and add cases to fhir-to-medos.transformer.spec.ts for the reverse direction.
  6. Update this doc: tick the box in the resource coverage matrix above.

Adding a new field on an existing resource — checklist

  1. Schema (if needed): add the field to the entity in packages/platform-api-schema/src/<area>/<entity>/.ts. If it’s a sub-array, set _id: false.
  2. Forward mapper: emit the field in the relevant to function. Skip it (omit) when absent — don’t emit null / empty arrays.
  3. Reverse mapper: parse the field in from. Be defensive about missing intermediate paths.
  4. Tests: add at least three cases — present (asserts emission), absent (asserts omission), edge (e.g. partial / unknown enum).

Sibling docs

  • docs/architecture/fhir-ipd-handoff-2026-05-09.md — handoff for the IPD-hardening work.
  • docs/architecture/bia-rt-2-fhir-careplan-profile.md — profile reference for the BIA CarePlan resource.
  • docs/architecture/imaging-acknowledgement-shapes.md — acknowledgement / Task shape catalogue (interacts with ack-to-fhir-task.transformer.ts).
  • CLAUDE.md — backend deployment caveats for services/public-api.
Ask Anything