Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ A collection of examples on top of Aidbox FHIR platform
- [Agentic FHIR Implementation Guide Development](developer-experience/agentic-coding-ig-development/)
- [Aidbox Firely .NET Client](developer-experience/aidbox-firely-dotnet-client/)
- [Aidbox HAPI FHIR Client](developer-experience/aidbox-hapi-client/)
- [@atomic-ehr/codegen: US Core Profiles in TypeScript: CSV -> FHIR Bundle](developer-experience/atomic-ehr-codegen-typescript-us-core-profiles/)

## Documentation

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.codegen-cache/
bundle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# US Core Profiles in TypeScript with @atomic-ehr/codegen

A small CSV-to-FHIR converter demonstrating [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) profile class generation for US Core. Companion to the blog post [@atomic-ehr/codegen: US Core Profiles in TypeScript](https://www.health-samurai.io/articles/atomic-ehr-codegen-typescript-us-core-profiles).

The example:

1. generates profile classes for [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) and [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) plus base `Bundle` from `hl7.fhir.r4.core`,
2. loads `patients.csv` (5 rows: MRN, name, demographics, race, one BP reading each),
3. converts each row into a validated `USCorePatientProfile` + `USCoreBloodPressureProfile`,
4. packages them as a `Bundle<Patient | Observation>` transaction with `urn:uuid` cross-references,
5. reads `bundle.json` back, filters with `USCoreBloodPressureProfile.is`, and prints the average BP.

## Files

| File | Purpose |
|------|---------|
| `generate.ts` | Runs `@atomic-ehr/codegen` to produce typed profile classes in `fhir-types/` |
| `fhir-types/` | Generated output (committed so you can browse without running the generator) |
| `patients.csv` | Sample input (5 rows) |
| `load.ts` | Parses CSV, builds the typed Bundle, writes `bundle.json` |
| `avg.ts` | Reads `bundle.json` back, filters with `is()`, computes average BP |

## Run It

```bash
npm install
npx tsx generate.ts # regenerate fhir-types/ (optional -- already committed)
npx tsx load.ts # reads patients.csv, writes bundle.json
npx tsx avg.ts # reads bundle.json, prints the average BP
```

Expected output:

```
$ npx tsx load.ts
Loaded 5 rows
Wrote bundle with 10 entries

$ npx tsx avg.ts
Avg BP: 125.2/82.0 mmHg (n=5)
```

## POST to a FHIR Server (Optional)

Run [Aidbox](https://www.health-samurai.io/fhir-server) locally and POST `bundle.json`:

```bash
curl -JO https://aidbox.app/runme && docker compose up -d
SECRET=$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)

curl -u "root:$SECRET" -X POST -H "Content-Type: application/fhir+json" \
-d @bundle.json http://localhost:8080/fhir
```

Aidbox resolves the `urn:uuid` references during the transaction commit.

## Notes on the Code

- **`Row` is all strings.** The parser doesn't narrow types; each converter (`rowToPatient`, `rowToBP`) casts or converts where needed (`gender as Patient["gender"]`, `Number(row.systolic)`).
- **Must-support base fields** (`gender`, `birthDate`) aren't profiled further by US Core, so the profile class doesn't emit `.setGender()`-style setters. We populate them directly on the base `Patient` literal in `rowToPatient`, then pass it to `USCorePatientProfile.apply()`. `validate()` warns if a must-support field is missing.
- **`Bundle<Patient | Observation>` propagation** narrows `entry[].resource` to that union at the type level. In `avg.ts` the runtime narrowing on top comes from `USCoreBloodPressureProfile.is` -- a non-throwing type guard that checks `resourceType` + `meta.profile.includes(canonicalUrl)`.
- **`urn:uuid` references work directly.** The generated `Reference.reference` is typed as a union covering every FHIR literal reference form (`Patient/${id}`, absolute `http://...`, `urn:uuid:...`, `urn:oid:...`, `#fragment`). Transaction Bundle placeholder UUIDs drop right in; the server rewrites them to real `Patient/<id>` on commit.
- **Generator warnings are pre-suppressed.** `generate.ts` passes `mkCodegenLogger({ suppressTags: ["#fieldTypeNotFound", "#duplicateSchema", "#duplicateCanonical", "#largeValueSet"] })` so the ~10k routine warnings collapse to a single summary line. `prettyReport(report)` prints the file/line counts at the end.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { readFileSync } from "node:fs";

import type { Bundle } from "./fhir-types/hl7-fhir-r4-core/Bundle";
import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation";
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles";

const bundle: Bundle<Patient | Observation> = JSON.parse(readFileSync("./bundle.json", "utf8"));

const bps = (bundle.entry ?? [])
.map(e => e.resource)
.filter(USCoreBloodPressureProfile.is)
.map(o => USCoreBloodPressureProfile.from(o));

const avg = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length;

const systolic = bps.map(bp => bp.getSystolic()!.value!);
const diastolic = bps.map(bp => bp.getDiastolic()!.value!);

console.log(`Avg BP: ${avg(systolic).toFixed(1)}/${avg(diastolic).toFixed(1)} mmHg (n=${bps.length})`);
Loading