@atomic-ehr/fhirpath 0.0.1-canary.0c6931e.20250727185306

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 (85) hide show
  1. package/README.md +473 -0
  2. package/dist/index.d.ts +462 -0
  3. package/dist/index.js +10307 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +58 -0
  6. package/src/analyzer/analyzer.ts +499 -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 +157 -0
  12. package/src/api/errors.ts +145 -0
  13. package/src/api/expression.ts +156 -0
  14. package/src/api/index.ts +122 -0
  15. package/src/api/inspect.ts +99 -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 +107 -0
  23. package/src/interpreter/README.md +78 -0
  24. package/src/interpreter/interpreter.ts +475 -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/lexer2/index.md +232 -0
  32. package/src/lexer2/index.perf.test.ts +68 -0
  33. package/src/lexer2/index.test.ts +549 -0
  34. package/src/lexer2/index.ts +1251 -0
  35. package/src/lexer2/notes.md +173 -0
  36. package/src/lexer2/optimization-summary.md +718 -0
  37. package/src/parser/ast-factory.ts +220 -0
  38. package/src/parser/ast.ts +144 -0
  39. package/src/parser/collection-parser.ts +89 -0
  40. package/src/parser/diagnostic-messages.ts +216 -0
  41. package/src/parser/diagnostics.ts +85 -0
  42. package/src/parser/error-reporter.ts +230 -0
  43. package/src/parser/index.ts +3 -0
  44. package/src/parser/literal-parser.ts +103 -0
  45. package/src/parser/parse-error.ts +16 -0
  46. package/src/parser/parser-error-factory.ts +141 -0
  47. package/src/parser/parser-state.ts +134 -0
  48. package/src/parser/parser.ts +1272 -0
  49. package/src/parser/pprint.ts +169 -0
  50. package/src/parser/precedence-manager.ts +64 -0
  51. package/src/parser/source-mapper.ts +248 -0
  52. package/src/parser/special-constructs.ts +142 -0
  53. package/src/parser/token-navigator.ts +110 -0
  54. package/src/parser/types.ts +60 -0
  55. package/src/parser2/index.md +177 -0
  56. package/src/parser2/index.perf.test.ts +184 -0
  57. package/src/parser2/index.test.ts +305 -0
  58. package/src/parser2/index.ts +578 -0
  59. package/src/parser2/optimization-summary.md +176 -0
  60. package/src/registry/default-analyzers.ts +257 -0
  61. package/src/registry/default-compilers.ts +31 -0
  62. package/src/registry/index.ts +96 -0
  63. package/src/registry/operations/arithmetic.ts +506 -0
  64. package/src/registry/operations/collection.ts +425 -0
  65. package/src/registry/operations/comparison.ts +432 -0
  66. package/src/registry/operations/existence.ts +703 -0
  67. package/src/registry/operations/filtering.ts +358 -0
  68. package/src/registry/operations/literals.ts +341 -0
  69. package/src/registry/operations/logical.ts +439 -0
  70. package/src/registry/operations/math.ts +128 -0
  71. package/src/registry/operations/membership.ts +132 -0
  72. package/src/registry/operations/navigation.ts +52 -0
  73. package/src/registry/operations/string.ts +507 -0
  74. package/src/registry/operations/subsetting.ts +174 -0
  75. package/src/registry/operations/type-checking.ts +162 -0
  76. package/src/registry/operations/type-conversion.ts +404 -0
  77. package/src/registry/operations/type-operators.ts +308 -0
  78. package/src/registry/operations/utility.ts +644 -0
  79. package/src/registry/registry.ts +146 -0
  80. package/src/registry/types.ts +161 -0
  81. package/src/registry/utils/evaluation-helpers.ts +93 -0
  82. package/src/registry/utils/index.ts +3 -0
  83. package/src/registry/utils/type-system.ts +173 -0
  84. package/src/runtime/context.ts +158 -0
  85. package/src/runtime/debug-context.ts +135 -0
package/README.md ADDED
@@ -0,0 +1,473 @@
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, options?: ParserOptions): ParseResult`
42
+
43
+ Parses a FHIRPath expression string into an AST (Abstract Syntax Tree) with optional parser features.
44
+
45
+ ```typescript
46
+ // Basic parsing (default - collects diagnostics)
47
+ const result = fhirpath.parse('Patient.name.given');
48
+ console.log(result.ast); // The parsed AST
49
+ console.log(result.diagnostics); // Array of any syntax issues
50
+ console.log(result.hasErrors); // Boolean indicating if there are errors
51
+
52
+ // Parse with error on first syntax error (fastest)
53
+ const ast = fhirpath.parseForEvaluation('Patient.name.given');
54
+ // Throws immediately on syntax errors - best for production use
55
+
56
+ // Parse with advanced features for development tools
57
+ const result = fhirpath.parse('Patient..name', {
58
+ errorRecovery: true, // Continue parsing after errors
59
+ trackRanges: true // Track source location for each AST node
60
+ });
61
+ console.log(result.isPartial); // true - indicates incomplete AST due to errors
62
+ console.log(result.ranges); // Map of AST nodes to source locations
63
+ ```
64
+
65
+ **Parser Options:**
66
+ - `throwOnError?: boolean` - Throw on first error instead of collecting diagnostics (performance mode)
67
+ - `trackRanges?: boolean` - Enable source range tracking for each AST node (useful for IDEs)
68
+ - `errorRecovery?: boolean` - Enable error recovery to continue parsing after errors (useful for IDEs)
69
+ - `maxErrors?: number` - Maximum number of errors to collect before stopping
70
+
71
+ #### `evaluate(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext): any[]`
72
+
73
+ Evaluates a FHIRPath expression against input data.
74
+
75
+ ```typescript
76
+ // Evaluate with string expression
77
+ const names = fhirpath.evaluate('name.family', patient);
78
+
79
+ // Evaluate with parsed expression
80
+ const expr = fhirpath.parse('name.family');
81
+ const names = fhirpath.evaluate(expr, patient);
82
+
83
+ // With context variables
84
+ const result = fhirpath.evaluate('%myVar + 5', null, {
85
+ variables: { myVar: 10 }
86
+ }); // [15]
87
+ ```
88
+
89
+ #### `compile(expression: string | FHIRPathExpression, options?: CompileOptions): CompiledExpression`
90
+
91
+ Compiles an expression into an optimized JavaScript function for better performance.
92
+
93
+ ```typescript
94
+ const compiled = fhirpath.compile('name.given');
95
+
96
+ // Use compiled function multiple times
97
+ const names1 = compiled(patient1);
98
+ const names2 = compiled(patient2);
99
+ ```
100
+
101
+ #### `analyze(expression: string | FHIRPathExpression, options?: AnalyzeOptions): AnalysisResult`
102
+
103
+ Performs static type analysis on an expression.
104
+
105
+ ```typescript
106
+ const analysis = fhirpath.analyze('name.given');
107
+ console.log(analysis.type); // Type information
108
+ console.log(analysis.errors); // Any type errors
109
+ ```
110
+
111
+ #### `inspect(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext, options?: InspectOptions): InspectResult`
112
+
113
+ Evaluates an expression while capturing rich debugging information including traces, execution time, and AST.
114
+
115
+ ```typescript
116
+ // Basic usage - capture trace output
117
+ const result = fhirpath.inspect(
118
+ 'name.trace("names").given.trace("given names")',
119
+ patient
120
+ );
121
+
122
+ console.log(result.result); // ['John', 'James', 'Johnny']
123
+ console.log(result.traces); // Array of trace entries
124
+ console.log(result.executionTime); // Time in milliseconds
125
+ console.log(result.ast); // Parsed AST
126
+
127
+ // Access trace information
128
+ result.traces.forEach(trace => {
129
+ console.log(`${trace.name}: ${JSON.stringify(trace.values)}`);
130
+ console.log(` at ${trace.timestamp}ms, depth: ${trace.depth}`);
131
+ });
132
+
133
+ // With options
134
+ const detailedResult = fhirpath.inspect(
135
+ 'Patient.name.where(use = "official")',
136
+ bundle,
137
+ undefined,
138
+ {
139
+ maxTraces: 100, // Limit number of traces collected
140
+ recordSteps: true // Future: enable step-by-step recording
141
+ }
142
+ );
143
+
144
+ // Error handling
145
+ const errorResult = fhirpath.inspect('invalid.expression()');
146
+ if (errorResult.errors) {
147
+ console.log('Errors:', errorResult.errors);
148
+ }
149
+ ```
150
+
151
+ The `InspectResult` contains:
152
+ - `result`: The evaluation result (same as `evaluate()`)
153
+ - `expression`: The original expression string
154
+ - `ast`: The parsed Abstract Syntax Tree
155
+ - `executionTime`: Total execution time in milliseconds
156
+ - `traces`: Array of trace entries from `trace()` calls
157
+ - `errors`: Any errors encountered during evaluation
158
+ - `warnings`: Any warnings (optional)
159
+ - `evaluationSteps`: Step-by-step evaluation details (when enabled)
160
+
161
+ ### Convenience Functions
162
+
163
+ #### `parseForEvaluation(expression: string): ASTNode`
164
+
165
+ Parses an expression and returns just the AST, throwing on any syntax errors. This is the most performant option for production use.
166
+
167
+ ```typescript
168
+ try {
169
+ const ast = fhirpath.parseForEvaluation('Patient.name.given');
170
+ // Use ast for evaluation
171
+ } catch (error) {
172
+ console.error('Parse error:', error.message);
173
+ }
174
+ ```
175
+
176
+ #### `validate(expression: string): { valid: boolean; diagnostics: ParseDiagnostic[] }`
177
+
178
+ Validates a FHIRPath expression syntax without throwing errors.
179
+
180
+ ```typescript
181
+ const validation = fhirpath.validate('Patient..name');
182
+ if (!validation.valid) {
183
+ console.log('Syntax errors:', validation.diagnostics);
184
+ }
185
+ ```
186
+
187
+ ### Registry API
188
+
189
+ The registry provides introspection capabilities for available operations.
190
+
191
+ ```typescript
192
+ // List all available functions
193
+ const functions = fhirpath.registry.listFunctions();
194
+ console.log(functions.map(f => f.name)); // ['where', 'select', 'first', ...]
195
+
196
+ // Check if operation exists
197
+ fhirpath.registry.hasFunction('where'); // true
198
+ fhirpath.registry.hasOperator('+'); // true
199
+
200
+ // Get operation details
201
+ const whereInfo = fhirpath.registry.getOperationInfo('where');
202
+ console.log(whereInfo.syntax.notation); // "where(expression)"
203
+
204
+ // Validate custom function names
205
+ fhirpath.registry.canRegisterFunction('myFunc'); // true
206
+ fhirpath.registry.canRegisterFunction('where'); // false (built-in)
207
+ ```
208
+
209
+ ### Builder Pattern
210
+
211
+ For advanced configurations, use the builder pattern:
212
+
213
+ ```typescript
214
+ import { FHIRPath } from '@atomic-ehr/fhirpath';
215
+
216
+ const fp = FHIRPath.builder()
217
+ // Add custom functions
218
+ .withCustomFunction('double', (context, input) => {
219
+ return input.map(x => x * 2);
220
+ })
221
+
222
+ // Set default variables
223
+ .withVariable('defaultStatus', 'active')
224
+
225
+ // Add model provider for type information
226
+ .withModelProvider({
227
+ resolveType: (typeName) => { /* ... */ },
228
+ getTypeHierarchy: (typeName) => { /* ... */ },
229
+ getProperties: (typeName) => { /* ... */ }
230
+ })
231
+
232
+ .build();
233
+
234
+ // Use the configured instance
235
+ const result = fp.evaluate('value.double()', { value: [5] }); // [10]
236
+ const status = fp.evaluate('%defaultStatus'); // ['active']
237
+ ```
238
+
239
+ ### Custom Functions
240
+
241
+ Custom functions extend FHIRPath with domain-specific operations:
242
+
243
+ ```typescript
244
+ const fp = FHIRPath.builder()
245
+ .withCustomFunction('age', (context, input) => {
246
+ // Calculate age from birthDate
247
+ return input.map(birthDate => {
248
+ const today = new Date();
249
+ const birth = new Date(birthDate);
250
+ let age = today.getFullYear() - birth.getFullYear();
251
+ const monthDiff = today.getMonth() - birth.getMonth();
252
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
253
+ age--;
254
+ }
255
+ return age;
256
+ });
257
+ })
258
+ .withCustomFunction('fullName', (context, input) => {
259
+ return input.map(name => {
260
+ if (name && typeof name === 'object') {
261
+ const given = Array.isArray(name.given) ? name.given.join(' ') : '';
262
+ const family = name.family || '';
263
+ return `${given} ${family}`.trim();
264
+ }
265
+ return '';
266
+ });
267
+ })
268
+ .build();
269
+
270
+ // Use custom functions
271
+ const age = fp.evaluate('birthDate.age()', patient);
272
+ const fullNames = fp.evaluate('name.fullName()', patient);
273
+ ```
274
+
275
+ ### Error Handling
276
+
277
+ All API functions throw `FHIRPathError` for invalid expressions or runtime errors:
278
+
279
+ ```typescript
280
+ import { FHIRPathError, ErrorCode } from '@atomic-ehr/fhirpath';
281
+
282
+ try {
283
+ fhirpath.parse('invalid..expression');
284
+ } catch (error) {
285
+ if (error instanceof FHIRPathError) {
286
+ console.error(`Error: ${error.message}`);
287
+ console.error(`Code: ${error.code}`);
288
+ console.error(`Location: Line ${error.location?.line}, Column ${error.location?.column}`);
289
+ }
290
+ }
291
+ ```
292
+
293
+ ### Type Definitions
294
+
295
+ The library is fully typed with TypeScript:
296
+
297
+ ```typescript
298
+ import type {
299
+ FHIRPathExpression,
300
+ CompiledExpression,
301
+ EvaluationContext,
302
+ ModelProvider,
303
+ CustomFunction,
304
+ OperationInfo,
305
+ AnalysisResult,
306
+ InspectResult,
307
+ InspectOptions,
308
+ // Parser types
309
+ ParserOptions,
310
+ ParseResult,
311
+ ParseDiagnostic,
312
+ DiagnosticSeverity,
313
+ TextRange,
314
+ ASTNode
315
+ } from '@atomic-ehr/fhirpath';
316
+ ```
317
+
318
+ ## Common Use Cases
319
+
320
+ ### Working with FHIR Resources
321
+
322
+ ```typescript
323
+ const bundle = {
324
+ resourceType: 'Bundle',
325
+ entry: [
326
+ { resource: { resourceType: 'Patient', id: '1', active: true } },
327
+ { resource: { resourceType: 'Patient', id: '2', active: false } },
328
+ { resource: { resourceType: 'Observation', status: 'final' } }
329
+ ]
330
+ };
331
+
332
+ // Get all patients
333
+ const patients = fhirpath.evaluate(
334
+ 'entry.resource.where(resourceType = \'Patient\')',
335
+ bundle
336
+ );
337
+
338
+ // Get active patients
339
+ const activePatients = fhirpath.evaluate(
340
+ 'entry.resource.where(resourceType = \'Patient\' and active = true)',
341
+ bundle
342
+ );
343
+
344
+ // Count resources by type
345
+ const patientCount = fhirpath.evaluate(
346
+ 'entry.resource.where(resourceType = \'Patient\').count()',
347
+ bundle
348
+ ); // [2]
349
+ ```
350
+
351
+ ### Complex Filtering
352
+
353
+ ```typescript
354
+ const observations = [
355
+ { code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 140 },
356
+ { code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 120 },
357
+ { code: { coding: [{ system: 'loinc', code: '5678' }] }, value: 98.6 }
358
+ ];
359
+
360
+ // Find high blood pressure readings
361
+ const highBP = fhirpath.evaluate(
362
+ 'where(code.coding.exists(system = \'loinc\' and code = \'1234\') and value > 130)',
363
+ observations
364
+ );
365
+ ```
366
+
367
+ ### Date Manipulation
368
+
369
+ ```typescript
370
+ const patient = {
371
+ birthDate: '1990-05-15'
372
+ };
373
+
374
+ // Check if patient is adult (>= 18 years)
375
+ const isAdult = fhirpath.evaluate(
376
+ 'today() - birthDate.toDateTime() >= 18 years',
377
+ patient
378
+ );
379
+ ```
380
+
381
+ ### Debugging Expressions
382
+
383
+ Use the `inspect()` function to debug complex FHIRPath expressions:
384
+
385
+ ```typescript
386
+ const bundle = {
387
+ entry: [
388
+ { resource: { resourceType: 'Patient', name: [{ given: ['John'] }] } },
389
+ { resource: { resourceType: 'Patient', name: [{ given: ['Jane'] }] } }
390
+ ]
391
+ };
392
+
393
+ // Debug a complex expression with traces
394
+ const result = fhirpath.inspect(
395
+ `entry.resource
396
+ .trace('all resources')
397
+ .where(resourceType = 'Patient')
398
+ .trace('patients only')
399
+ .name.given
400
+ .trace('all given names')`,
401
+ bundle
402
+ );
403
+
404
+ // Analyze the execution
405
+ console.log('Result:', result.result);
406
+ console.log('Execution time:', result.executionTime + 'ms');
407
+ console.log('\nTrace output:');
408
+ result.traces.forEach(trace => {
409
+ console.log(`- ${trace.name}: ${trace.values.length} items`);
410
+ });
411
+
412
+ // Output:
413
+ // Result: ['John', 'Jane']
414
+ // Execution time: 0.523ms
415
+ //
416
+ // Trace output:
417
+ // - all resources: 2 items
418
+ // - patients only: 2 items
419
+ // - all given names: 2 items
420
+ ```
421
+
422
+ ## Performance Tips
423
+
424
+ 1. **Use Fast Parsing for Production**: When parsing expressions in production, use `parseForEvaluation()` or enable `throwOnError`:
425
+ ```typescript
426
+ // Fastest - throws on first error
427
+ const ast = fhirpath.parseForEvaluation('name.given');
428
+
429
+ // Or use throwOnError option
430
+ const result = fhirpath.parse('name.given', { throwOnError: true });
431
+ ```
432
+
433
+ 2. **Parse Once, Evaluate Many**: Parse expressions once and reuse the parsed AST:
434
+ ```typescript
435
+ const expr = fhirpath.parse('name.given');
436
+ for (const patient of patients) {
437
+ const names = fhirpath.evaluate(expr, patient);
438
+ }
439
+ ```
440
+
441
+ 3. **Use Compiled Functions**: For expressions evaluated frequently, use compilation:
442
+ ```typescript
443
+ const getName = fhirpath.compile('name.given');
444
+ const results = patients.map(p => getName(p));
445
+ ```
446
+
447
+ 4. **Disable Unnecessary Parser Features**: Only enable parser features you need:
448
+ ```typescript
449
+ // For development tools (IDEs, linters)
450
+ const result = fhirpath.parse(expr, {
451
+ errorRecovery: true, // Only if you need partial ASTs
452
+ trackRanges: true // Only if you need source mapping
453
+ });
454
+
455
+ // For production (fastest)
456
+ const ast = fhirpath.parseForEvaluation(expr);
457
+ ```
458
+
459
+ 5. **Builder Instance**: Create a configured instance once and reuse:
460
+ ```typescript
461
+ const fp = FHIRPath.builder()
462
+ .withCustomFunction('myFunc', /* ... */)
463
+ .build();
464
+ // Use fp instance throughout your application
465
+ ```
466
+
467
+ ## Contributing
468
+
469
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.
470
+
471
+ ## License
472
+
473
+ MIT