Skip to content

Commit 03918df

Browse files
committed
codex ui part 2
1 parent 185b170 commit 03918df

6 files changed

Lines changed: 537 additions & 44 deletions

File tree

test-server/README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,42 @@ A barebones implementation of the FHIR [$cql](https://build.fhir.org/ig/HL7/cql-
3333
## CQL Execution Workbench
3434

3535
When the server is running, open `http://localhost:8000` in a browser. The workbench
36-
accepts a **System-only CQL expression** (for example, `1 + 2`), translates it to ELM,
37-
and executes it against the current checkout of `cql-execution`. It displays both the
38-
native execution result and the translated ELM.
36+
supports two local testing modes:
37+
38+
- **Expression** mode accepts a System-only CQL expression (for example, `1 + 2`),
39+
wraps it in a temporary `Unfiltered` library, translates it to ELM, and executes it
40+
against the current checkout of `cql-execution`.
41+
- **FHIR Bundle** mode accepts a full CQL library plus a FHIR Bundle JSON payload,
42+
translates the library to ELM, and executes it in `Patient` context so CQL
43+
**Retrieves** such as `[Condition]` run against the supplied Bundle.
3944

4045
The page calls `POST /api/execute` with a JSON body such as:
4146

4247
```json
4348
{ "cql": "1 + 2" }
4449
```
4550

46-
It returns a JSON object containing `result` and `elm`. Invalid or non-executable CQL
47-
returns HTTP 422 with an error message. This endpoint is intended for local testing and
48-
demos; `/fhir/$cql` remains the FHIR-operation endpoint.
51+
FHIR mode includes a `bundle` field:
52+
53+
```json
54+
{
55+
"cql": "library BundleDemo version '1.0.0' ...",
56+
"bundle": {
57+
"resourceType": "Bundle",
58+
"type": "collection",
59+
"entry": []
60+
}
61+
}
62+
```
63+
64+
The response always includes `elm` and `result`. In FHIR mode, `result` contains
65+
`patientResults` and `unfilteredResults`. Invalid or non-executable CQL returns HTTP 422
66+
with an error message. Invalid Bundle payloads return HTTP 400. This endpoint is intended
67+
for local testing and demos; `/fhir/$cql` remains the FHIR-operation endpoint.
68+
69+
FHIR Bundle execution depends on the optional `cql-exec-fhir` package. If it is not
70+
available in the local environment yet, the workbench returns a clear error explaining
71+
that the dependency must be installed before FHIR mode can run.
4972

5073
## Prerequisites
5174

test-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"description": "",
2222
"dependencies": {
2323
"@cqframework/cql": "^4.0.0-beta.1",
24+
"cql-exec-fhir": "^2.1.6",
2425
"dotenv": "^17.4.2",
2526
"express": "^5.2.1",
2627
"tslog": "^4.11.0"

test-server/src/app.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'dotenv/config';
22
import express, { type Request, type Response } from 'express';
33
import path from 'node:path';
44
import logger from './logger';
5-
import { $cql, executeExpression } from './operation';
5+
import { $cql, executeExpression, executeFhirLibrary } from './operation';
66

77
const app = express();
88
app.use(express.json({ type: ['application/json', 'application/fhir+json'] }));
@@ -22,18 +22,51 @@ function getCql(body: unknown): string | undefined {
2222
return undefined;
2323
}
2424

25+
function hasBundle(body: unknown): body is { bundle: unknown } {
26+
return typeof body === 'object' && body !== null && 'bundle' in body;
27+
}
28+
29+
function getBundle(body: unknown): Record<string, unknown> | undefined {
30+
if (!hasBundle(body)) {
31+
return undefined;
32+
}
33+
34+
const { bundle } = body;
35+
if (
36+
typeof bundle === 'object' &&
37+
bundle !== null &&
38+
'resourceType' in bundle &&
39+
bundle.resourceType === 'Bundle'
40+
) {
41+
return bundle as Record<string, unknown>;
42+
}
43+
44+
return undefined;
45+
}
46+
2547
app.post('/api/execute', async (req: Request, res: Response) => {
2648
const cql = getCql(req.body);
2749
if (!cql) {
2850
return res.status(400).json({ error: "Missing non-empty 'cql' string in request body" });
2951
}
3052

3153
try {
54+
const bundle = getBundle(req.body);
55+
if (hasBundle(req.body) && !bundle) {
56+
return res.status(400).json({
57+
error: "If provided, 'bundle' must be a FHIR Bundle object with resourceType 'Bundle'"
58+
});
59+
}
60+
61+
if (bundle) {
62+
return res.json(await executeFhirLibrary(cql, bundle));
63+
}
64+
3265
return res.json(await executeExpression(cql));
3366
} catch (err) {
34-
logger.error('Error executing CQL expression:', err);
67+
logger.error('Error executing CQL request:', err);
3568
return res.status(422).json({
36-
error: err instanceof Error ? err.message : 'Unable to translate or execute the CQL expression'
69+
error: err instanceof Error ? err.message : 'Unable to translate or execute the supplied CQL'
3770
});
3871
}
3972
});

test-server/src/operation.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Parameters } from 'fhir/r4';
22
import logger from './logger';
3-
import { translate } from './translate';
3+
import { translateExpression, translateLibrary } from './translate';
44
import { CodeService, DateTime, Executor, Library, PatientSource } from '../..';
55
import { toParameters } from './convert';
66

@@ -13,12 +13,44 @@ export interface ExpressionExecution {
1313
result: unknown;
1414
}
1515

16+
export interface FhirLibraryExecution {
17+
elm: unknown;
18+
result: {
19+
patientResults: unknown;
20+
unfilteredResults: unknown;
21+
};
22+
}
23+
24+
interface FhirPatientSourceModule {
25+
PatientSource?: {
26+
FHIRv401: (options?: { requireProfileTagging?: boolean }) => {
27+
loadBundles: (bundles: unknown[]) => void;
28+
};
29+
};
30+
}
31+
32+
function getFhirPatientSource() {
33+
try {
34+
const module = require('cql-exec-fhir') as FhirPatientSourceModule;
35+
const factory = module.PatientSource?.FHIRv401;
36+
if (typeof factory !== 'function') {
37+
throw new Error("Module does not expose PatientSource.FHIRv401()");
38+
}
39+
return factory();
40+
} catch (err) {
41+
const message = err instanceof Error ? err.message : String(err);
42+
throw new Error(
43+
`FHIR execution requires the optional 'cql-exec-fhir' dependency. Install test-server dependencies when your environment allows it. Details: ${message}`
44+
);
45+
}
46+
}
47+
1648
export async function executeExpression(expression: string): Promise<ExpressionExecution> {
1749
const id = counter++;
1850
logger.debug(`[${id}] Expression: ${expression}`);
1951

2052
// 1: Translate CQL to ELM
21-
const elm = await translate(expression, USE_TRANSLATION_SERVICE);
53+
const elm = await translateExpression(expression, USE_TRANSLATION_SERVICE);
2254

2355
// 2: Execute ELM
2456
const library = new Library(elm);
@@ -32,6 +64,31 @@ export async function executeExpression(expression: string): Promise<ExpressionE
3264
return { elm, result: result.unfilteredResults.expression };
3365
}
3466

67+
export async function executeFhirLibrary(cql: string, bundle: unknown): Promise<FhirLibraryExecution> {
68+
const id = counter++;
69+
logger.debug(`[${id}] FHIR CQL library received`);
70+
71+
const elm = await translateLibrary(cql, USE_TRANSLATION_SERVICE);
72+
const library = new Library(elm);
73+
const codeService = new CodeService();
74+
const patientSource = getFhirPatientSource();
75+
patientSource.loadBundles([bundle]);
76+
77+
const executionDateTime = DateTime.fromJSDate(new Date(), 0);
78+
const executor = new Executor(library, codeService);
79+
const result = await executor.exec(patientSource as never, executionDateTime);
80+
logger.debug(`[${id}] Patient Results: `, result.patientResults);
81+
logger.debug(`[${id}] Unfiltered Results: `, result.unfilteredResults);
82+
83+
return {
84+
elm,
85+
result: {
86+
patientResults: result.patientResults,
87+
unfilteredResults: result.unfilteredResults
88+
}
89+
};
90+
}
91+
3592
export async function $cql(expression: string): Promise<Parameters> {
3693
const execution = await executeExpression(expression);
3794

0 commit comments

Comments
 (0)