Skip to content

Commit 705ef5e

Browse files
Add canonical mapping example (HIS to FHIR) (#27)
Two architectural approaches for mapping proprietary Hospital Information System data to FHIR R4: a synchronous Redis-cached facade and an event-driven RabbitMQ consumer storing to Aidbox. Includes sample HIS API server, shared test data, generated FHIR types via @atomic-ehr/codegen, and a reusable Claude skill for type generation. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe30433 commit 705ef5e

66 files changed

Lines changed: 6765 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
@@ -39,6 +39,7 @@ A collection of examples on top of Aidbox FHIR platform
3939

4040
## Aidbox Features
4141

42+
- [Canonical Mapping: HIS to FHIR](aidbox-features/aidbox-canonical-mapping/)
4243
- [Aidbox Notify via Custom Resources](aidbox-features/aidbox-notify-via-custom-resources/)
4344
- [Topic-Based Subscription to Kafka](aidbox-features/aidbox-subscriptions-to-kafka/)
4445
- [Aidbox with read-only replica](aidbox-features/aidbox-with-ro-replica/)
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
name: generate-types
3+
description: Generate or update FHIR TypeScript types using @atomic-ehr/codegen with tree-shaking
4+
---
5+
6+
# FHIR Type Generation with @atomic-ehr/codegen
7+
8+
Generate type-safe FHIR R4 TypeScript interfaces from StructureDefinitions. Uses tree-shaking to only include the resource types you need.
9+
10+
## Setup (if not already configured)
11+
12+
### 1. Install the codegen package
13+
14+
```sh
15+
# npm
16+
npm install -D @atomic-ehr/codegen
17+
18+
# pnpm
19+
pnpm add -D @atomic-ehr/codegen
20+
21+
# yarn
22+
yarn add -D @atomic-ehr/codegen
23+
24+
# bun
25+
bun add -d @atomic-ehr/codegen
26+
```
27+
28+
For npm/pnpm/yarn projects, also install `tsx` to run the TypeScript generation script:
29+
30+
```sh
31+
npm install -D tsx
32+
```
33+
34+
### 2. Create the generation script
35+
36+
Create `scripts/generate-types.ts`:
37+
38+
```ts
39+
import { APIBuilder, prettyReport } from "@atomic-ehr/codegen";
40+
41+
const builder = new APIBuilder()
42+
.throwException()
43+
.fromPackage("hl7.fhir.r4.core", "4.0.1")
44+
.typescript({
45+
withDebugComment: false,
46+
generateProfile: false,
47+
openResourceTypeSet: false,
48+
})
49+
.typeSchema({
50+
treeShake: {
51+
"hl7.fhir.r4.core": {
52+
// Add the resource types you need here.
53+
// All dependency types (Identifier, Reference, CodeableConcept, etc.)
54+
// are included automatically.
55+
"http://hl7.org/fhir/StructureDefinition/Patient": {},
56+
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
57+
"http://hl7.org/fhir/StructureDefinition/OperationOutcome": {},
58+
},
59+
},
60+
})
61+
.outputTo("./src/fhir-types")
62+
.cleanOutput(true);
63+
64+
const report = await builder.generate();
65+
console.log(prettyReport(report));
66+
if (!report.success) process.exit(1);
67+
```
68+
69+
### 3. Add the script to package.json
70+
71+
```json
72+
{
73+
"scripts": {
74+
"generate-types": "bun run scripts/generate-types.ts"
75+
}
76+
}
77+
```
78+
79+
For npm/pnpm/yarn projects (using tsx instead of bun):
80+
81+
```json
82+
{
83+
"scripts": {
84+
"generate-types": "tsx scripts/generate-types.ts"
85+
}
86+
}
87+
```
88+
89+
### 4. Add to .gitignore
90+
91+
```
92+
.codegen-cache/
93+
```
94+
95+
### 5. Include scripts in tsconfig.json
96+
97+
```json
98+
{
99+
"include": ["src/**/*", "scripts/**/*"]
100+
}
101+
```
102+
103+
### 6. Run generation
104+
105+
```sh
106+
# bun
107+
bun run generate-types
108+
109+
# npm
110+
npm run generate-types
111+
112+
# pnpm
113+
pnpm run generate-types
114+
```
115+
116+
This outputs typed interfaces to `src/fhir-types/hl7-fhir-r4-core/`. Commit these files — they are the project's FHIR type definitions.
117+
118+
## APIBuilder reference
119+
120+
### Loading FHIR packages
121+
122+
```ts
123+
// FHIR R4 base (required)
124+
.fromPackage("hl7.fhir.r4.core", "4.0.1")
125+
126+
// Implementation Guides (optional) — load from tgz URL
127+
.fromPackageRef("https://fs.get-ig.org/-/hl7.fhir.us.core-7.0.0.tgz")
128+
.fromPackageRef("https://fs.get-ig.org/-/fhir.r4.ukcore.stu2-2.0.2.tgz")
129+
```
130+
131+
### TypeScript options
132+
133+
```ts
134+
.typescript({
135+
withDebugComment: false, // omit debug comments in generated files
136+
generateProfile: false, // set true when loading IGs with constrained profiles
137+
openResourceTypeSet: false, // stricter resource type unions
138+
})
139+
```
140+
141+
### Tree-shaking
142+
143+
Tree-shaking controls which resource types are generated. Without it, ALL FHIR resources are included (~150+ files). With it, only the listed types and their transitive dependencies are generated.
144+
145+
```ts
146+
.typeSchema({
147+
treeShake: {
148+
"<package-name>": {
149+
"<StructureDefinition canonical URL>": {},
150+
// ...
151+
},
152+
},
153+
})
154+
```
155+
156+
**StructureDefinition URL patterns:**
157+
158+
| Source | Pattern | Example |
159+
|--------|---------|---------|
160+
| FHIR R4 base | `http://hl7.org/fhir/StructureDefinition/{ResourceType}` | `http://hl7.org/fhir/StructureDefinition/Patient` |
161+
| US Core | `http://hl7.org/fhir/us/core/StructureDefinition/{profile}` | `http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient` |
162+
| UK Core | `https://fhir.hl7.org.uk/StructureDefinition/{profile}` | `https://fhir.hl7.org.uk/StructureDefinition/UKCore-Patient` |
163+
164+
**Common FHIR R4 resource types:**
165+
166+
```ts
167+
"http://hl7.org/fhir/StructureDefinition/Patient": {},
168+
"http://hl7.org/fhir/StructureDefinition/Encounter": {},
169+
"http://hl7.org/fhir/StructureDefinition/Observation": {},
170+
"http://hl7.org/fhir/StructureDefinition/Condition": {},
171+
"http://hl7.org/fhir/StructureDefinition/Procedure": {},
172+
"http://hl7.org/fhir/StructureDefinition/MedicationRequest": {},
173+
"http://hl7.org/fhir/StructureDefinition/DiagnosticReport": {},
174+
"http://hl7.org/fhir/StructureDefinition/AllergyIntolerance": {},
175+
"http://hl7.org/fhir/StructureDefinition/Immunization": {},
176+
"http://hl7.org/fhir/StructureDefinition/Location": {},
177+
"http://hl7.org/fhir/StructureDefinition/Organization": {},
178+
"http://hl7.org/fhir/StructureDefinition/Practitioner": {},
179+
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
180+
"http://hl7.org/fhir/StructureDefinition/OperationOutcome": {},
181+
"http://hl7.org/fhir/StructureDefinition/Questionnaire": {},
182+
"http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse": {},
183+
```
184+
185+
### Output options
186+
187+
```ts
188+
.outputTo("./src/fhir-types") // output directory
189+
.cleanOutput(true) // delete output dir before regenerating
190+
```
191+
192+
## Example: Adding an Implementation Guide
193+
194+
To generate US Core profiled types alongside base R4:
195+
196+
```ts
197+
import { APIBuilder, prettyReport } from "@atomic-ehr/codegen";
198+
199+
const builder = new APIBuilder()
200+
.throwException()
201+
.fromPackage("hl7.fhir.r4.core", "4.0.1")
202+
.fromPackageRef("https://fs.get-ig.org/-/hl7.fhir.us.core-7.0.0.tgz")
203+
.typescript({
204+
withDebugComment: false,
205+
generateProfile: true, // enable for IG profiles
206+
openResourceTypeSet: false,
207+
})
208+
.typeSchema({
209+
treeShake: {
210+
"hl7.fhir.r4.core": {
211+
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
212+
"http://hl7.org/fhir/StructureDefinition/OperationOutcome": {},
213+
},
214+
"hl7.fhir.us.core": {
215+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {},
216+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-encounter-diagnosis": {},
217+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab": {},
218+
},
219+
},
220+
})
221+
.outputTo("./src/fhir-types")
222+
.cleanOutput(true);
223+
224+
const report = await builder.generate();
225+
console.log(prettyReport(report));
226+
if (!report.success) process.exit(1);
227+
```
228+
229+
## What gets generated
230+
231+
For each resource type, a `.ts` file is generated containing:
232+
233+
- **Main interface** — e.g., `Patient`, `Encounter`, `Bundle`
234+
- **BackboneElement interfaces** — nested structures like `EncounterLocation`, `PatientContact`, `BundleEntry`
235+
- **Type guard function** — e.g., `isPatient(resource)`, `isEncounter(resource)`
236+
- **Re-exports** of dependency types (Identifier, Reference, CodeableConcept, etc.)
237+
- **Barrel export**`index.ts` re-exports everything for convenience
238+
239+
Generated types are fully typed with FHIR value set enums where applicable:
240+
241+
```ts
242+
// Encounter.status is a union of valid FHIR values
243+
status: ("planned" | "arrived" | "triaged" | "in-progress" | "onleave" | "finished" | "cancelled" | "entered-in-error" | "unknown");
244+
245+
// References are typed with target resource types
246+
subject?: Reference<"Group" | "Patient">;
247+
location: Reference<"Location">;
248+
```
249+
250+
## Import patterns
251+
252+
```ts
253+
// Direct file import (preferred — explicit about what you use)
254+
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
255+
import type { Encounter, EncounterLocation } from "./fhir-types/hl7-fhir-r4-core/Encounter";
256+
import type { Bundle, BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle";
257+
import type { Identifier } from "./fhir-types/hl7-fhir-r4-core/Identifier";
258+
import type { Reference } from "./fhir-types/hl7-fhir-r4-core/Reference";
259+
260+
// Barrel import (convenient for grabbing many types)
261+
import type { Patient, Encounter, Location, Bundle } from "./fhir-types/hl7-fhir-r4-core";
262+
263+
// Type guards (value imports, not type-only)
264+
import { isPatient, isEncounter } from "./fhir-types/hl7-fhir-r4-core";
265+
```
266+
267+
## Workflow for adding a new resource type
268+
269+
1. Edit `scripts/generate-types.ts` — add the StructureDefinition URL to `treeShake`
270+
2. Run `npm run generate-types` (or `bun run generate-types`, `pnpm run generate-types`)
271+
3. Import the new type in your code
272+
4. Run typecheck to verify
273+
5. Commit the updated `src/fhir-types/` directory
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# FHIR Facade Configuration
2+
3+
# Server
4+
PORT=3000
5+
CACHE_TTL_SECONDS=60
6+
REDIS_URL=redis://redis:6379
7+
8+
# HIS (Hospital Information System) API
9+
HIS_BASE_URL=http://his:4000
10+
HIS_CLIENT_ID=his-client
11+
HIS_CLIENT_SECRET=his-secret
12+
HIS_ENVIRONMENT=TEST
13+
14+
# Requesting Product header value
15+
REQUESTING_PRODUCT=FHIR-Facade/1.0.0
16+
17+
# Event-Driven Architecture
18+
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
19+
FHIR_SERVER_URL=http://aidbox:8080
20+
AIDBOX_CLIENT_ID=root
21+
AIDBOX_CLIENT_SECRET=WdodyB65ij
22+
PREFETCH=10
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Environment
5+
.env
6+
.env.local
7+
.env.*.local
8+
9+
# Build
10+
dist/
11+
*.tsbuildinfo
12+
13+
# IDE
14+
.idea/
15+
.vscode/
16+
*.swp
17+
*.swo
18+
*~
19+
20+
# OS
21+
.DS_Store
22+
Thumbs.db
23+
24+
# Logs
25+
*.log
26+
npm-debug.log*
27+
28+
# Bun
29+
bun.lock
30+
31+
# Codegen
32+
.codegen-cache/
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# CLAUDE.md
2+
3+
## Project Overview
4+
5+
This project demonstrates canonical mapping — translating proprietary Hospital Information System (HIS) data into FHIR R4 resources. It shows two architectural approaches:
6+
7+
1. **Pure Facade** (synchronous) — Redis-cached proxy that fetches from HIS API on demand
8+
2. **Event-Driven** — RabbitMQ consumer that maps ADT events to FHIR and stores in Aidbox
9+
10+
## FHIR Type Generation
11+
12+
FHIR TypeScript types are generated using `@atomic-ehr/codegen`. The config is at `scripts/generate-types.ts` with tree-shaking for Patient, Encounter, Location, Bundle, and OperationOutcome.
13+
14+
Generated types are output to `src/fhir-types/hl7-fhir-r4-core/` — do not edit these files manually.
15+
16+
```ts
17+
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
18+
import type { Encounter, EncounterLocation } from "./fhir-types/hl7-fhir-r4-core/Encounter";
19+
import type { Bundle, BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle";
20+
```
21+
22+
To add a new resource type, add its StructureDefinition URL to the `treeShake` config and run `bun run generate-types`.
23+
24+
## Commands
25+
26+
| Command | Description |
27+
|---------|-------------|
28+
| `bun run generate-types` | Regenerate FHIR TypeScript types |
29+
| `bun run typecheck` | TypeScript type check |
30+
| `bun run dev` | Start facade with hot reload |
31+
| `bun run start:consumer` | Start event consumer |
32+
| `bun run publish:admit` | Publish 7 test admit events |
33+
| `docker compose --profile facade up -d --build` | Run facade approach |
34+
| `docker compose --profile event-driven up -d --build` | Run event-driven approach |
35+
36+
## Project Structure
37+
38+
- `src/facade/index.ts` — FHIR facade HTTP server (Bun.serve)
39+
- `src/facade/cache.ts` — Redis TTL cache
40+
- `src/facade/his-server.ts` — Sample HIS API server
41+
- `src/event-driven/consumer.ts` — RabbitMQ ADT event consumer
42+
- `src/event-driven/publisher.ts` — ADT event simulator
43+
- `src/event-driven/fhir-client.ts` — Aidbox FHIR client
44+
- `src/shared/his-client.ts` — HIS API client (OAuth 2.0)
45+
- `src/shared/fhir-mapper.ts` — HIS → FHIR R4 mapping functions
46+
- `src/shared/test-data.ts` — Shared test data (7 sample patients)
47+
- `src/fhir-types/` — Auto-generated FHIR types (do not edit)
48+
- `scripts/generate-types.ts` — Codegen configuration

0 commit comments

Comments
 (0)