@atomic-ehr/fhirpath 0.0.1-canary.1825db0.20250725140030

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.
Files changed (59) hide show
  1. package/README.md +400 -0
  2. package/dist/index.d.ts +398 -0
  3. package/dist/index.js +8372 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/analyzer/analyzer.ts +486 -0
  7. package/src/analyzer/model-provider.ts +244 -0
  8. package/src/analyzer/schemas/index.ts +2 -0
  9. package/src/analyzer/schemas/types.ts +40 -0
  10. package/src/analyzer/types.ts +142 -0
  11. package/src/api/builder.ts +155 -0
  12. package/src/api/errors.ts +134 -0
  13. package/src/api/expression.ts +156 -0
  14. package/src/api/index.ts +70 -0
  15. package/src/api/inspect.ts +96 -0
  16. package/src/api/registry.ts +128 -0
  17. package/src/api/types.ts +210 -0
  18. package/src/compiler/compiler.ts +546 -0
  19. package/src/compiler/index.ts +2 -0
  20. package/src/compiler/prototype-context-adapter.ts +99 -0
  21. package/src/compiler/types.ts +24 -0
  22. package/src/index.ts +76 -0
  23. package/src/interpreter/README.md +78 -0
  24. package/src/interpreter/interpreter.ts +463 -0
  25. package/src/interpreter/types.ts +108 -0
  26. package/src/lexer/char-tables.ts +37 -0
  27. package/src/lexer/errors.ts +31 -0
  28. package/src/lexer/index.ts +5 -0
  29. package/src/lexer/lexer.ts +745 -0
  30. package/src/lexer/token.ts +104 -0
  31. package/src/parser/ast.ts +123 -0
  32. package/src/parser/index.ts +3 -0
  33. package/src/parser/parser.ts +701 -0
  34. package/src/parser/pprint.ts +169 -0
  35. package/src/registry/default-analyzers.ts +257 -0
  36. package/src/registry/default-compilers.ts +31 -0
  37. package/src/registry/index.ts +94 -0
  38. package/src/registry/operations/arithmetic.ts +506 -0
  39. package/src/registry/operations/collection.ts +425 -0
  40. package/src/registry/operations/comparison.ts +432 -0
  41. package/src/registry/operations/existence.ts +703 -0
  42. package/src/registry/operations/filtering.ts +358 -0
  43. package/src/registry/operations/literals.ts +341 -0
  44. package/src/registry/operations/logical.ts +439 -0
  45. package/src/registry/operations/math.ts +128 -0
  46. package/src/registry/operations/membership.ts +132 -0
  47. package/src/registry/operations/string.ts +507 -0
  48. package/src/registry/operations/subsetting.ts +174 -0
  49. package/src/registry/operations/type-checking.ts +162 -0
  50. package/src/registry/operations/type-conversion.ts +404 -0
  51. package/src/registry/operations/type-operators.ts +308 -0
  52. package/src/registry/operations/utility.ts +644 -0
  53. package/src/registry/registry.ts +146 -0
  54. package/src/registry/types.ts +161 -0
  55. package/src/registry/utils/evaluation-helpers.ts +93 -0
  56. package/src/registry/utils/index.ts +3 -0
  57. package/src/registry/utils/type-system.ts +173 -0
  58. package/src/runtime/context.ts +158 -0
  59. package/src/runtime/debug-context.ts +135 -0
package/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # @atomic-ehr/fhirpath
2
+
3
+ A TypeScript implementation of FHIRPath, the path-based navigation and extraction language for FHIR (Fast Healthcare Interoperability Resources).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @atomic-ehr/fhirpath
9
+ # or
10
+ bun add @atomic-ehr/fhirpath
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import fhirpath from '@atomic-ehr/fhirpath';
17
+
18
+ const patient = {
19
+ name: [
20
+ { given: ['John', 'James'], family: 'Doe' },
21
+ { given: ['Johnny'], family: 'Doe' }
22
+ ],
23
+ birthDate: '1990-01-01'
24
+ };
25
+
26
+ // Simple evaluation
27
+ const givenNames = fhirpath.evaluate('name.given', patient);
28
+ console.log(givenNames); // ['John', 'James', 'Johnny']
29
+
30
+ // With filtering
31
+ const officialName = fhirpath.evaluate('name.where(use = \'official\').given', patient);
32
+
33
+ // Arithmetic
34
+ const age = fhirpath.evaluate('today().year() - birthDate.toDateTime().year()', patient);
35
+ ```
36
+
37
+ ## API Reference
38
+
39
+ ### Core Functions
40
+
41
+ #### `parse(expression: string): FHIRPathExpression`
42
+
43
+ Parses a FHIRPath expression string into an AST (Abstract Syntax Tree).
44
+
45
+ ```typescript
46
+ const expr = fhirpath.parse('Patient.name.given');
47
+ // Use the parsed expression multiple times
48
+ const result1 = fhirpath.evaluate(expr, patient1);
49
+ const result2 = fhirpath.evaluate(expr, patient2);
50
+ ```
51
+
52
+ #### `evaluate(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext): any[]`
53
+
54
+ Evaluates a FHIRPath expression against input data.
55
+
56
+ ```typescript
57
+ // Evaluate with string expression
58
+ const names = fhirpath.evaluate('name.family', patient);
59
+
60
+ // Evaluate with parsed expression
61
+ const expr = fhirpath.parse('name.family');
62
+ const names = fhirpath.evaluate(expr, patient);
63
+
64
+ // With context variables
65
+ const result = fhirpath.evaluate('%myVar + 5', null, {
66
+ variables: { myVar: 10 }
67
+ }); // [15]
68
+ ```
69
+
70
+ #### `compile(expression: string | FHIRPathExpression, options?: CompileOptions): CompiledExpression`
71
+
72
+ Compiles an expression into an optimized JavaScript function for better performance.
73
+
74
+ ```typescript
75
+ const compiled = fhirpath.compile('name.given');
76
+
77
+ // Use compiled function multiple times
78
+ const names1 = compiled(patient1);
79
+ const names2 = compiled(patient2);
80
+ ```
81
+
82
+ #### `analyze(expression: string | FHIRPathExpression, options?: AnalyzeOptions): AnalysisResult`
83
+
84
+ Performs static type analysis on an expression.
85
+
86
+ ```typescript
87
+ const analysis = fhirpath.analyze('name.given');
88
+ console.log(analysis.type); // Type information
89
+ console.log(analysis.errors); // Any type errors
90
+ ```
91
+
92
+ #### `inspect(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext, options?: InspectOptions): InspectResult`
93
+
94
+ Evaluates an expression while capturing rich debugging information including traces, execution time, and AST.
95
+
96
+ ```typescript
97
+ // Basic usage - capture trace output
98
+ const result = fhirpath.inspect(
99
+ 'name.trace("names").given.trace("given names")',
100
+ patient
101
+ );
102
+
103
+ console.log(result.result); // ['John', 'James', 'Johnny']
104
+ console.log(result.traces); // Array of trace entries
105
+ console.log(result.executionTime); // Time in milliseconds
106
+ console.log(result.ast); // Parsed AST
107
+
108
+ // Access trace information
109
+ result.traces.forEach(trace => {
110
+ console.log(`${trace.name}: ${JSON.stringify(trace.values)}`);
111
+ console.log(` at ${trace.timestamp}ms, depth: ${trace.depth}`);
112
+ });
113
+
114
+ // With options
115
+ const detailedResult = fhirpath.inspect(
116
+ 'Patient.name.where(use = "official")',
117
+ bundle,
118
+ undefined,
119
+ {
120
+ maxTraces: 100, // Limit number of traces collected
121
+ recordSteps: true // Future: enable step-by-step recording
122
+ }
123
+ );
124
+
125
+ // Error handling
126
+ const errorResult = fhirpath.inspect('invalid.expression()');
127
+ if (errorResult.errors) {
128
+ console.log('Errors:', errorResult.errors);
129
+ }
130
+ ```
131
+
132
+ The `InspectResult` contains:
133
+ - `result`: The evaluation result (same as `evaluate()`)
134
+ - `expression`: The original expression string
135
+ - `ast`: The parsed Abstract Syntax Tree
136
+ - `executionTime`: Total execution time in milliseconds
137
+ - `traces`: Array of trace entries from `trace()` calls
138
+ - `errors`: Any errors encountered during evaluation
139
+ - `warnings`: Any warnings (optional)
140
+ - `evaluationSteps`: Step-by-step evaluation details (when enabled)
141
+
142
+ ### Registry API
143
+
144
+ The registry provides introspection capabilities for available operations.
145
+
146
+ ```typescript
147
+ // List all available functions
148
+ const functions = fhirpath.registry.listFunctions();
149
+ console.log(functions.map(f => f.name)); // ['where', 'select', 'first', ...]
150
+
151
+ // Check if operation exists
152
+ fhirpath.registry.hasFunction('where'); // true
153
+ fhirpath.registry.hasOperator('+'); // true
154
+
155
+ // Get operation details
156
+ const whereInfo = fhirpath.registry.getOperationInfo('where');
157
+ console.log(whereInfo.syntax.notation); // "where(expression)"
158
+
159
+ // Validate custom function names
160
+ fhirpath.registry.canRegisterFunction('myFunc'); // true
161
+ fhirpath.registry.canRegisterFunction('where'); // false (built-in)
162
+ ```
163
+
164
+ ### Builder Pattern
165
+
166
+ For advanced configurations, use the builder pattern:
167
+
168
+ ```typescript
169
+ import { FHIRPath } from '@atomic-ehr/fhirpath';
170
+
171
+ const fp = FHIRPath.builder()
172
+ // Add custom functions
173
+ .withCustomFunction('double', (context, input) => {
174
+ return input.map(x => x * 2);
175
+ })
176
+
177
+ // Set default variables
178
+ .withVariable('defaultStatus', 'active')
179
+
180
+ // Add model provider for type information
181
+ .withModelProvider({
182
+ resolveType: (typeName) => { /* ... */ },
183
+ getTypeHierarchy: (typeName) => { /* ... */ },
184
+ getProperties: (typeName) => { /* ... */ }
185
+ })
186
+
187
+ .build();
188
+
189
+ // Use the configured instance
190
+ const result = fp.evaluate('value.double()', { value: [5] }); // [10]
191
+ const status = fp.evaluate('%defaultStatus'); // ['active']
192
+ ```
193
+
194
+ ### Custom Functions
195
+
196
+ Custom functions extend FHIRPath with domain-specific operations:
197
+
198
+ ```typescript
199
+ const fp = FHIRPath.builder()
200
+ .withCustomFunction('age', (context, input) => {
201
+ // Calculate age from birthDate
202
+ return input.map(birthDate => {
203
+ const today = new Date();
204
+ const birth = new Date(birthDate);
205
+ let age = today.getFullYear() - birth.getFullYear();
206
+ const monthDiff = today.getMonth() - birth.getMonth();
207
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
208
+ age--;
209
+ }
210
+ return age;
211
+ });
212
+ })
213
+ .withCustomFunction('fullName', (context, input) => {
214
+ return input.map(name => {
215
+ if (name && typeof name === 'object') {
216
+ const given = Array.isArray(name.given) ? name.given.join(' ') : '';
217
+ const family = name.family || '';
218
+ return `${given} ${family}`.trim();
219
+ }
220
+ return '';
221
+ });
222
+ })
223
+ .build();
224
+
225
+ // Use custom functions
226
+ const age = fp.evaluate('birthDate.age()', patient);
227
+ const fullNames = fp.evaluate('name.fullName()', patient);
228
+ ```
229
+
230
+ ### Error Handling
231
+
232
+ All API functions throw `FHIRPathError` for invalid expressions or runtime errors:
233
+
234
+ ```typescript
235
+ import { FHIRPathError, ErrorCode } from '@atomic-ehr/fhirpath';
236
+
237
+ try {
238
+ fhirpath.parse('invalid..expression');
239
+ } catch (error) {
240
+ if (error instanceof FHIRPathError) {
241
+ console.error(`Error: ${error.message}`);
242
+ console.error(`Code: ${error.code}`);
243
+ console.error(`Location: Line ${error.location?.line}, Column ${error.location?.column}`);
244
+ }
245
+ }
246
+ ```
247
+
248
+ ### Type Definitions
249
+
250
+ The library is fully typed with TypeScript:
251
+
252
+ ```typescript
253
+ import type {
254
+ FHIRPathExpression,
255
+ CompiledExpression,
256
+ EvaluationContext,
257
+ ModelProvider,
258
+ CustomFunction,
259
+ OperationInfo,
260
+ AnalysisResult,
261
+ InspectResult,
262
+ InspectOptions
263
+ } from '@atomic-ehr/fhirpath';
264
+ ```
265
+
266
+ ## Common Use Cases
267
+
268
+ ### Working with FHIR Resources
269
+
270
+ ```typescript
271
+ const bundle = {
272
+ resourceType: 'Bundle',
273
+ entry: [
274
+ { resource: { resourceType: 'Patient', id: '1', active: true } },
275
+ { resource: { resourceType: 'Patient', id: '2', active: false } },
276
+ { resource: { resourceType: 'Observation', status: 'final' } }
277
+ ]
278
+ };
279
+
280
+ // Get all patients
281
+ const patients = fhirpath.evaluate(
282
+ 'entry.resource.where(resourceType = \'Patient\')',
283
+ bundle
284
+ );
285
+
286
+ // Get active patients
287
+ const activePatients = fhirpath.evaluate(
288
+ 'entry.resource.where(resourceType = \'Patient\' and active = true)',
289
+ bundle
290
+ );
291
+
292
+ // Count resources by type
293
+ const patientCount = fhirpath.evaluate(
294
+ 'entry.resource.where(resourceType = \'Patient\').count()',
295
+ bundle
296
+ ); // [2]
297
+ ```
298
+
299
+ ### Complex Filtering
300
+
301
+ ```typescript
302
+ const observations = [
303
+ { code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 140 },
304
+ { code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 120 },
305
+ { code: { coding: [{ system: 'loinc', code: '5678' }] }, value: 98.6 }
306
+ ];
307
+
308
+ // Find high blood pressure readings
309
+ const highBP = fhirpath.evaluate(
310
+ 'where(code.coding.exists(system = \'loinc\' and code = \'1234\') and value > 130)',
311
+ observations
312
+ );
313
+ ```
314
+
315
+ ### Date Manipulation
316
+
317
+ ```typescript
318
+ const patient = {
319
+ birthDate: '1990-05-15'
320
+ };
321
+
322
+ // Check if patient is adult (>= 18 years)
323
+ const isAdult = fhirpath.evaluate(
324
+ 'today() - birthDate.toDateTime() >= 18 years',
325
+ patient
326
+ );
327
+ ```
328
+
329
+ ### Debugging Expressions
330
+
331
+ Use the `inspect()` function to debug complex FHIRPath expressions:
332
+
333
+ ```typescript
334
+ const bundle = {
335
+ entry: [
336
+ { resource: { resourceType: 'Patient', name: [{ given: ['John'] }] } },
337
+ { resource: { resourceType: 'Patient', name: [{ given: ['Jane'] }] } }
338
+ ]
339
+ };
340
+
341
+ // Debug a complex expression with traces
342
+ const result = fhirpath.inspect(
343
+ `entry.resource
344
+ .trace('all resources')
345
+ .where(resourceType = 'Patient')
346
+ .trace('patients only')
347
+ .name.given
348
+ .trace('all given names')`,
349
+ bundle
350
+ );
351
+
352
+ // Analyze the execution
353
+ console.log('Result:', result.result);
354
+ console.log('Execution time:', result.executionTime + 'ms');
355
+ console.log('\nTrace output:');
356
+ result.traces.forEach(trace => {
357
+ console.log(`- ${trace.name}: ${trace.values.length} items`);
358
+ });
359
+
360
+ // Output:
361
+ // Result: ['John', 'Jane']
362
+ // Execution time: 0.523ms
363
+ //
364
+ // Trace output:
365
+ // - all resources: 2 items
366
+ // - patients only: 2 items
367
+ // - all given names: 2 items
368
+ ```
369
+
370
+ ## Performance Tips
371
+
372
+ 1. **Parse Once, Evaluate Many**: Parse expressions once and reuse the parsed AST:
373
+ ```typescript
374
+ const expr = fhirpath.parse('name.given');
375
+ for (const patient of patients) {
376
+ const names = fhirpath.evaluate(expr, patient);
377
+ }
378
+ ```
379
+
380
+ 2. **Use Compiled Functions**: For expressions evaluated frequently, use compilation:
381
+ ```typescript
382
+ const getName = fhirpath.compile('name.given');
383
+ const results = patients.map(p => getName(p));
384
+ ```
385
+
386
+ 3. **Builder Instance**: Create a configured instance once and reuse:
387
+ ```typescript
388
+ const fp = FHIRPath.builder()
389
+ .withCustomFunction('myFunc', /* ... */)
390
+ .build();
391
+ // Use fp instance throughout your application
392
+ ```
393
+
394
+ ## Contributing
395
+
396
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.
397
+
398
+ ## License
399
+
400
+ MIT