Skip to content

Commit d7f8e91

Browse files
authored
Merge pull request #30 from Aidbox/example/atomic-ehr-codegen-us-core-profiles
Add US Core profiles TypeScript example (CSV to FHIR Bundle)
2 parents 759ea43 + 3a6b3f4 commit d7f8e91

65 files changed

Lines changed: 13178 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ A collection of examples on top of Aidbox FHIR platform
6060
- [Agentic FHIR Implementation Guide Development](developer-experience/agentic-coding-ig-development/)
6161
- [Aidbox Firely .NET Client](developer-experience/aidbox-firely-dotnet-client/)
6262
- [Aidbox HAPI FHIR Client](developer-experience/aidbox-hapi-client/)
63+
- [@atomic-ehr/codegen: US Core Profiles in TypeScript: CSV -> FHIR Bundle](developer-experience/atomic-ehr-codegen-typescript-us-core-profiles/)
6364

6465
## Documentation
6566

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
.codegen-cache/
3+
bundle.json
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# US Core Profiles in TypeScript with @atomic-ehr/codegen
2+
3+
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).
4+
5+
The example:
6+
7+
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`,
8+
2. loads `patients.csv` (5 rows: MRN, name, demographics, race, one BP reading each),
9+
3. converts each row into a validated `USCorePatientProfile` + `USCoreBloodPressureProfile`,
10+
4. packages them as a `Bundle<Patient | Observation>` transaction with `urn:uuid` cross-references,
11+
5. reads `bundle.json` back, filters with `USCoreBloodPressureProfile.is`, and prints the average BP.
12+
13+
## Files
14+
15+
| File | Purpose |
16+
|------|---------|
17+
| `generate.ts` | Runs `@atomic-ehr/codegen` to produce typed profile classes in `fhir-types/` |
18+
| `fhir-types/` | Generated output (committed so you can browse without running the generator) |
19+
| `patients.csv` | Sample input (5 rows) |
20+
| `load.ts` | Parses CSV, builds the typed Bundle, writes `bundle.json` |
21+
| `avg.ts` | Reads `bundle.json` back, filters with `is()`, computes average BP |
22+
23+
## Run It
24+
25+
```bash
26+
npm install
27+
npx tsx generate.ts # regenerate fhir-types/ (optional -- already committed)
28+
npx tsx load.ts # reads patients.csv, writes bundle.json
29+
npx tsx avg.ts # reads bundle.json, prints the average BP
30+
```
31+
32+
Expected output:
33+
34+
```
35+
$ npx tsx load.ts
36+
Loaded 5 rows
37+
Wrote bundle with 10 entries
38+
39+
$ npx tsx avg.ts
40+
Avg BP: 125.2/82.0 mmHg (n=5)
41+
```
42+
43+
## POST to a FHIR Server (Optional)
44+
45+
Run [Aidbox](https://www.health-samurai.io/fhir-server) locally and POST `bundle.json`:
46+
47+
```bash
48+
curl -JO https://aidbox.app/runme && docker compose up -d
49+
SECRET=$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)
50+
51+
curl -u "root:$SECRET" -X POST -H "Content-Type: application/fhir+json" \
52+
-d @bundle.json http://localhost:8080/fhir
53+
```
54+
55+
Aidbox resolves the `urn:uuid` references during the transaction commit.
56+
57+
## Notes on the Code
58+
59+
- **`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)`).
60+
- **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.
61+
- **`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)`.
62+
- **`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.
63+
- **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.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { readFileSync } from "node:fs";
2+
3+
import type { Bundle } from "./fhir-types/hl7-fhir-r4-core/Bundle";
4+
import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation";
5+
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
6+
import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles";
7+
8+
const bundle: Bundle<Patient | Observation> = JSON.parse(readFileSync("./bundle.json", "utf8"));
9+
10+
const bps = (bundle.entry ?? [])
11+
.map(e => e.resource)
12+
.filter(USCoreBloodPressureProfile.is)
13+
.map(o => USCoreBloodPressureProfile.from(o));
14+
15+
const avg = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length;
16+
17+
const systolic = bps.map(bp => bp.getSystolic()!.value!);
18+
const diastolic = bps.map(bp => bp.getDiastolic()!.value!);
19+
20+
console.log(`Avg BP: ${avg(systolic).toFixed(1)}/${avg(diastolic).toFixed(1)} mmHg (n=${bps.length})`);

0 commit comments

Comments
 (0)