Skip to content
Open
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 @@ -39,6 +39,7 @@ A collection of examples on top of Aidbox FHIR platform

## Aidbox Features

- [Canonical Mapping: HIS to FHIR](aidbox-features/aidbox-canonical-mapping/)
- [Aidbox Notify via Custom Resources](aidbox-features/aidbox-notify-via-custom-resources/)
- [Topic-Based Subscription to Kafka](aidbox-features/aidbox-subscriptions-to-kafka/)
- [Aidbox with read-only replica](aidbox-features/aidbox-with-ro-replica/)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
---
name: generate-types
description: Generate or update FHIR TypeScript types using @atomic-ehr/codegen with tree-shaking
---

# FHIR Type Generation with @atomic-ehr/codegen

Generate type-safe FHIR R4 TypeScript interfaces from StructureDefinitions. Uses tree-shaking to only include the resource types you need.

## Setup (if not already configured)

### 1. Install the codegen package

```sh
# npm
npm install -D @atomic-ehr/codegen

# pnpm
pnpm add -D @atomic-ehr/codegen

# yarn
yarn add -D @atomic-ehr/codegen

# bun
bun add -d @atomic-ehr/codegen
```

For npm/pnpm/yarn projects, also install `tsx` to run the TypeScript generation script:

```sh
npm install -D tsx
```

### 2. Create the generation script

Create `scripts/generate-types.ts`:

```ts
import { APIBuilder, prettyReport } from "@atomic-ehr/codegen";

const builder = new APIBuilder()
.throwException()
.fromPackage("hl7.fhir.r4.core", "4.0.1")
.typescript({
withDebugComment: false,
generateProfile: false,
openResourceTypeSet: false,
})
.typeSchema({
treeShake: {
"hl7.fhir.r4.core": {
// Add the resource types you need here.
// All dependency types (Identifier, Reference, CodeableConcept, etc.)
// are included automatically.
"http://hl7.org/fhir/StructureDefinition/Patient": {},
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
"http://hl7.org/fhir/StructureDefinition/OperationOutcome": {},
},
},
})
.outputTo("./src/fhir-types")
.cleanOutput(true);

const report = await builder.generate();
console.log(prettyReport(report));
if (!report.success) process.exit(1);
```

### 3. Add the script to package.json

```json
{
"scripts": {
"generate-types": "bun run scripts/generate-types.ts"
}
}
```

For npm/pnpm/yarn projects (using tsx instead of bun):

```json
{
"scripts": {
"generate-types": "tsx scripts/generate-types.ts"
}
}
```

### 4. Add to .gitignore

```
.codegen-cache/
```

### 5. Include scripts in tsconfig.json

```json
{
"include": ["src/**/*", "scripts/**/*"]
}
```

### 6. Run generation

```sh
# bun
bun run generate-types

# npm
npm run generate-types

# pnpm
pnpm run generate-types
```

This outputs typed interfaces to `src/fhir-types/hl7-fhir-r4-core/`. Commit these files — they are the project's FHIR type definitions.

## APIBuilder reference

### Loading FHIR packages

```ts
// FHIR R4 base (required)
.fromPackage("hl7.fhir.r4.core", "4.0.1")

// Implementation Guides (optional) — load from tgz URL
.fromPackageRef("https://fs.get-ig.org/-/hl7.fhir.us.core-7.0.0.tgz")
.fromPackageRef("https://fs.get-ig.org/-/fhir.r4.ukcore.stu2-2.0.2.tgz")
```

### TypeScript options

```ts
.typescript({
withDebugComment: false, // omit debug comments in generated files
generateProfile: false, // set true when loading IGs with constrained profiles
openResourceTypeSet: false, // stricter resource type unions
})
```

### Tree-shaking

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.

```ts
.typeSchema({
treeShake: {
"<package-name>": {
"<StructureDefinition canonical URL>": {},
// ...
},
},
})
```

**StructureDefinition URL patterns:**

| Source | Pattern | Example |
|--------|---------|---------|
| FHIR R4 base | `http://hl7.org/fhir/StructureDefinition/{ResourceType}` | `http://hl7.org/fhir/StructureDefinition/Patient` |
| US Core | `http://hl7.org/fhir/us/core/StructureDefinition/{profile}` | `http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient` |
| UK Core | `https://fhir.hl7.org.uk/StructureDefinition/{profile}` | `https://fhir.hl7.org.uk/StructureDefinition/UKCore-Patient` |

**Common FHIR R4 resource types:**

```ts
"http://hl7.org/fhir/StructureDefinition/Patient": {},
"http://hl7.org/fhir/StructureDefinition/Encounter": {},
"http://hl7.org/fhir/StructureDefinition/Observation": {},
"http://hl7.org/fhir/StructureDefinition/Condition": {},
"http://hl7.org/fhir/StructureDefinition/Procedure": {},
"http://hl7.org/fhir/StructureDefinition/MedicationRequest": {},
"http://hl7.org/fhir/StructureDefinition/DiagnosticReport": {},
"http://hl7.org/fhir/StructureDefinition/AllergyIntolerance": {},
"http://hl7.org/fhir/StructureDefinition/Immunization": {},
"http://hl7.org/fhir/StructureDefinition/Location": {},
"http://hl7.org/fhir/StructureDefinition/Organization": {},
"http://hl7.org/fhir/StructureDefinition/Practitioner": {},
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
"http://hl7.org/fhir/StructureDefinition/OperationOutcome": {},
"http://hl7.org/fhir/StructureDefinition/Questionnaire": {},
"http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse": {},
```

### Output options

```ts
.outputTo("./src/fhir-types") // output directory
.cleanOutput(true) // delete output dir before regenerating
```

## Example: Adding an Implementation Guide

To generate US Core profiled types alongside base R4:

```ts
import { APIBuilder, prettyReport } from "@atomic-ehr/codegen";

const builder = new APIBuilder()
.throwException()
.fromPackage("hl7.fhir.r4.core", "4.0.1")
.fromPackageRef("https://fs.get-ig.org/-/hl7.fhir.us.core-7.0.0.tgz")
.typescript({
withDebugComment: false,
generateProfile: true, // enable for IG profiles
openResourceTypeSet: false,
})
.typeSchema({
treeShake: {
"hl7.fhir.r4.core": {
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
"http://hl7.org/fhir/StructureDefinition/OperationOutcome": {},
},
"hl7.fhir.us.core": {
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {},
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-encounter-diagnosis": {},
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab": {},
},
},
})
.outputTo("./src/fhir-types")
.cleanOutput(true);

const report = await builder.generate();
console.log(prettyReport(report));
if (!report.success) process.exit(1);
```

## What gets generated

For each resource type, a `.ts` file is generated containing:

- **Main interface** — e.g., `Patient`, `Encounter`, `Bundle`
- **BackboneElement interfaces** — nested structures like `EncounterLocation`, `PatientContact`, `BundleEntry`
- **Type guard function** — e.g., `isPatient(resource)`, `isEncounter(resource)`
- **Re-exports** of dependency types (Identifier, Reference, CodeableConcept, etc.)
- **Barrel export** — `index.ts` re-exports everything for convenience

Generated types are fully typed with FHIR value set enums where applicable:

```ts
// Encounter.status is a union of valid FHIR values
status: ("planned" | "arrived" | "triaged" | "in-progress" | "onleave" | "finished" | "cancelled" | "entered-in-error" | "unknown");

// References are typed with target resource types
subject?: Reference<"Group" | "Patient">;
location: Reference<"Location">;
```

## Import patterns

```ts
// Direct file import (preferred — explicit about what you use)
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import type { Encounter, EncounterLocation } from "./fhir-types/hl7-fhir-r4-core/Encounter";
import type { Bundle, BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle";
import type { Identifier } from "./fhir-types/hl7-fhir-r4-core/Identifier";
import type { Reference } from "./fhir-types/hl7-fhir-r4-core/Reference";

// Barrel import (convenient for grabbing many types)
import type { Patient, Encounter, Location, Bundle } from "./fhir-types/hl7-fhir-r4-core";

// Type guards (value imports, not type-only)
import { isPatient, isEncounter } from "./fhir-types/hl7-fhir-r4-core";
```

## Workflow for adding a new resource type

1. Edit `scripts/generate-types.ts` — add the StructureDefinition URL to `treeShake`
2. Run `npm run generate-types` (or `bun run generate-types`, `pnpm run generate-types`)
3. Import the new type in your code
4. Run typecheck to verify
5. Commit the updated `src/fhir-types/` directory
22 changes: 22 additions & 0 deletions aidbox-features/aidbox-canonical-mapping/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# FHIR Facade Configuration

# Server
PORT=3000
CACHE_TTL_SECONDS=60
REDIS_URL=redis://redis:6379

# HIS (Hospital Information System) API
HIS_BASE_URL=http://his:4000
HIS_CLIENT_ID=his-client
HIS_CLIENT_SECRET=his-secret
HIS_ENVIRONMENT=TEST

# Requesting Product header value
REQUESTING_PRODUCT=FHIR-Facade/1.0.0

# Event-Driven Architecture
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
FHIR_SERVER_URL=http://aidbox:8080
AIDBOX_CLIENT_ID=root
AIDBOX_CLIENT_SECRET=WdodyB65ij
PREFETCH=10
32 changes: 32 additions & 0 deletions aidbox-features/aidbox-canonical-mapping/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Dependencies
node_modules/

# Environment
.env
.env.local
.env.*.local

# Build
dist/
*.tsbuildinfo

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

# Bun
bun.lock

# Codegen
.codegen-cache/
48 changes: 48 additions & 0 deletions aidbox-features/aidbox-canonical-mapping/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# CLAUDE.md

## Project Overview

This project demonstrates canonical mapping — translating proprietary Hospital Information System (HIS) data into FHIR R4 resources. It shows two architectural approaches:

1. **Pure Facade** (synchronous) — Redis-cached proxy that fetches from HIS API on demand
2. **Event-Driven** — RabbitMQ consumer that maps ADT events to FHIR and stores in Aidbox

## FHIR Type Generation

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.

Generated types are output to `src/fhir-types/hl7-fhir-r4-core/` — do not edit these files manually.

```ts
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import type { Encounter, EncounterLocation } from "./fhir-types/hl7-fhir-r4-core/Encounter";
import type { Bundle, BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle";
```

To add a new resource type, add its StructureDefinition URL to the `treeShake` config and run `bun run generate-types`.

## Commands

| Command | Description |
|---------|-------------|
| `bun run generate-types` | Regenerate FHIR TypeScript types |
| `bun run typecheck` | TypeScript type check |
| `bun run dev` | Start facade with hot reload |
| `bun run start:consumer` | Start event consumer |
| `bun run publish:admit` | Publish 7 test admit events |
| `docker compose --profile facade up -d --build` | Run facade approach |
| `docker compose --profile event-driven up -d --build` | Run event-driven approach |

## Project Structure

- `src/facade/index.ts` — FHIR facade HTTP server (Bun.serve)
- `src/facade/cache.ts` — Redis TTL cache
- `src/facade/his-server.ts` — Sample HIS API server
- `src/event-driven/consumer.ts` — RabbitMQ ADT event consumer
- `src/event-driven/publisher.ts` — ADT event simulator
- `src/event-driven/fhir-client.ts` — Aidbox FHIR client
- `src/shared/his-client.ts` — HIS API client (OAuth 2.0)
- `src/shared/fhir-mapper.ts` — HIS → FHIR R4 mapping functions
- `src/shared/test-data.ts` — Shared test data (7 sample patients)
- `src/fhir-types/` — Auto-generated FHIR types (do not edit)
- `scripts/generate-types.ts` — Codegen configuration
Loading