Skip to content
 
 

Repository files navigation

@atomic-ehr/fhirpath

npm version

A TypeScript implementation of FHIRPath, the path-based navigation and extraction language for FHIR (Fast Healthcare Interoperability Resources).

Installation

npm install @atomic-ehr/fhirpath
# or
bun add @atomic-ehr/fhirpath

Quick Start

import fhirpath from '@atomic-ehr/fhirpath';

const patient = {
  name: [
    { given: ['John', 'James'], family: 'Doe' },
    { given: ['Johnny'], family: 'Doe' }
  ],
  birthDate: '1990-01-01'
};

// Simple evaluation (async)
const givenNames = await fhirpath.evaluate('name.given', patient);
console.log(givenNames); // ['John', 'James', 'Johnny']

// With filtering
const officialName = await fhirpath.evaluate('name.where(use = \'official\').given', patient);

// Arithmetic
const age = await fhirpath.evaluate('today().year() - birthDate.toDateTime().year()', patient);

API Reference

Important Note: As of version 2.0.0, all evaluation and analysis functions are async to support lazy loading of FHIR schemas and improved performance. Make sure to use await when calling evaluate(), compile(), analyze(), and inspect().

Core Functions

parse(expression: string, options?: ParserOptions): ParseResult

Parses a FHIRPath expression string into an AST (Abstract Syntax Tree) with optional parser features.

// Basic parsing (default - collects diagnostics)
const result = fhirpath.parse('Patient.name.given');
console.log(result.ast);         // The parsed AST
console.log(result.diagnostics); // Array of any syntax issues
console.log(result.hasErrors);   // Boolean indicating if there are errors

// Parse with error on first syntax error (fastest)
const ast = fhirpath.parseForEvaluation('Patient.name.given');
// Throws immediately on syntax errors - best for production use

// Parse with advanced features for development tools
const result = fhirpath.parse('Patient..name', {
  errorRecovery: true,  // Continue parsing after errors
  trackRanges: true     // Track source location for each AST node
});
console.log(result.isPartial);  // true - indicates incomplete AST due to errors
console.log(result.ranges);     // Map of AST nodes to source locations

Parser Options:

  • throwOnError?: boolean - Throw on first error instead of collecting diagnostics (performance mode)
  • trackRanges?: boolean - Enable source range tracking for each AST node (useful for IDEs)
  • errorRecovery?: boolean - Enable error recovery to continue parsing after errors (useful for IDEs)
  • maxErrors?: number - Maximum number of errors to collect before stopping

evaluate(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext): Promise<any[]>

Evaluates a FHIRPath expression against input data. Note: This function is now async.

// Evaluate with string expression
const names = await fhirpath.evaluate('name.family', patient);

// Evaluate with parsed expression
const expr = fhirpath.parse('name.family');
const names = await fhirpath.evaluate(expr, patient);

// With context variables
const result = await fhirpath.evaluate('%myVar + 5', null, {
  variables: { myVar: 10 }
}); // [15]

compile(expression: string | FHIRPathExpression, options?: CompileOptions): Promise<CompiledExpression>

Compiles an expression into an optimized JavaScript function for better performance. Note: Both compilation and execution are now async.

const compiled = await fhirpath.compile('name.given');

// Use compiled function multiple times (async)
const names1 = await compiled(patient1);
const names2 = await compiled(patient2);

analyze(expression: string, options?: AnalyzeOptions): Promise<AnalysisResult>

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.

// Basic analysis
const analysis = await fhirpath.analyze('name.given');
console.log(analysis.diagnostics); // Array of any issues found
console.log(analysis.ast); // The analyzed AST with type information

// With FHIR model provider for type checking
import { FHIRModelProvider } from '@atomic-ehr/fhirpath';

const modelProvider = new FHIRModelProvider({
  packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
});
await modelProvider.initialize();

const analysis = await fhirpath.analyze('Patient.birthDate.substring(0, 4)', {
  modelProvider
});
// Will report type error: substring() expects String but birthDate is date

// With error recovery for IDE/tooling scenarios
const analysis = await fhirpath.analyze('Patient.name.', {
  errorRecovery: true,  // Won't throw on syntax errors
  modelProvider
});
console.log(analysis.diagnostics); // Contains parse error: "Expected identifier after '.'"
console.log(analysis.ast); // Partial AST with error nodes

// With input type context
const analysis = await fhirpath.analyze('name.given', {
  modelProvider,
  inputType: { 
    type: 'HumanName',
    namespace: 'FHIR',
    singleton: false 
  }
});

Analyze Options:

  • modelProvider?: ModelProvider - Provides type information for FHIR resources and data types
  • errorRecovery?: boolean - Enable error tolerance for broken expressions (useful for IDEs)
  • variables?: Record<string, unknown> - Variables available in the expression context
  • inputType?: TypeInfo - Type information for the input context

inspect(expression: string | FHIRPathExpression, input?: any, context?: EvaluationContext, options?: InspectOptions): Promise<InspectResult>

Evaluates an expression while capturing rich debugging information including traces, execution time, and AST. Note: This function is now async.

// Basic usage - capture trace output
const result = await fhirpath.inspect(
  'name.trace("names").given.trace("given names")',
  patient
);

console.log(result.result);        // ['John', 'James', 'Johnny']
console.log(result.traces);        // Array of trace entries
console.log(result.executionTime); // Time in milliseconds
console.log(result.ast);           // Parsed AST

// Access trace information
result.traces.forEach(trace => {
  console.log(`${trace.name}: ${JSON.stringify(trace.values)}`);
  console.log(`  at ${trace.timestamp}ms, depth: ${trace.depth}`);
});

// With options
const detailedResult = await fhirpath.inspect(
  'Patient.name.where(use = "official")',
  bundle,
  undefined,
  { 
    maxTraces: 100,      // Limit number of traces collected
    recordSteps: true    // Future: enable step-by-step recording
  }
);

// Error handling
const errorResult = await fhirpath.inspect('invalid.expression()');
if (errorResult.errors) {
  console.log('Errors:', errorResult.errors);
}

The InspectResult contains:

  • result: The evaluation result (same as evaluate())
  • expression: The original expression string
  • ast: The parsed Abstract Syntax Tree
  • executionTime: Total execution time in milliseconds
  • traces: Array of trace entries from trace() calls
  • errors: Any errors encountered during evaluation
  • warnings: Any warnings (optional)
  • evaluationSteps: Step-by-step evaluation details (when enabled)

Convenience Functions

parseForEvaluation(expression: string): ASTNode

Parses an expression and returns just the AST, throwing on any syntax errors. This is the most performant option for production use.

try {
  const ast = fhirpath.parseForEvaluation('Patient.name.given');
  // Use ast for evaluation
} catch (error) {
  console.error('Parse error:', error.message);
}

validate(expression: string): { valid: boolean; diagnostics: ParseDiagnostic[] }

Validates a FHIRPath expression syntax without throwing errors.

const validation = fhirpath.validate('Patient..name');
if (!validation.valid) {
  console.log('Syntax errors:', validation.diagnostics);
}

Registry API

The registry provides introspection capabilities for available operations.

// List all available functions
const functions = fhirpath.registry.listFunctions();
console.log(functions.map(f => f.name)); // ['where', 'select', 'first', ...]

// Check if operation exists
fhirpath.registry.hasFunction('where'); // true
fhirpath.registry.hasOperator('+'); // true

// Get operation details
const whereInfo = fhirpath.registry.getOperationInfo('where');
console.log(whereInfo.syntax.notation); // "where(expression)"

// Validate custom function names
fhirpath.registry.canRegisterFunction('myFunc'); // true
fhirpath.registry.canRegisterFunction('where'); // false (built-in)

FHIR Model Provider

The FHIR Model Provider enables advanced type checking and validation for FHIR resources:

import { FHIRModelProvider, analyze } from '@atomic-ehr/fhirpath';

// Create and initialize the model provider
const modelProvider = new FHIRModelProvider({
  packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
});

// Initialize before use (loads FHIR type definitions)
await modelProvider.initialize();

// Use with analyze for type checking (async)
const result = await analyze('Patient.active.substring(0, 1)', { modelProvider });
// Diagnostics: "Type mismatch: function 'substring' expects input type String but got Boolean"

// Type-aware property navigation
const result2 = await analyze('Patient.birthDate.year()', { modelProvider });
// Works correctly - birthDate is recognized as a date type

// Detect invalid property access
const result3 = await analyze('Patient.invalidProperty', { modelProvider });
// Diagnostics: "Unknown property 'invalidProperty' on type FHIR.Patient"

// Works with complex paths
const result4 = await analyze(
  'Bundle.entry.resource.where(resourceType = "Patient").name.given',
  { modelProvider }
);
// Validates entire path with proper type information

The model provider supports:

  • Property validation and type checking
  • Polymorphic type resolution
  • Choice type handling (e.g., value[x])
  • Extension navigation
  • Full FHIR R4 type system

Builder Pattern

For advanced configurations, use the builder pattern:

import { FHIRPath } from '@atomic-ehr/fhirpath';

const fp = FHIRPath.builder()
  // Add custom functions
  .withCustomFunction('double', async (context, input) => {
    return input.map(x => x * 2);
  })
  
  // Set default variables
  .withVariable('defaultStatus', 'active')
  
  // Add model provider for type information
  .withModelProvider({
    getType: async (typeName) => { /* ... */ },
    getElementType: async (parentType, propertyName) => { /* ... */ },
    ofType: (type, typeName) => { /* ... */ },
    getElementNames: (parentType) => { /* ... */ },
    getChildrenType: async (parentType) => { /* ... */ }
  })
  
  .build();

// Use the configured instance (async)
const result = await fp.evaluate('value.double()', { value: [5] }); // [10]
const status = await fp.evaluate('%defaultStatus'); // ['active']

Custom Functions

Custom functions extend FHIRPath with domain-specific operations:

const fp = FHIRPath.builder()
  .withCustomFunction('age', async (context, input) => {
    // Calculate age from birthDate
    return input.map(birthDate => {
      const today = new Date();
      const birth = new Date(birthDate);
      let age = today.getFullYear() - birth.getFullYear();
      const monthDiff = today.getMonth() - birth.getMonth();
      if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
        age--;
      }
      return age;
    });
  })
  .withCustomFunction('fullName', async (context, input) => {
    return input.map(name => {
      if (name && typeof name === 'object') {
        const given = Array.isArray(name.given) ? name.given.join(' ') : '';
        const family = name.family || '';
        return `${given} ${family}`.trim();
      }
      return '';
    });
  })
  .build();

// Use custom functions (async)
const age = await fp.evaluate('birthDate.age()', patient);
const fullNames = await fp.evaluate('name.fullName()', patient);

Error Handling

All API functions throw FHIRPathError for invalid expressions or runtime errors:

import { FHIRPathError, ErrorCode } from '@atomic-ehr/fhirpath';

try {
  fhirpath.parse('invalid..expression');
} catch (error) {
  if (error instanceof FHIRPathError) {
    console.error(`Error: ${error.message}`);
    console.error(`Code: ${error.code}`);
    console.error(`Location: Line ${error.location?.line}, Column ${error.location?.column}`);
  }
}

Type Definitions

The library is fully typed with TypeScript:

import type {
  FHIRPathExpression,
  CompiledExpression,
  EvaluationContext,
  ModelProvider,
  CustomFunction,
  OperationInfo,
  AnalysisResult,
  InspectResult,
  InspectOptions,
  // Parser types
  ParserOptions,
  ParseResult,
  ParseDiagnostic,
  DiagnosticSeverity,
  TextRange,
  ASTNode,
  // Analyzer types
  AnalyzeOptions,
  Diagnostic,
  TypeInfo,
  // FHIR Model Provider
  FHIRModelProvider,
  FHIRModelProviderConfig
} from '@atomic-ehr/fhirpath';

IDE Integration and Error Recovery

The analyzer supports error recovery mode for IDE and tooling scenarios, allowing analysis of incomplete or syntactically incorrect expressions:

// Normal mode - throws on syntax errors
try {
  const result = await fhirpath.analyze('Patient.name.');
} catch (error) {
  console.error('Syntax error:', error.message);
}

// Error recovery mode - continues analysis despite errors
const result = await fhirpath.analyze('Patient.name.', {
  errorRecovery: true
});

// Check diagnostics instead of catching errors
console.log(result.diagnostics);
// [{ 
//   message: "Expected identifier after '.', got: EOF",
//   severity: DiagnosticSeverity.Error,
//   range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }
// }]

// The AST contains error nodes but analysis continues
console.log(result.ast); // Partial AST with error nodes

// Complex broken expression - multiple errors reported
const result2 = await fhirpath.analyze('Patient.name.where(use = ).given.', {
  errorRecovery: true,
  modelProvider
});
console.log(result2.diagnostics.length); // 2 errors

// Type information is preserved for valid parts
const result3 = await fhirpath.analyze('5 + 3 * ', {
  errorRecovery: true
});
// Even though expression is incomplete, literals 5 and 3 have type info

This is particularly useful for:

  • Language servers: Provide diagnostics while users type
  • IDE plugins: Show errors inline without breaking analysis
  • Code completion: Analyze partial expressions for context
  • Linting tools: Report all issues in a single pass

Common Use Cases

Working with FHIR Resources

const bundle = {
  resourceType: 'Bundle',
  entry: [
    { resource: { resourceType: 'Patient', id: '1', active: true } },
    { resource: { resourceType: 'Patient', id: '2', active: false } },
    { resource: { resourceType: 'Observation', status: 'final' } }
  ]
};

// Get all patients
const patients = await fhirpath.evaluate(
  'entry.resource.where(resourceType = \'Patient\')',
  bundle
);

// Get active patients
const activePatients = await fhirpath.evaluate(
  'entry.resource.where(resourceType = \'Patient\' and active = true)',
  bundle
);

// Count resources by type
const patientCount = await fhirpath.evaluate(
  'entry.resource.where(resourceType = \'Patient\').count()',
  bundle
); // [2]

Complex Filtering

const observations = [
  { code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 140 },
  { code: { coding: [{ system: 'loinc', code: '1234' }] }, value: 120 },
  { code: { coding: [{ system: 'loinc', code: '5678' }] }, value: 98.6 }
];

// Find high blood pressure readings
const highBP = await fhirpath.evaluate(
  'where(code.coding.exists(system = \'loinc\' and code = \'1234\') and value > 130)',
  observations
);

Date Manipulation

const patient = {
  birthDate: '1990-05-15'
};

// Check if patient is adult (>= 18 years)
const isAdult = await fhirpath.evaluate(
  'today() - birthDate.toDateTime() >= 18 years',
  patient
);

Debugging Expressions

Use the inspect() function to debug complex FHIRPath expressions:

const bundle = {
  entry: [
    { resource: { resourceType: 'Patient', name: [{ given: ['John'] }] } },
    { resource: { resourceType: 'Patient', name: [{ given: ['Jane'] }] } }
  ]
};

// Debug a complex expression with traces
const result = await fhirpath.inspect(
  `entry.resource
    .trace('all resources')
    .where(resourceType = 'Patient')
    .trace('patients only')
    .name.given
    .trace('all given names')`,
  bundle
);

// Analyze the execution
console.log('Result:', result.result);
console.log('Execution time:', result.executionTime + 'ms');
console.log('\nTrace output:');
result.traces.forEach(trace => {
  console.log(`- ${trace.name}: ${trace.values.length} items`);
});

// Output:
// Result: ['John', 'Jane']
// Execution time: 0.523ms
// 
// Trace output:
// - all resources: 2 items
// - patients only: 2 items
// - all given names: 2 items

Performance Tips

  1. Use Fast Parsing for Production: When parsing expressions in production, use parseForEvaluation() or enable throwOnError:

    // Fastest - throws on first error
    const ast = fhirpath.parseForEvaluation('name.given');
    
    // Or use throwOnError option
    const result = fhirpath.parse('name.given', { throwOnError: true });
  2. Parse Once, Evaluate Many: Parse expressions once and reuse the parsed AST:

    const expr = fhirpath.parse('name.given');
    for (const patient of patients) {
      const names = await fhirpath.evaluate(expr, patient);
    }
  3. Use Compiled Functions: For expressions evaluated frequently, use compilation:

    const getName = await fhirpath.compile('name.given');
    const results = await Promise.all(patients.map(p => getName(p)));
  4. Disable Unnecessary Parser Features: Only enable parser features you need:

    // For development tools (IDEs, linters)
    const result = fhirpath.parse(expr, {
      errorRecovery: true,  // Only if you need partial ASTs
      trackRanges: true     // Only if you need source mapping
    });
    
    // For production (fastest)
    const ast = fhirpath.parseForEvaluation(expr);
  5. Builder Instance: Create a configured instance once and reuse:

    const fp = FHIRPath.builder()
      .withCustomFunction('myFunc', /* ... */)
      .build();
    // Use fp instance throughout your application

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages