|
| 1 | +--- |
| 2 | +features: [CQL, Custom operations, Spring Boot, FHIR operations, Clinical decision support, Quality measures] |
| 3 | +languages: [Java, Kotlin] |
| 4 | +--- |
| 5 | +# Aidbox CQL Integration with Spring Boot |
| 6 | + |
| 7 | +This Spring Boot application integrates the [cqframework CQL engine](https://github.com/cqframework/clinical_quality_language) with Aidbox and implements the [Library/$evaluate](https://build.fhir.org/ig/HL7/cql-ig/OperationDefinition-cql-library-evaluate.html) operation. |
| 8 | + |
| 9 | +Includes sample data and CQL libraries for 4 CMS quality measures: CMS130, CMS125, CMS131, CMS165. |
| 10 | + |
| 11 | +## Stack |
| 12 | + |
| 13 | +| Component | Version | |
| 14 | +|---|---| |
| 15 | +| CQL engine | cqframework 4.5.0 (Kotlin Multiplatform) | |
| 16 | +| HAPI FHIR | 8.8.0 | |
| 17 | +| Spring Boot | 3.4.1 | |
| 18 | +| Aidbox | edge | |
| 19 | + |
| 20 | +## Prerequisites |
| 21 | + |
| 22 | +- [Docker](https://docs.docker.com/get-docker/) with Docker Compose |
| 23 | +- [curl](https://curl.se/) |
| 24 | +## Quick Start |
| 25 | + |
| 26 | +### 1. Start Aidbox and the CQL app |
| 27 | + |
| 28 | +``` |
| 29 | +docker compose up --build |
| 30 | +``` |
| 31 | + |
| 32 | +### 2. Activate Aidbox |
| 33 | + |
| 34 | +Open http://localhost:8888 in your browser and activate the Aidbox instance. |
| 35 | + |
| 36 | +### 3. Verify with the simple example |
| 37 | + |
| 38 | +Create a sample patient: |
| 39 | + |
| 40 | +```bash |
| 41 | +curl -u root:secret -X POST http://localhost:8888/fhir/Patient \ |
| 42 | + -H "Content-Type: application/json" \ |
| 43 | + -d '{"resourceType":"Patient","gender":"male","name":[{"family":"Smith"}]}' |
| 44 | +``` |
| 45 | + |
| 46 | +Evaluate the `example` CQL library: |
| 47 | + |
| 48 | +```bash |
| 49 | +curl -u root:secret -X POST http://localhost:8888/Library/example/\$evaluate |
| 50 | +``` |
| 51 | + |
| 52 | +Expected response: |
| 53 | +```json |
| 54 | +{ |
| 55 | + "resourceType": "Parameters", |
| 56 | + "parameters": [ |
| 57 | + { "name": "MalePatients", "valueHumanName": { "family": "Smith" } } |
| 58 | + ] |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +## Running CMS Quality Measures |
| 63 | + |
| 64 | +### 4. Load shared terminology (once) |
| 65 | + |
| 66 | +```bash |
| 67 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 68 | + -H "Content-Type: application/json" -d @data/codesystems-bundle.json |
| 69 | +``` |
| 70 | + |
| 71 | +Create stub resources (required by test data references): |
| 72 | + |
| 73 | +```bash |
| 74 | +curl -u root:secret -X PUT http://localhost:8888/fhir/Organization/example \ |
| 75 | + -H "Content-Type: application/json" \ |
| 76 | + -d '{"resourceType":"Organization","id":"example","name":"Example Organization"}' |
| 77 | + |
| 78 | +curl -u root:secret -X PUT http://localhost:8888/fhir/Practitioner/example \ |
| 79 | + -H "Content-Type: application/json" \ |
| 80 | + -d '{"resourceType":"Practitioner","id":"example","name":[{"family":"Example","given":["Practitioner"]}]}' |
| 81 | +``` |
| 82 | + |
| 83 | +### 5. Load a measure and evaluate |
| 84 | + |
| 85 | +**CMS130 — Colorectal Cancer Screening** (64 test patients): |
| 86 | + |
| 87 | +```bash |
| 88 | +# Load ValueSets and clinical data |
| 89 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 90 | + -H "Content-Type: application/json" -d @data/cms130-valuesets.json |
| 91 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 92 | + -H "Content-Type: application/json" -d @data/cms130-clinical-data.json |
| 93 | + |
| 94 | +# Evaluate for a patient |
| 95 | +curl -u root:secret -X POST \ |
| 96 | + 'http://localhost:8888/Library/CMS130FHIRColorectalCancerScrn/$evaluate?patientId=007ec5f1-08cf-474a-a472-f6a92cca4b79' |
| 97 | +``` |
| 98 | + |
| 99 | +Expected response: |
| 100 | +```json |
| 101 | +{ |
| 102 | + "resourceType": "Parameters", |
| 103 | + "parameters": [ |
| 104 | + { "name": "Initial Population", "valueBoolean": true }, |
| 105 | + { "name": "Denominator", "valueBoolean": true }, |
| 106 | + { "name": "Denominator Exclusions", "valueBoolean": true }, |
| 107 | + { "name": "Numerator", "valueBoolean": false } |
| 108 | + ] |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +**CMS125 — Breast Cancer Screening** (66 test patients): |
| 113 | + |
| 114 | +```bash |
| 115 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 116 | + -H "Content-Type: application/json" -d @data/cms125-valuesets.json |
| 117 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 118 | + -H "Content-Type: application/json" -d @data/cms125-clinical-data.json |
| 119 | + |
| 120 | +curl -u root:secret -X POST \ |
| 121 | + 'http://localhost:8888/Library/CMS125FHIRBreastCancerScreen/$evaluate?patientId=01c88972-84e2-4594-835b-924481b9990a' |
| 122 | +``` |
| 123 | + |
| 124 | +**CMS131 — Diabetes Eye Exam** (63 test patients): |
| 125 | + |
| 126 | +```bash |
| 127 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 128 | + -H "Content-Type: application/json" -d @data/cms131-valuesets.json |
| 129 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 130 | + -H "Content-Type: application/json" -d @data/cms131-clinical-data.json |
| 131 | + |
| 132 | +curl -u root:secret -X POST \ |
| 133 | + 'http://localhost:8888/Library/CMS131FHIRDiabetesEyeExam/$evaluate?patientId=89073685-3807-41f5-bc32-2cf44c1b8227' |
| 134 | +``` |
| 135 | + |
| 136 | +**CMS165 — Controlling High Blood Pressure** (68 test patients): |
| 137 | + |
| 138 | +```bash |
| 139 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 140 | + -H "Content-Type: application/json" -d @data/cms165-valuesets.json |
| 141 | +curl -u root:secret -X POST http://localhost:8888/fhir \ |
| 142 | + -H "Content-Type: application/json" -d @data/cms165-clinical-data.json |
| 143 | + |
| 144 | +curl -u root:secret -X POST \ |
| 145 | + 'http://localhost:8888/Library/CMS165FHIRControllingHighBP/$evaluate?patientId=45e01fed-56bb-483d-a860-af3d566bda11' |
| 146 | +``` |
| 147 | + |
| 148 | +## How It Works |
| 149 | + |
| 150 | +``` |
| 151 | +Client → POST /Library/{name}/$evaluate?patientId={id} |
| 152 | + → Aidbox (custom operation routing via App resource) |
| 153 | + → Spring Boot CQL app (port 8080) |
| 154 | + → cqframework engine |
| 155 | + → reads CQL from classpath |
| 156 | + → retrieves FHIR data from Aidbox |
| 157 | + → expands ValueSets via Aidbox $expand |
| 158 | + ← FHIR Parameters response |
| 159 | + ← proxied back |
| 160 | + ← returned to client |
| 161 | +``` |
| 162 | + |
| 163 | +## Adding Your Own CQL Library |
| 164 | + |
| 165 | +1. Place `.cql` file in `src/main/resources/` |
| 166 | +2. Rebuild: `docker compose build cql-app && docker compose up -d cql-app` |
| 167 | +3. Evaluate: `POST /Library/{library-name}/$evaluate` |
| 168 | + |
| 169 | +## Known Limitations |
| 170 | + |
| 171 | +- **ToConcept(Quantity) bug** — measures using `USCoreBMIProfile` with value comparison fail with `Could not resolve call to operator 'ToConcept'`. This is an upstream cqframework issue ([#564](https://github.com/cqframework/clinical_quality_language/issues/564)). |
| 172 | + |
| 173 | +## Aidbox-Specific Adaptations |
| 174 | + |
| 175 | +These adaptations are needed because Aidbox handles terminology differently from HAPI FHIR server (the reference test environment for cqframework): |
| 176 | + |
| 177 | +| What | Why | |
| 178 | +|---|---| |
| 179 | +| `FullExpandTerminologyWrapper` | Aidbox `$expand` may return incomplete results without explicit `count` parameter. Wrapper adds `count=10000`. | |
| 180 | +| `maxCodesPerQuery=64` | Large ValueSets cause HTTP 414 (URI Too Long) when codes are inlined in search URLs | |
| 181 | +| QICore data provider registration | CMS measures use QICore profiles. Engine looks up data by model URI — without registering `http://hl7.org/fhir/us/qicore`, it silently returns no data. | |
| 182 | +| kotlinx-io bridge | CQL engine 4.x (Kotlin) uses `kotlinx.io.Source` instead of `java.io.InputStream` for loading CQL files. Bridge code converts between the two. | |
| 183 | +| `buildResponse()` | CQL engine returns Java objects (`Map<String, ExpressionResult>`). This method serializes them into a FHIR Parameters JSON response. | |
| 184 | +| `BOX_FHIR_TERMINOLOGY_ENGINE: hybrid` | Aidbox requires explicit terminology engine configuration for `$expand` | |
| 185 | +| Stub `Organization/example` + `Practitioner/example` | Test data Coverage and MedicationRequest resources reference these | |
| 186 | +| ValueSet `description` + `valueDate` fix | Aidbox requires `description` (FHIR says optional) and rejects `valueDate` extensions | |
| 187 | + |
| 188 | +## Test Data Source |
| 189 | + |
| 190 | +Clinical data and ValueSets come from [dqm-content-qicore-2025](https://github.com/cqframework/dqm-content-qicore-2025), the official test suite for CQL quality measures. |
0 commit comments