|
| 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. |
0 commit comments