@atomic-ehr/fhirpath 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ bun add @atomic-ehr/fhirpath
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
|
-
import
|
|
18
|
+
import { evaluate } from '@atomic-ehr/fhirpath';
|
|
19
19
|
|
|
20
20
|
const patient = {
|
|
21
21
|
name: [
|
|
@@ -26,14 +26,20 @@ const patient = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
// Simple evaluation (async)
|
|
29
|
-
const givenNames = await
|
|
29
|
+
const givenNames = await evaluate('name.given', { input: patient });
|
|
30
30
|
console.log(givenNames); // ['John', 'James', 'Johnny']
|
|
31
31
|
|
|
32
32
|
// With filtering
|
|
33
|
-
const officialName = await
|
|
33
|
+
const officialName = await evaluate(
|
|
34
|
+
"name.where(use = 'official').given",
|
|
35
|
+
{ input: patient }
|
|
36
|
+
);
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
38
|
+
// With variables
|
|
39
|
+
const result = await evaluate('%x + 5', {
|
|
40
|
+
variables: { x: 10 }
|
|
41
|
+
});
|
|
42
|
+
console.log(result); // [15]
|
|
37
43
|
```
|
|
38
44
|
|
|
39
45
|
## API Reference
|
|
@@ -47,68 +53,63 @@ const age = await fhirpath.evaluate('today().year() - birthDate.toDateTime().yea
|
|
|
47
53
|
Parses a FHIRPath expression string into an AST (Abstract Syntax Tree) with optional parser features.
|
|
48
54
|
|
|
49
55
|
```typescript
|
|
50
|
-
|
|
51
|
-
const result = fhirpath.parse('Patient.name.given');
|
|
52
|
-
console.log(result.ast); // The parsed AST
|
|
53
|
-
console.log(result.diagnostics); // Array of any syntax issues
|
|
54
|
-
console.log(result.hasErrors); // Boolean indicating if there are errors
|
|
56
|
+
import { parse } from '@atomic-ehr/fhirpath';
|
|
55
57
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
//
|
|
58
|
+
// Basic parsing (collects diagnostics)
|
|
59
|
+
const result = parse('Patient.name.given');
|
|
60
|
+
console.log(result.ast); // The parsed AST
|
|
61
|
+
console.log(result.errors); // Array of any syntax errors
|
|
59
62
|
|
|
60
|
-
// Parse with
|
|
61
|
-
const result =
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// Parse with error recovery for IDE tools
|
|
64
|
+
const result = parse('Patient..name', {
|
|
65
|
+
mode: 'lsp', // Enable LSP mode
|
|
66
|
+
errorRecovery: true // Continue parsing after errors
|
|
64
67
|
});
|
|
65
|
-
console.log(result.
|
|
66
|
-
console.log(result.
|
|
68
|
+
console.log(result.errors); // Contains parse errors
|
|
69
|
+
console.log(result.ast); // Partial AST with error nodes
|
|
67
70
|
```
|
|
68
71
|
|
|
69
72
|
**Parser Options:**
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
72
|
-
- `
|
|
73
|
-
- `maxErrors?: number` - Maximum number of errors to collect before stopping
|
|
73
|
+
- `mode?: 'simple' | 'lsp'` - Parser mode (simple for fast parsing, lsp for IDE features)
|
|
74
|
+
- `errorRecovery?: boolean` - Enable error recovery to continue parsing after errors (LSP mode only)
|
|
75
|
+
- `cursorPosition?: number` - Cursor position for completion support (LSP mode only)
|
|
74
76
|
|
|
75
|
-
#### `evaluate(expression: string
|
|
77
|
+
#### `evaluate(expression: string, options?: EvaluateOptions): Promise<any[]>`
|
|
76
78
|
|
|
77
|
-
Evaluates a FHIRPath expression against input data. **Note: This function is
|
|
79
|
+
Evaluates a FHIRPath expression against input data. **Note: This function is async.**
|
|
78
80
|
|
|
79
81
|
```typescript
|
|
80
|
-
|
|
81
|
-
const names = await fhirpath.evaluate('name.family', patient);
|
|
82
|
+
import { evaluate } from '@atomic-ehr/fhirpath';
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
const names = await fhirpath.evaluate(expr, patient);
|
|
84
|
+
// Simple evaluation
|
|
85
|
+
const names = await evaluate('name.family', { input: patient });
|
|
86
86
|
|
|
87
|
-
// With
|
|
88
|
-
const result = await
|
|
87
|
+
// With variables
|
|
88
|
+
const result = await evaluate('%myVar + 5', {
|
|
89
89
|
variables: { myVar: 10 }
|
|
90
90
|
}); // [15]
|
|
91
|
-
```
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const compiled = await fhirpath.compile('name.given');
|
|
92
|
+
// With model provider for type checking
|
|
93
|
+
const modelProvider = new FHIRModelProvider({
|
|
94
|
+
packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
|
|
95
|
+
});
|
|
96
|
+
await modelProvider.initialize();
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
const typed = await evaluate('Patient.name.given', {
|
|
99
|
+
input: patient,
|
|
100
|
+
modelProvider,
|
|
101
|
+
inputType: { type: 'Patient', singleton: true }
|
|
102
|
+
});
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
|
|
105
106
|
#### `analyze(expression: string, options?: AnalyzeOptions): Promise<AnalysisResult>`
|
|
106
107
|
|
|
107
108
|
Performs static type analysis on a FHIRPath expression, with optional type checking using a FHIR model provider and error recovery for broken expressions. **Note: This function is now async.**
|
|
108
109
|
|
|
109
110
|
```typescript
|
|
110
111
|
// Basic analysis
|
|
111
|
-
const analysis = await
|
|
112
|
+
const analysis = await analyze('name.given');
|
|
112
113
|
console.log(analysis.diagnostics); // Array of any issues found
|
|
113
114
|
console.log(analysis.ast); // The analyzed AST with type information
|
|
114
115
|
|
|
@@ -120,13 +121,13 @@ const modelProvider = new FHIRModelProvider({
|
|
|
120
121
|
});
|
|
121
122
|
await modelProvider.initialize();
|
|
122
123
|
|
|
123
|
-
const analysis = await
|
|
124
|
+
const analysis = await analyze('Patient.birthDate.substring(0, 4)', {
|
|
124
125
|
modelProvider
|
|
125
126
|
});
|
|
126
127
|
// Will report type error: substring() expects String but birthDate is date
|
|
127
128
|
|
|
128
129
|
// With error recovery for IDE/tooling scenarios
|
|
129
|
-
const analysis = await
|
|
130
|
+
const analysis = await analyze('Patient.name.', {
|
|
130
131
|
errorRecovery: true, // Won't throw on syntax errors
|
|
131
132
|
modelProvider
|
|
132
133
|
});
|
|
@@ -134,11 +135,10 @@ console.log(analysis.diagnostics); // Contains parse error: "Expected identifier
|
|
|
134
135
|
console.log(analysis.ast); // Partial AST with error nodes
|
|
135
136
|
|
|
136
137
|
// With input type context
|
|
137
|
-
const analysis = await
|
|
138
|
+
const analysis = await analyze('name.given', {
|
|
138
139
|
modelProvider,
|
|
139
140
|
inputType: {
|
|
140
141
|
type: 'HumanName',
|
|
141
|
-
namespace: 'FHIR',
|
|
142
142
|
singleton: false
|
|
143
143
|
}
|
|
144
144
|
});
|
|
@@ -150,15 +150,17 @@ const analysis = await fhirpath.analyze('name.given', {
|
|
|
150
150
|
- `variables?: Record<string, unknown>` - Variables available in the expression context
|
|
151
151
|
- `inputType?: TypeInfo` - Type information for the input context
|
|
152
152
|
|
|
153
|
-
#### `inspect(expression: string
|
|
153
|
+
#### `inspect(expression: string, options?: InspectOptions): Promise<InspectResult>`
|
|
154
154
|
|
|
155
|
-
Evaluates an expression while capturing rich debugging information including traces, execution time, and AST. **Note: This function is
|
|
155
|
+
Evaluates an expression while capturing rich debugging information including traces, execution time, and AST. **Note: This function is async.**
|
|
156
156
|
|
|
157
157
|
```typescript
|
|
158
|
+
import { inspect } from '@atomic-ehr/fhirpath';
|
|
159
|
+
|
|
158
160
|
// Basic usage - capture trace output
|
|
159
|
-
const result = await
|
|
161
|
+
const result = await inspect(
|
|
160
162
|
'name.trace("names").given.trace("given names")',
|
|
161
|
-
patient
|
|
163
|
+
{ input: patient }
|
|
162
164
|
);
|
|
163
165
|
|
|
164
166
|
console.log(result.result); // ['John', 'James', 'Johnny']
|
|
@@ -173,18 +175,16 @@ result.traces.forEach(trace => {
|
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
// With options
|
|
176
|
-
const detailedResult = await
|
|
178
|
+
const detailedResult = await inspect(
|
|
177
179
|
'Patient.name.where(use = "official")',
|
|
178
|
-
bundle,
|
|
179
|
-
undefined,
|
|
180
180
|
{
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
input: bundle,
|
|
182
|
+
maxTraces: 100 // Limit number of traces collected
|
|
183
183
|
}
|
|
184
184
|
);
|
|
185
185
|
|
|
186
186
|
// Error handling
|
|
187
|
-
const errorResult = await
|
|
187
|
+
const errorResult = await inspect('invalid.expression()', {});
|
|
188
188
|
if (errorResult.errors) {
|
|
189
189
|
console.log('Errors:', errorResult.errors);
|
|
190
190
|
}
|
|
@@ -200,52 +200,29 @@ The `InspectResult` contains:
|
|
|
200
200
|
- `warnings`: Any warnings (optional)
|
|
201
201
|
- `evaluationSteps`: Step-by-step evaluation details (when enabled)
|
|
202
202
|
|
|
203
|
-
### Convenience Functions
|
|
204
|
-
|
|
205
|
-
#### `parseForEvaluation(expression: string): ASTNode`
|
|
206
|
-
|
|
207
|
-
Parses an expression and returns just the AST, throwing on any syntax errors. This is the most performant option for production use.
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
try {
|
|
211
|
-
const ast = fhirpath.parseForEvaluation('Patient.name.given');
|
|
212
|
-
// Use ast for evaluation
|
|
213
|
-
} catch (error) {
|
|
214
|
-
console.error('Parse error:', error.message);
|
|
215
|
-
}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
#### `validate(expression: string): { valid: boolean; diagnostics: ParseDiagnostic[] }`
|
|
219
|
-
|
|
220
|
-
Validates a FHIRPath expression syntax without throwing errors.
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
const validation = fhirpath.validate('Patient..name');
|
|
224
|
-
if (!validation.valid) {
|
|
225
|
-
console.log('Syntax errors:', validation.diagnostics);
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
203
|
|
|
229
204
|
### Registry API
|
|
230
205
|
|
|
231
206
|
The registry provides introspection capabilities for available operations.
|
|
232
207
|
|
|
233
208
|
```typescript
|
|
209
|
+
import { registry } from '@atomic-ehr/fhirpath';
|
|
210
|
+
|
|
234
211
|
// List all available functions
|
|
235
|
-
const functions =
|
|
212
|
+
const functions = registry.listFunctions();
|
|
236
213
|
console.log(functions.map(f => f.name)); // ['where', 'select', 'first', ...]
|
|
237
214
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
215
|
+
// List all available operators
|
|
216
|
+
const operators = registry.listOperators();
|
|
217
|
+
console.log(operators.map(o => o.name)); // ['+', '-', '*', ...]
|
|
241
218
|
|
|
242
|
-
// Get
|
|
243
|
-
const
|
|
244
|
-
console.log(
|
|
219
|
+
// Get function metadata
|
|
220
|
+
const whereFunction = registry.getFunction('where');
|
|
221
|
+
console.log(whereFunction?.signatures); // Function signatures
|
|
245
222
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
223
|
+
// Get operator metadata
|
|
224
|
+
const plusOperator = registry.getOperator('+');
|
|
225
|
+
console.log(plusOperator?.precedence); // Operator precedence
|
|
249
226
|
```
|
|
250
227
|
|
|
251
228
|
### FHIR Model Provider
|
|
@@ -290,73 +267,6 @@ The model provider supports:
|
|
|
290
267
|
- Extension navigation
|
|
291
268
|
- Full FHIR R4 type system
|
|
292
269
|
|
|
293
|
-
### Builder Pattern
|
|
294
|
-
|
|
295
|
-
For advanced configurations, use the builder pattern:
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
import { FHIRPath } from '@atomic-ehr/fhirpath';
|
|
299
|
-
|
|
300
|
-
const fp = FHIRPath.builder()
|
|
301
|
-
// Add custom functions
|
|
302
|
-
.withCustomFunction('double', async (context, input) => {
|
|
303
|
-
return input.map(x => x * 2);
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
// Set default variables
|
|
307
|
-
.withVariable('defaultStatus', 'active')
|
|
308
|
-
|
|
309
|
-
// Add model provider for type information
|
|
310
|
-
.withModelProvider({
|
|
311
|
-
getType: async (typeName) => { /* ... */ },
|
|
312
|
-
getElementType: async (parentType, propertyName) => { /* ... */ },
|
|
313
|
-
ofType: (type, typeName) => { /* ... */ },
|
|
314
|
-
getElementNames: (parentType) => { /* ... */ },
|
|
315
|
-
getChildrenType: async (parentType) => { /* ... */ }
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
.build();
|
|
319
|
-
|
|
320
|
-
// Use the configured instance (async)
|
|
321
|
-
const result = await fp.evaluate('value.double()', { value: [5] }); // [10]
|
|
322
|
-
const status = await fp.evaluate('%defaultStatus'); // ['active']
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### Custom Functions
|
|
326
|
-
|
|
327
|
-
Custom functions extend FHIRPath with domain-specific operations:
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
const fp = FHIRPath.builder()
|
|
331
|
-
.withCustomFunction('age', async (context, input) => {
|
|
332
|
-
// Calculate age from birthDate
|
|
333
|
-
return input.map(birthDate => {
|
|
334
|
-
const today = new Date();
|
|
335
|
-
const birth = new Date(birthDate);
|
|
336
|
-
let age = today.getFullYear() - birth.getFullYear();
|
|
337
|
-
const monthDiff = today.getMonth() - birth.getMonth();
|
|
338
|
-
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
|
339
|
-
age--;
|
|
340
|
-
}
|
|
341
|
-
return age;
|
|
342
|
-
});
|
|
343
|
-
})
|
|
344
|
-
.withCustomFunction('fullName', async (context, input) => {
|
|
345
|
-
return input.map(name => {
|
|
346
|
-
if (name && typeof name === 'object') {
|
|
347
|
-
const given = Array.isArray(name.given) ? name.given.join(' ') : '';
|
|
348
|
-
const family = name.family || '';
|
|
349
|
-
return `${given} ${family}`.trim();
|
|
350
|
-
}
|
|
351
|
-
return '';
|
|
352
|
-
});
|
|
353
|
-
})
|
|
354
|
-
.build();
|
|
355
|
-
|
|
356
|
-
// Use custom functions (async)
|
|
357
|
-
const age = await fp.evaluate('birthDate.age()', patient);
|
|
358
|
-
const fullNames = await fp.evaluate('name.fullName()', patient);
|
|
359
|
-
```
|
|
360
270
|
|
|
361
271
|
### Error Handling
|
|
362
272
|
|
|
@@ -382,29 +292,26 @@ The library is fully typed with TypeScript:
|
|
|
382
292
|
|
|
383
293
|
```typescript
|
|
384
294
|
import type {
|
|
385
|
-
FHIRPathExpression,
|
|
386
|
-
CompiledExpression,
|
|
387
|
-
EvaluationContext,
|
|
388
|
-
ModelProvider,
|
|
389
|
-
CustomFunction,
|
|
390
|
-
OperationInfo,
|
|
391
|
-
AnalysisResult,
|
|
392
|
-
InspectResult,
|
|
393
|
-
InspectOptions,
|
|
394
|
-
// Parser types
|
|
395
|
-
ParserOptions,
|
|
396
|
-
ParseResult,
|
|
397
|
-
ParseDiagnostic,
|
|
398
|
-
DiagnosticSeverity,
|
|
399
|
-
TextRange,
|
|
400
295
|
ASTNode,
|
|
401
|
-
|
|
402
|
-
AnalyzeOptions,
|
|
296
|
+
AnalysisResult,
|
|
403
297
|
Diagnostic,
|
|
298
|
+
DiagnosticSeverity,
|
|
404
299
|
TypeInfo,
|
|
300
|
+
ModelProvider,
|
|
301
|
+
OperatorDefinition,
|
|
302
|
+
FunctionDefinition,
|
|
303
|
+
// Parser types
|
|
304
|
+
ParseResult,
|
|
405
305
|
// FHIR Model Provider
|
|
406
306
|
FHIRModelProvider,
|
|
407
|
-
FHIRModelProviderConfig
|
|
307
|
+
FHIRModelProviderConfig,
|
|
308
|
+
// Inspect types
|
|
309
|
+
InspectResult,
|
|
310
|
+
InspectOptions,
|
|
311
|
+
ASTMetadata,
|
|
312
|
+
// Completion types
|
|
313
|
+
CompletionItem,
|
|
314
|
+
CompletionOptions
|
|
408
315
|
} from '@atomic-ehr/fhirpath';
|
|
409
316
|
```
|
|
410
317
|
|
|
@@ -415,13 +322,13 @@ The analyzer supports error recovery mode for IDE and tooling scenarios, allowin
|
|
|
415
322
|
```typescript
|
|
416
323
|
// Normal mode - throws on syntax errors
|
|
417
324
|
try {
|
|
418
|
-
const result = await
|
|
325
|
+
const result = await analyze('Patient.name.');
|
|
419
326
|
} catch (error) {
|
|
420
327
|
console.error('Syntax error:', error.message);
|
|
421
328
|
}
|
|
422
329
|
|
|
423
330
|
// Error recovery mode - continues analysis despite errors
|
|
424
|
-
const result = await
|
|
331
|
+
const result = await analyze('Patient.name.', {
|
|
425
332
|
errorRecovery: true
|
|
426
333
|
});
|
|
427
334
|
|
|
@@ -437,14 +344,14 @@ console.log(result.diagnostics);
|
|
|
437
344
|
console.log(result.ast); // Partial AST with error nodes
|
|
438
345
|
|
|
439
346
|
// Complex broken expression - multiple errors reported
|
|
440
|
-
const result2 = await
|
|
347
|
+
const result2 = await analyze('Patient.name.where(use = ).given.', {
|
|
441
348
|
errorRecovery: true,
|
|
442
349
|
modelProvider
|
|
443
350
|
});
|
|
444
351
|
console.log(result2.diagnostics.length); // 2 errors
|
|
445
352
|
|
|
446
353
|
// Type information is preserved for valid parts
|
|
447
|
-
const result3 = await
|
|
354
|
+
const result3 = await analyze('5 + 3 * ', {
|
|
448
355
|
errorRecovery: true
|
|
449
356
|
});
|
|
450
357
|
// Even though expression is incomplete, literals 5 and 3 have type info
|
|
@@ -456,6 +363,107 @@ This is particularly useful for:
|
|
|
456
363
|
- **Code completion**: Analyze partial expressions for context
|
|
457
364
|
- **Linting tools**: Report all issues in a single pass
|
|
458
365
|
|
|
366
|
+
## Supported Features
|
|
367
|
+
|
|
368
|
+
### Complete FHIRPath Function Coverage
|
|
369
|
+
|
|
370
|
+
This implementation supports **100% of FHIRPath functions** as defined in the specification:
|
|
371
|
+
|
|
372
|
+
#### Arithmetic Functions (13)
|
|
373
|
+
- **Basic**: `+`, `-`, `*`, `/`, `div`, `mod`, `power`
|
|
374
|
+
- **Math**: `abs`, `ceiling`, `floor`, `round`, `sqrt`, `truncate`
|
|
375
|
+
|
|
376
|
+
#### String Functions (19)
|
|
377
|
+
- **Properties**: `length`
|
|
378
|
+
- **Manipulation**: `substring`, `upper`, `lower`, `trim`, `toChars`, `repeat`
|
|
379
|
+
- **Testing**: `startsWith`, `endsWith`, `contains`, `matches`, `matchesFull`
|
|
380
|
+
- **Search**: `indexOf`, `lastIndexOf`
|
|
381
|
+
- **Transform**: `replace`, `replaceMatches`, `split`, `join`
|
|
382
|
+
|
|
383
|
+
#### Collection Functions (23)
|
|
384
|
+
- **Filtering**: `where`, `select`, `ofType`
|
|
385
|
+
- **Subsetting**: `first`, `last`, `tail`, `skip`, `take`, `single`
|
|
386
|
+
- **Existence**: `exists`, `empty`, `count`, `distinct`, `isDistinct`
|
|
387
|
+
- **Aggregation**: `all`, `allTrue`, `anyTrue`, `allFalse`, `anyFalse`
|
|
388
|
+
- **Combining**: `combine`, `union`, `intersect`, `exclude`
|
|
389
|
+
- **Membership**: `subsetOf`, `supersetOf`
|
|
390
|
+
|
|
391
|
+
#### Type Functions (16)
|
|
392
|
+
- **Conversion**: `toBoolean`, `toInteger`, `toLong`, `toDecimal`, `toString`, `toQuantity`
|
|
393
|
+
- **Testing**: `convertsToBoolean`, `convertsToInteger`, `convertsToLong`, `convertsToDecimal`, `convertsToString`, `convertsToQuantity`
|
|
394
|
+
- **Operators**: `is`, `as`
|
|
395
|
+
- **Boundaries**: `highBoundary`, `lowBoundary`
|
|
396
|
+
|
|
397
|
+
#### Temporal Functions (15)
|
|
398
|
+
- **Current**: `now`, `today`, `timeOfDay`
|
|
399
|
+
- **Extraction**: `dateOf`, `timeOf`, `yearOf`, `monthOf`, `dayOf`, `hourOf`, `minuteOf`, `secondOf`, `millisecondOf`, `timezoneOffsetOf`
|
|
400
|
+
- **Arithmetic**: Full support for date/time arithmetic with durations
|
|
401
|
+
|
|
402
|
+
#### Logical Operators (6)
|
|
403
|
+
- **Boolean**: `and`, `or`, `xor`, `not`, `implies`
|
|
404
|
+
- **Membership**: `in`, `contains`
|
|
405
|
+
|
|
406
|
+
#### Comparison Operators (8)
|
|
407
|
+
- **Equality**: `=`, `!=`, `~`, `!~`
|
|
408
|
+
- **Ordering**: `<`, `>`, `<=`, `>=`
|
|
409
|
+
|
|
410
|
+
#### Utility Functions (7)
|
|
411
|
+
- **Control**: `iif`, `defineVariable`
|
|
412
|
+
- **Navigation**: `children`, `descendants`
|
|
413
|
+
- **Debugging**: `trace`
|
|
414
|
+
- **Advanced**: `aggregate`
|
|
415
|
+
- **Indexing**: Array indexing with `[n]`
|
|
416
|
+
|
|
417
|
+
### Advanced Features
|
|
418
|
+
|
|
419
|
+
#### FHIR Model Integration
|
|
420
|
+
- Full R4, R5, STU3, DSTU2 support via `@atomic-ehr/fhir-canonical-manager`
|
|
421
|
+
- Type-aware property navigation
|
|
422
|
+
- Polymorphic type resolution
|
|
423
|
+
- Choice type handling (e.g., `value[x]`)
|
|
424
|
+
- Extension navigation
|
|
425
|
+
|
|
426
|
+
#### UCUM Quantity Support
|
|
427
|
+
- Complete UCUM unit system
|
|
428
|
+
- Unit conversion and normalization
|
|
429
|
+
- Quantity arithmetic with unit compatibility checking
|
|
430
|
+
- Temporal quantity support (years, months, days, etc.)
|
|
431
|
+
|
|
432
|
+
#### Temporal Types
|
|
433
|
+
- FHIR Date, DateTime, and Time types
|
|
434
|
+
- Timezone-aware operations
|
|
435
|
+
- Partial date support (e.g., "2023", "2023-05")
|
|
436
|
+
- Date/time boundary calculations
|
|
437
|
+
- Duration arithmetic
|
|
438
|
+
|
|
439
|
+
#### Variables and Context
|
|
440
|
+
- User-defined variables with `%` prefix
|
|
441
|
+
- Built-in variables: `$this`, `$index`, `$total`
|
|
442
|
+
- System variables: `%context`, `%resource`, `%rootResource`, `%ucum`
|
|
443
|
+
- Scoped variable definitions with `defineVariable()`
|
|
444
|
+
|
|
445
|
+
#### Type System
|
|
446
|
+
- Complete type inference
|
|
447
|
+
- Union type support
|
|
448
|
+
- Polymorphic function signatures
|
|
449
|
+
- Type coercion rules
|
|
450
|
+
- Singleton vs collection tracking
|
|
451
|
+
|
|
452
|
+
#### Performance Optimizations
|
|
453
|
+
- Compiled expression mode for repeated evaluations
|
|
454
|
+
- Prototype-based context for minimal allocation
|
|
455
|
+
- Schema and type hierarchy caching
|
|
456
|
+
- Optimized operator dispatch
|
|
457
|
+
- Lazy evaluation where applicable
|
|
458
|
+
|
|
459
|
+
#### IDE and Tooling Support
|
|
460
|
+
- LSP (Language Server Protocol) integration
|
|
461
|
+
- Context-aware code completions
|
|
462
|
+
- Error recovery for partial expressions
|
|
463
|
+
- Source range tracking for diagnostics
|
|
464
|
+
- Trivia preservation (comments, whitespace)
|
|
465
|
+
- Step-by-step debugging with `inspect()`
|
|
466
|
+
|
|
459
467
|
## Common Use Cases
|
|
460
468
|
|
|
461
469
|
### Working with FHIR Resources
|
|
@@ -471,54 +479,542 @@ const bundle = {
|
|
|
471
479
|
};
|
|
472
480
|
|
|
473
481
|
// Get all patients
|
|
474
|
-
const patients = await
|
|
475
|
-
|
|
476
|
-
bundle
|
|
482
|
+
const patients = await evaluate(
|
|
483
|
+
"entry.resource.where(resourceType = 'Patient')",
|
|
484
|
+
{ input: bundle }
|
|
477
485
|
);
|
|
478
486
|
|
|
479
487
|
// Get active patients
|
|
480
|
-
const activePatients = await
|
|
481
|
-
|
|
482
|
-
bundle
|
|
488
|
+
const activePatients = await evaluate(
|
|
489
|
+
"entry.resource.where(resourceType = 'Patient' and active = true)",
|
|
490
|
+
{ input: bundle }
|
|
483
491
|
);
|
|
484
492
|
|
|
485
493
|
// Count resources by type
|
|
486
|
-
const patientCount = await
|
|
487
|
-
|
|
488
|
-
bundle
|
|
494
|
+
const patientCount = await evaluate(
|
|
495
|
+
"entry.resource.where(resourceType = 'Patient').count()",
|
|
496
|
+
{ input: bundle }
|
|
489
497
|
); // [2]
|
|
490
498
|
```
|
|
491
499
|
|
|
492
|
-
### Complex Filtering
|
|
500
|
+
### Complex Filtering and Navigation
|
|
501
|
+
|
|
502
|
+
**Important Note on Polymorphic Properties**: In FHIR JSON, polymorphic properties like `value[x]` are stored with their type suffix (e.g., `valueQuantity`, `valueString`). However, in FHIRPath expressions, you access them without the suffix and use `ofType()` to filter by type:
|
|
503
|
+
- JSON: `observation.valueQuantity.value`
|
|
504
|
+
- FHIRPath: `observation.value.ofType(Quantity).value`
|
|
493
505
|
|
|
494
506
|
```typescript
|
|
507
|
+
// Blood pressure observations (FHIR Observation resources)
|
|
495
508
|
const observations = [
|
|
496
|
-
{
|
|
497
|
-
|
|
498
|
-
|
|
509
|
+
{
|
|
510
|
+
resourceType: 'Observation',
|
|
511
|
+
code: { coding: [{ system: 'loinc', code: '85354-9' }] },
|
|
512
|
+
valueQuantity: { value: 140, unit: 'mm[Hg]' },
|
|
513
|
+
effectiveDateTime: '2024-01-15T10:30:00Z' // In FHIR JSON, stored as effectiveDateTime
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
resourceType: 'Observation',
|
|
517
|
+
code: { coding: [{ system: 'loinc', code: '85354-9' }] },
|
|
518
|
+
valueQuantity: { value: 120, unit: 'mm[Hg]' },
|
|
519
|
+
effectiveDateTime: '2024-01-14T09:00:00Z' // In FHIR JSON, stored as effectiveDateTime
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
resourceType: 'Observation',
|
|
523
|
+
code: { coding: [{ system: 'loinc', code: '8310-5' }] },
|
|
524
|
+
valueQuantity: { value: 98.6, unit: '[degF]' },
|
|
525
|
+
effectiveDateTime: '2024-01-15T10:30:00Z' // In FHIR JSON, stored as effectiveDateTime
|
|
526
|
+
}
|
|
499
527
|
];
|
|
500
528
|
|
|
501
|
-
// Find high blood pressure readings
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
529
|
+
// Find high blood pressure readings (systolic > 130)
|
|
530
|
+
// Note: In FHIRPath, polymorphic properties like value[x] are accessed directly as 'value'
|
|
531
|
+
const highBP = await evaluate(
|
|
532
|
+
`where(code.coding.exists(system = 'loinc' and code = '85354-9')
|
|
533
|
+
and value.ofType(Quantity).value > 130)`,
|
|
534
|
+
{ input: observations }
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// Get all observation values with units (for Quantity types)
|
|
538
|
+
const valuesWithUnits = await evaluate(
|
|
539
|
+
"value.ofType(Quantity).select(value.toString() + ' ' + unit)",
|
|
540
|
+
{ input: observations }
|
|
541
|
+
); // ['140 mm[Hg]', '120 mm[Hg]', '98.6 [degF]']
|
|
542
|
+
|
|
543
|
+
// Find most recent observation
|
|
544
|
+
// Note: effective[x] can be DateTime, Period, Timing, or Instant
|
|
545
|
+
const mostRecent = await evaluate(
|
|
546
|
+
'where(effective.ofType(dateTime) = effective.ofType(dateTime).max()).first()',
|
|
547
|
+
{ input: observations }
|
|
505
548
|
);
|
|
506
549
|
```
|
|
507
550
|
|
|
508
|
-
###
|
|
551
|
+
### Patient Demographics and Identifiers
|
|
509
552
|
|
|
510
553
|
```typescript
|
|
511
554
|
const patient = {
|
|
512
|
-
|
|
555
|
+
resourceType: 'Patient',
|
|
556
|
+
identifier: [
|
|
557
|
+
{ system: 'http://hospital.org/mrn', value: 'MRN-12345' },
|
|
558
|
+
{ system: 'http://hl7.org/fhir/sid/us-ssn', value: '123-45-6789' }
|
|
559
|
+
],
|
|
560
|
+
name: [
|
|
561
|
+
{
|
|
562
|
+
use: 'official',
|
|
563
|
+
family: 'Smith',
|
|
564
|
+
given: ['John', 'Robert'],
|
|
565
|
+
prefix: ['Dr']
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
use: 'nickname',
|
|
569
|
+
given: ['Johnny']
|
|
570
|
+
}
|
|
571
|
+
],
|
|
572
|
+
telecom: [
|
|
573
|
+
{ system: 'phone', value: '555-0123', use: 'home' },
|
|
574
|
+
{ system: 'email', value: 'john@example.com', use: 'work' }
|
|
575
|
+
],
|
|
576
|
+
birthDate: '1980-05-15',
|
|
577
|
+
address: [
|
|
578
|
+
{
|
|
579
|
+
use: 'home',
|
|
580
|
+
line: ['123 Main St', 'Apt 4B'],
|
|
581
|
+
city: 'Boston',
|
|
582
|
+
state: 'MA',
|
|
583
|
+
postalCode: '02101'
|
|
584
|
+
}
|
|
585
|
+
]
|
|
513
586
|
};
|
|
514
587
|
|
|
515
|
-
//
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
patient
|
|
588
|
+
// Get patient's MRN
|
|
589
|
+
const mrn = await evaluate(
|
|
590
|
+
"identifier.where(system = 'http://hospital.org/mrn').value",
|
|
591
|
+
{ input: patient }
|
|
592
|
+
); // ['MRN-12345']
|
|
593
|
+
|
|
594
|
+
// Get full official name
|
|
595
|
+
const fullName = await evaluate(
|
|
596
|
+
"name.where(use = 'official').select((prefix | {}).first() + ' ' + given.join(' ') + ' ' + family)",
|
|
597
|
+
{ input: patient }
|
|
598
|
+
); // ['Dr John Robert Smith']
|
|
599
|
+
|
|
600
|
+
// Get all phone numbers
|
|
601
|
+
const phones = await evaluate(
|
|
602
|
+
"telecom.where(system = 'phone').value",
|
|
603
|
+
{ input: patient }
|
|
604
|
+
); // ['555-0123']
|
|
605
|
+
|
|
606
|
+
// Get home address as single line
|
|
607
|
+
const address = await evaluate(
|
|
608
|
+
"address.where(use = 'home').select(line.join(', ') + ', ' + city + ', ' + state + ' ' + postalCode)",
|
|
609
|
+
{ input: patient }
|
|
610
|
+
); // ['123 Main St, Apt 4B, Boston, MA 02101']
|
|
611
|
+
|
|
612
|
+
// Calculate age
|
|
613
|
+
const age = await evaluate(
|
|
614
|
+
"today().toString().substring(0, 4).toInteger() - birthDate.substring(0, 4).toInteger()",
|
|
615
|
+
{ input: patient }
|
|
616
|
+
); // [44] (as of 2024)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Medication and Dosage Calculations
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
const medicationRequest = {
|
|
623
|
+
resourceType: 'MedicationRequest',
|
|
624
|
+
medication: {
|
|
625
|
+
coding: [{
|
|
626
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
627
|
+
code: '1049502',
|
|
628
|
+
display: 'Acetaminophen 325 MG Oral Tablet'
|
|
629
|
+
}]
|
|
630
|
+
},
|
|
631
|
+
dosageInstruction: [
|
|
632
|
+
{
|
|
633
|
+
timing: {
|
|
634
|
+
repeat: {
|
|
635
|
+
frequency: 2,
|
|
636
|
+
period: 1,
|
|
637
|
+
periodUnit: 'd'
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
doseAndRate: [{
|
|
641
|
+
doseQuantity: {
|
|
642
|
+
value: 650,
|
|
643
|
+
unit: 'mg',
|
|
644
|
+
system: 'http://unitsofmeasure.org'
|
|
645
|
+
}
|
|
646
|
+
}]
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
dispenseRequest: {
|
|
650
|
+
quantity: {
|
|
651
|
+
value: 60,
|
|
652
|
+
unit: 'TAB'
|
|
653
|
+
},
|
|
654
|
+
expectedSupplyDuration: {
|
|
655
|
+
value: 30,
|
|
656
|
+
unit: 'd'
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Get medication name (medication[x] can be CodeableConcept or Reference)
|
|
662
|
+
const medName = await evaluate(
|
|
663
|
+
'medication.ofType(CodeableConcept).coding.display.first()',
|
|
664
|
+
{ input: medicationRequest }
|
|
665
|
+
); // ['Acetaminophen 325 MG Oral Tablet']
|
|
666
|
+
|
|
667
|
+
// Calculate daily dose
|
|
668
|
+
const dailyDose = await evaluate(
|
|
669
|
+
'dosageInstruction.first().doseAndRate.doseQuantity.value * dosageInstruction.first().timing.repeat.frequency',
|
|
670
|
+
{ input: medicationRequest }
|
|
671
|
+
); // [1300] (650mg * 2 times per day)
|
|
672
|
+
|
|
673
|
+
// Get supply duration
|
|
674
|
+
const duration = await evaluate(
|
|
675
|
+
"dispenseRequest.expectedSupplyDuration.value.toString() + ' ' + dispenseRequest.expectedSupplyDuration.unit",
|
|
676
|
+
{ input: medicationRequest }
|
|
677
|
+
); // ['30 d']
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Date and Time Operations
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
const appointment = {
|
|
684
|
+
resourceType: 'Appointment',
|
|
685
|
+
start: '2024-03-15T14:30:00Z',
|
|
686
|
+
end: '2024-03-15T15:00:00Z',
|
|
687
|
+
created: '2024-03-01T10:00:00Z',
|
|
688
|
+
participant: [
|
|
689
|
+
{
|
|
690
|
+
actor: { reference: 'Patient/123' },
|
|
691
|
+
required: 'required',
|
|
692
|
+
status: 'accepted'
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
actor: { reference: 'Practitioner/456' },
|
|
696
|
+
required: 'required',
|
|
697
|
+
status: 'accepted'
|
|
698
|
+
}
|
|
699
|
+
]
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// Calculate appointment duration in minutes
|
|
703
|
+
const duration = await evaluate(
|
|
704
|
+
'(end - start) / 1 minute',
|
|
705
|
+
{ input: appointment }
|
|
706
|
+
); // [30]
|
|
707
|
+
|
|
708
|
+
// Check if appointment is in the future
|
|
709
|
+
const isFuture = await evaluate(
|
|
710
|
+
'start > now()',
|
|
711
|
+
{ input: appointment }
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
// Get appointment date only
|
|
715
|
+
const appointmentDate = await evaluate(
|
|
716
|
+
'start.toString().substring(0, 10)',
|
|
717
|
+
{ input: appointment }
|
|
718
|
+
); // ['2024-03-15']
|
|
719
|
+
|
|
720
|
+
// Check if all participants accepted
|
|
721
|
+
const allAccepted = await evaluate(
|
|
722
|
+
"participant.all(status = 'accepted')",
|
|
723
|
+
{ input: appointment }
|
|
724
|
+
); // [true]
|
|
725
|
+
|
|
726
|
+
// Days until appointment
|
|
727
|
+
const daysUntil = await evaluate(
|
|
728
|
+
'(start - now()) / 1 day',
|
|
729
|
+
{ input: appointment }
|
|
519
730
|
);
|
|
520
731
|
```
|
|
521
732
|
|
|
733
|
+
### Laboratory Results Analysis
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
const labResults = {
|
|
737
|
+
resourceType: 'Bundle',
|
|
738
|
+
entry: [
|
|
739
|
+
{
|
|
740
|
+
resource: {
|
|
741
|
+
resourceType: 'Observation',
|
|
742
|
+
code: {
|
|
743
|
+
coding: [{
|
|
744
|
+
system: 'http://loinc.org',
|
|
745
|
+
code: '2951-2',
|
|
746
|
+
display: 'Sodium'
|
|
747
|
+
}]
|
|
748
|
+
},
|
|
749
|
+
valueQuantity: { value: 145, unit: 'mmol/L' }, // In actual FHIR, this is stored as valueQuantity
|
|
750
|
+
referenceRange: [{
|
|
751
|
+
low: { value: 136, unit: 'mmol/L' },
|
|
752
|
+
high: { value: 145, unit: 'mmol/L' }
|
|
753
|
+
}],
|
|
754
|
+
status: 'final',
|
|
755
|
+
effectiveDateTime: '2024-01-15T08:00:00Z' // In FHIR JSON, stored as effectiveDateTime
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
resource: {
|
|
760
|
+
resourceType: 'Observation',
|
|
761
|
+
code: {
|
|
762
|
+
coding: [{
|
|
763
|
+
system: 'http://loinc.org',
|
|
764
|
+
code: '2823-3',
|
|
765
|
+
display: 'Potassium'
|
|
766
|
+
}]
|
|
767
|
+
},
|
|
768
|
+
valueQuantity: { value: 5.5, unit: 'mmol/L' }, // In actual FHIR, this is stored as valueQuantity
|
|
769
|
+
referenceRange: [{
|
|
770
|
+
low: { value: 3.5, unit: 'mmol/L' },
|
|
771
|
+
high: { value: 5.0, unit: 'mmol/L' }
|
|
772
|
+
}],
|
|
773
|
+
status: 'final',
|
|
774
|
+
effectiveDateTime: '2024-01-15T08:00:00Z' // In FHIR JSON, stored as effectiveDateTime
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Find all abnormal results (outside reference range)
|
|
781
|
+
const abnormalResults = await evaluate(
|
|
782
|
+
`entry.resource.where(
|
|
783
|
+
value.ofType(Quantity).value < referenceRange.low.value or
|
|
784
|
+
value.ofType(Quantity).value > referenceRange.high.value
|
|
785
|
+
)`,
|
|
786
|
+
{ input: labResults }
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
// Get test names and values
|
|
790
|
+
const testSummary = await evaluate(
|
|
791
|
+
`entry.resource.select(
|
|
792
|
+
code.coding.display.first() + ': ' +
|
|
793
|
+
value.ofType(Quantity).value.toString() + ' ' +
|
|
794
|
+
value.ofType(Quantity).unit
|
|
795
|
+
)`,
|
|
796
|
+
{ input: labResults }
|
|
797
|
+
); // ['Sodium: 145 mmol/L', 'Potassium: 5.5 mmol/L']
|
|
798
|
+
|
|
799
|
+
// Find critical values (e.g., potassium > 5.0)
|
|
800
|
+
const criticalValues = await evaluate(
|
|
801
|
+
`entry.resource.where(
|
|
802
|
+
code.coding.exists(code = '2823-3') and
|
|
803
|
+
value.ofType(Quantity).value > 5.0
|
|
804
|
+
)`,
|
|
805
|
+
{ input: labResults }
|
|
806
|
+
);
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Allergy and Intolerance Checking
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
const allergyIntolerance = {
|
|
813
|
+
resourceType: 'AllergyIntolerance',
|
|
814
|
+
clinicalStatus: {
|
|
815
|
+
coding: [{
|
|
816
|
+
system: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
|
|
817
|
+
code: 'active'
|
|
818
|
+
}]
|
|
819
|
+
},
|
|
820
|
+
verificationStatus: {
|
|
821
|
+
coding: [{
|
|
822
|
+
system: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification',
|
|
823
|
+
code: 'confirmed'
|
|
824
|
+
}]
|
|
825
|
+
},
|
|
826
|
+
type: 'allergy',
|
|
827
|
+
category: ['medication'],
|
|
828
|
+
criticality: 'high',
|
|
829
|
+
code: {
|
|
830
|
+
coding: [{
|
|
831
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
832
|
+
code: '7980',
|
|
833
|
+
display: 'Penicillin'
|
|
834
|
+
}]
|
|
835
|
+
},
|
|
836
|
+
reaction: [
|
|
837
|
+
{
|
|
838
|
+
substance: {
|
|
839
|
+
coding: [{
|
|
840
|
+
system: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
841
|
+
code: '7980',
|
|
842
|
+
display: 'Penicillin'
|
|
843
|
+
}]
|
|
844
|
+
},
|
|
845
|
+
manifestation: [
|
|
846
|
+
{
|
|
847
|
+
coding: [{
|
|
848
|
+
system: 'http://snomed.info/sct',
|
|
849
|
+
code: '39579001',
|
|
850
|
+
display: 'Anaphylaxis'
|
|
851
|
+
}]
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
coding: [{
|
|
855
|
+
system: 'http://snomed.info/sct',
|
|
856
|
+
code: '271807003',
|
|
857
|
+
display: 'Rash'
|
|
858
|
+
}]
|
|
859
|
+
}
|
|
860
|
+
],
|
|
861
|
+
severity: 'severe'
|
|
862
|
+
}
|
|
863
|
+
]
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// Check if allergy is active and confirmed
|
|
867
|
+
const isActiveConfirmed = await evaluate(
|
|
868
|
+
"clinicalStatus.coding.code = 'active' and verificationStatus.coding.code = 'confirmed'",
|
|
869
|
+
{ input: allergyIntolerance }
|
|
870
|
+
); // [true]
|
|
871
|
+
|
|
872
|
+
// Get all reaction manifestations
|
|
873
|
+
const manifestations = await evaluate(
|
|
874
|
+
'reaction.manifestation.coding.display',
|
|
875
|
+
{ input: allergyIntolerance }
|
|
876
|
+
); // ['Anaphylaxis', 'Rash']
|
|
877
|
+
|
|
878
|
+
// Check for severe reactions
|
|
879
|
+
const hasSevereReaction = await evaluate(
|
|
880
|
+
"reaction.exists(severity = 'severe')",
|
|
881
|
+
{ input: allergyIntolerance }
|
|
882
|
+
); // [true]
|
|
883
|
+
|
|
884
|
+
// Get allergen name
|
|
885
|
+
const allergen = await evaluate(
|
|
886
|
+
'code.coding.display.first()',
|
|
887
|
+
{ input: allergyIntolerance }
|
|
888
|
+
); // ['Penicillin']
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### Bundle Processing and Resource Extraction
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
const bundle = {
|
|
895
|
+
resourceType: 'Bundle',
|
|
896
|
+
type: 'searchset',
|
|
897
|
+
total: 3,
|
|
898
|
+
entry: [
|
|
899
|
+
{
|
|
900
|
+
fullUrl: 'http://example.org/Patient/123',
|
|
901
|
+
resource: {
|
|
902
|
+
resourceType: 'Patient',
|
|
903
|
+
id: '123',
|
|
904
|
+
name: [{ family: 'Smith', given: ['John'] }],
|
|
905
|
+
birthDate: '1980-01-01'
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
fullUrl: 'http://example.org/Encounter/456',
|
|
910
|
+
resource: {
|
|
911
|
+
resourceType: 'Encounter',
|
|
912
|
+
id: '456',
|
|
913
|
+
status: 'finished',
|
|
914
|
+
class: { code: 'IMP' },
|
|
915
|
+
subject: { reference: 'Patient/123' }
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
fullUrl: 'http://example.org/Condition/789',
|
|
920
|
+
resource: {
|
|
921
|
+
resourceType: 'Condition',
|
|
922
|
+
id: '789',
|
|
923
|
+
clinicalStatus: {
|
|
924
|
+
coding: [{ code: 'active' }]
|
|
925
|
+
},
|
|
926
|
+
code: {
|
|
927
|
+
coding: [{
|
|
928
|
+
system: 'http://snomed.info/sct',
|
|
929
|
+
code: '44054006',
|
|
930
|
+
display: 'Diabetes mellitus type 2'
|
|
931
|
+
}]
|
|
932
|
+
},
|
|
933
|
+
subject: { reference: 'Patient/123' },
|
|
934
|
+
onsetDateTime: '2015-06-15' // In FHIR JSON, stored as onsetDateTime
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
]
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// Extract all patients
|
|
941
|
+
const patients = await evaluate(
|
|
942
|
+
"entry.resource.where(resourceType = 'Patient')",
|
|
943
|
+
{ input: bundle }
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Get all active conditions
|
|
947
|
+
const activeConditions = await evaluate(
|
|
948
|
+
"entry.resource.where(resourceType = 'Condition' and clinicalStatus.coding.code = 'active')",
|
|
949
|
+
{ input: bundle }
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
// Count resources by type
|
|
953
|
+
const patientCount = await evaluate(
|
|
954
|
+
"entry.resource.where(resourceType = 'Patient').count()",
|
|
955
|
+
{ input: bundle }
|
|
956
|
+
); // [1]
|
|
957
|
+
|
|
958
|
+
// Get all resources for a specific patient
|
|
959
|
+
const patientResources = await evaluate(
|
|
960
|
+
"entry.resource.where(subject.reference = 'Patient/123')",
|
|
961
|
+
{ input: bundle }
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
// Extract condition names
|
|
965
|
+
const conditions = await evaluate(
|
|
966
|
+
"entry.resource.where(resourceType = 'Condition').code.coding.display",
|
|
967
|
+
{ input: bundle }
|
|
968
|
+
); // ['Diabetes mellitus type 2']
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Using Variables and Advanced Control Flow
|
|
972
|
+
|
|
973
|
+
```typescript
|
|
974
|
+
// Using defineVariable for complex calculations
|
|
975
|
+
const result = await evaluate(
|
|
976
|
+
`name
|
|
977
|
+
.defineVariable('fullName', given.first() + ' ' + family)
|
|
978
|
+
.select(%fullName + ' (' + use + ')')`,
|
|
979
|
+
{
|
|
980
|
+
input: {
|
|
981
|
+
name: [
|
|
982
|
+
{ use: 'official', given: ['John'], family: 'Doe' },
|
|
983
|
+
{ use: 'nickname', given: ['Johnny'], family: 'D' }
|
|
984
|
+
]
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
); // ['John Doe (official)', 'Johnny D (nickname)']
|
|
988
|
+
|
|
989
|
+
// Using aggregate for running calculations
|
|
990
|
+
const sum = await evaluate(
|
|
991
|
+
'aggregate(0, $total + $this)',
|
|
992
|
+
{ input: [1, 2, 3, 4, 5] }
|
|
993
|
+
); // [15]
|
|
994
|
+
|
|
995
|
+
// Using iif for conditional logic
|
|
996
|
+
const ageGroup = await evaluate(
|
|
997
|
+
"iif(age < 18, 'child', iif(age < 65, 'adult', 'senior'))",
|
|
998
|
+
{ input: { age: 70 } }
|
|
999
|
+
); // ['senior']
|
|
1000
|
+
|
|
1001
|
+
// Complex aggregation with variables
|
|
1002
|
+
const stats = await evaluate(
|
|
1003
|
+
`defineVariable('values', value)
|
|
1004
|
+
.defineVariable('sum', %values.aggregate(0, $total + $this))
|
|
1005
|
+
.defineVariable('count', %values.count())
|
|
1006
|
+
.defineVariable('avg', %sum / %count)
|
|
1007
|
+
.select('Average: ' + %avg.toString())`,
|
|
1008
|
+
{
|
|
1009
|
+
input: [
|
|
1010
|
+
{ value: 10 },
|
|
1011
|
+
{ value: 20 },
|
|
1012
|
+
{ value: 30 }
|
|
1013
|
+
]
|
|
1014
|
+
}
|
|
1015
|
+
); // ['Average: 20']
|
|
1016
|
+
```
|
|
1017
|
+
|
|
522
1018
|
### Debugging Expressions
|
|
523
1019
|
|
|
524
1020
|
Use the `inspect()` function to debug complex FHIRPath expressions:
|
|
@@ -532,14 +1028,14 @@ const bundle = {
|
|
|
532
1028
|
};
|
|
533
1029
|
|
|
534
1030
|
// Debug a complex expression with traces
|
|
535
|
-
const result = await
|
|
1031
|
+
const result = await inspect(
|
|
536
1032
|
`entry.resource
|
|
537
1033
|
.trace('all resources')
|
|
538
1034
|
.where(resourceType = 'Patient')
|
|
539
1035
|
.trace('patients only')
|
|
540
1036
|
.name.given
|
|
541
1037
|
.trace('all given names')`,
|
|
542
|
-
bundle
|
|
1038
|
+
{ input: bundle }
|
|
543
1039
|
);
|
|
544
1040
|
|
|
545
1041
|
// Analyze the execution
|
|
@@ -562,48 +1058,30 @@ result.traces.forEach(trace => {
|
|
|
562
1058
|
|
|
563
1059
|
## Performance Tips
|
|
564
1060
|
|
|
565
|
-
1. **
|
|
566
|
-
```typescript
|
|
567
|
-
// Fastest - throws on first error
|
|
568
|
-
const ast = fhirpath.parseForEvaluation('name.given');
|
|
569
|
-
|
|
570
|
-
// Or use throwOnError option
|
|
571
|
-
const result = fhirpath.parse('name.given', { throwOnError: true });
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
2. **Parse Once, Evaluate Many**: Parse expressions once and reuse the parsed AST:
|
|
1061
|
+
1. **Reuse Parsed Expressions**: Parse expressions once when possible:
|
|
575
1062
|
```typescript
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
const names = await fhirpath.evaluate(expr, patient);
|
|
579
|
-
}
|
|
1063
|
+
const parsed = parse('name.given');
|
|
1064
|
+
// Use parsed.ast for multiple evaluations
|
|
580
1065
|
```
|
|
581
1066
|
|
|
582
|
-
|
|
1067
|
+
2. **Cache Analysis Results**: For repeated type checking, cache the analyzed AST:
|
|
583
1068
|
```typescript
|
|
584
|
-
const
|
|
585
|
-
|
|
1069
|
+
const analysis = await analyze('name.given', { modelProvider });
|
|
1070
|
+
// Reuse analysis.ast which includes type information
|
|
586
1071
|
```
|
|
587
1072
|
|
|
588
|
-
|
|
1073
|
+
3. **Use Simple Parser Mode**: For production, use simple parsing:
|
|
589
1074
|
```typescript
|
|
590
|
-
//
|
|
591
|
-
const result =
|
|
592
|
-
errorRecovery: true, // Only if you need partial ASTs
|
|
593
|
-
trackRanges: true // Only if you need source mapping
|
|
594
|
-
});
|
|
1075
|
+
// Simple mode (default) - fastest
|
|
1076
|
+
const result = parse('name.given');
|
|
595
1077
|
|
|
596
|
-
//
|
|
597
|
-
const
|
|
1078
|
+
// LSP mode - only for IDE tools
|
|
1079
|
+
const result = parse('name.given', {
|
|
1080
|
+
mode: 'lsp',
|
|
1081
|
+
errorRecovery: true
|
|
1082
|
+
});
|
|
598
1083
|
```
|
|
599
1084
|
|
|
600
|
-
5. **Builder Instance**: Create a configured instance once and reuse:
|
|
601
|
-
```typescript
|
|
602
|
-
const fp = FHIRPath.builder()
|
|
603
|
-
.withCustomFunction('myFunc', /* ... */)
|
|
604
|
-
.build();
|
|
605
|
-
// Use fp instance throughout your application
|
|
606
|
-
```
|
|
607
1085
|
|
|
608
1086
|
## Contributing
|
|
609
1087
|
|