Skip to main content
fi-fhir docs

Overview

TypeScript SDK Planning

This document describes the TypeScript/JavaScript SDK for fi-fhir.

Overview

The TypeScript SDK provides a native JavaScript interface to fi-fhir functionality, enabling:

  • Healthcare message parsing in Node.js applications
  • Type-safe event handling with TypeScript definitions
  • Easy integration with existing JavaScript/TypeScript codebases

Architecture Options

ApproachProsCons
CLI WrapperSimple, no build complexityProcess spawn overhead
HTTP ServerLanguage agnostic, scalableRequires running service
WebAssemblyBrowser support, fastWASM size, Go WASM limitations
Native AddonBest performanceComplex build, platform-specific

Chosen: CLI Wrapper - Simplest to maintain, acceptable performance for most use cases.

Package Structure

sdk/typescript/
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts           # Main exports
│   ├── parser.ts          # Parsing functions
│   ├── workflow.ts        # Workflow execution
│   ├── types/
│   │   ├── events.ts      # Event type definitions
│   │   ├── patient.ts     # Patient types
│   │   ├── encounter.ts   # Encounter types
│   │   └── lab.ts         # Lab result types
│   └── utils/
│       └── cli.ts         # CLI wrapper utilities
├── tests/
│   ├── parser.test.ts
│   └── workflow.test.ts
└── examples/
    ├── parse-hl7.ts
    └── csv-workflow.ts

API Design

Parsing

import { parse, parseHL7, parseCSV } from '@fi-fhir/sdk';

// Auto-detect format
const events = await parse(messageContent);

// Explicit format
const hl7Event = await parseHL7(hl7Message, {
  source: 'epic_adt',
  profile: './profiles/epic.yaml',
});

// CSV with options
const csvEvents = await parseCSV(csvContent, {
  hasHeader: true,
  delimiter: ',',
  eventType: 'patient',
});

Event Types

interface EventMeta {
  id: string;
  type: EventType;
  timestamp: string;
  receivedAt: string;
  source: string;
  sourceFormat: SourceFormat;
  sourceMessageId?: string;
}

interface PatientAdmitEvent extends EventMeta {
  type: 'patient_admit';
  patient: Patient;
  encounter: Encounter;
}

interface LabResultEvent extends EventMeta {
  type: 'lab_result';
  patient: Patient;
  test: LabTest;
  result: LabValue;
  isCritical: boolean;
}

interface EligibilityInquiryEvent extends EventMeta {
  type: 'eligibility_inquiry';
  informationSource: Provider;
  informationReceiver: Provider;
  subscriber: Patient;
  dependent?: Patient;
  inquiry: EligibilityInquiry;
  traceNumber?: string;
}

interface EligibilityResponseEvent extends EventMeta {
  type: 'eligibility_response';
  informationSource: Provider;
  informationReceiver: Provider;
  subscriber: Patient;
  dependent?: Patient;
  status: EligibilityStatus;
  benefits?: EligibilityBenefit[];
  errors?: EligibilityValidationError[];
  traceNumber?: string;
  planBeginDate?: string;
  planEndDate?: string;
}

interface ClaimStatusRequestEvent extends EventMeta {
  type: 'claim_status_request';
  payer: Provider;
  provider: Provider;
  subscriber: Patient;
  dependent?: Patient;
  inquiry: ClaimStatusInquiry;
  traceNumber?: string;
}

interface ClaimStatusResponseEvent extends EventMeta {
  type: 'claim_status_response';
  payer: Provider;
  provider: Provider;
  subscriber: Patient;
  dependent?: Patient;
  claimSubmitterID?: string;
  payerClaimID?: string;
  statuses: ClaimStatusInfo[];
  serviceLines?: ClaimServiceLineStatus[];
  traceNumber?: string;
}

type HealthcareEvent =
  | PatientAdmitEvent
  | PatientUpdateEvent
  | PatientDischargeEvent
  | LabResultEvent
  | AppointmentEvent
  | ClaimSubmittedEvent
  | ClaimAdjudicatedEvent
  | EligibilityInquiryEvent
  | EligibilityResponseEvent
  | ClaimStatusRequestEvent
  | ClaimStatusResponseEvent;

Workflow

import { Workflow } from '@fi-fhir/sdk';

const workflow = await Workflow.load('./workflow.yaml');

// Process events
const results = await workflow.process(events);

// Dry run
const dryRunResults = await workflow.dryRun(events);

Workflow YAML files support CEL expressions for complex filtering:

routes:
  - name: critical_labs
    filter:
      event_type: lab_result
      condition: event.result.interpretation == "critical" # CEL expression
    actions:
      - type: webhook
        url: https://alerts.example.com

See WORKFLOW-DSL.md for the full CEL Quick Reference.

Streaming

import { createHL7Parser } from '@fi-fhir/sdk';

const parser = createHL7Parser({ source: 'lab_system' });

// Stream processing
inputStream
  .pipe(parser)
  .on('event', (event) => console.log(event))
  .on('warning', (warning) => console.warn(warning))
  .on('error', (error) => console.error(error));

Implementation

CLI Wrapper

// src/utils/cli.ts
import { spawn } from 'child_process';
import { join } from 'path';

const BINARY_PATH = process.env.FI_FHIR_PATH || 'fi-fhir';

export async function execFiFhir(
  args: string[],
  input?: string
): Promise<{ stdout: string; stderr: string }> {
  return new Promise((resolve, reject) => {
    const proc = spawn(BINARY_PATH, args, {
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    let stdout = '';
    let stderr = '';

    proc.stdout.on('data', (data) => {
      stdout += data;
    });
    proc.stderr.on('data', (data) => {
      stderr += data;
    });

    if (input) {
      proc.stdin.write(input);
      proc.stdin.end();
    }

    proc.on('close', (code) => {
      if (code === 0) {
        resolve({ stdout, stderr });
      } else {
        reject(new Error(`fi-fhir exited with code ${code}: ${stderr}`));
      }
    });
  });
}

Parser Implementation

// src/parser.ts
import { execFiFhir } from './utils/cli';
import type { HealthcareEvent, ParseOptions } from './types';

export async function parse(
  content: string,
  options: ParseOptions = {}
): Promise<HealthcareEvent[]> {
  const args = ['parse'];

  if (options.format) {
    args.push('-f', options.format);
  }
  if (options.source) {
    args.push('-s', options.source);
  }
  if (options.profile) {
    args.push('--profile', options.profile);
  }
  if (options.eventType) {
    args.push('-t', options.eventType);
  }

  args.push('-'); // Read from stdin

  const { stdout } = await execFiFhir(args, content);

  const result = JSON.parse(stdout);

  // Normalize to array
  return Array.isArray(result) ? result : [result];
}

export async function parseHL7(
  content: string,
  options: Omit<ParseOptions, 'format'> = {}
): Promise<HealthcareEvent> {
  const events = await parse(content, { ...options, format: 'hl7v2' });
  return events[0];
}

export async function parseCSV(
  content: string,
  options: CSVParseOptions = {}
): Promise<HealthcareEvent[]> {
  const args = ['parse', '-f', 'csv'];

  if (options.source) args.push('-s', options.source);
  if (options.eventType) args.push('-t', options.eventType);
  if (options.delimiter) args.push('-d', options.delimiter);
  if (options.hasHeader === false) args.push('--no-header');
  if (options.inferSchema) args.push('--infer-schema');

  args.push('-');

  const { stdout } = await execFiFhir(args, content);
  return JSON.parse(stdout);
}

Type Generation

Types are manually maintained to match Go structs in pkg/events/events.go.

Future: Consider generating TypeScript types from Go structs using:

  • go-typescript tool
  • JSON Schema generation from Go → TypeScript from JSON Schema

Binary Distribution

Options for distributing the fi-fhir binary:

  1. npm postinstall - Download platform-specific binary during install
  2. Bundled binaries - Include all platform binaries in npm package
  3. External dependency - Require user to install fi-fhir separately

Recommended: platform-specific npm packages (one per OS/arch) installed as optional dependencies.

{
  "optionalDependencies": {
    "@fi-fhir/fi-fhir-darwin-arm64": "0.1.0",
    "@fi-fhir/fi-fhir-darwin-x64": "0.1.0",
    "@fi-fhir/fi-fhir-linux-arm64": "0.1.0",
    "@fi-fhir/fi-fhir-linux-x64": "0.1.0",
    "@fi-fhir/fi-fhir-win32-x64": "0.1.0"
  }
}

CI publishes these packages on tags, using the binaries built by release:binaries.

Testing Strategy

// tests/parser.test.ts
import { describe, it, expect } from 'vitest';
import { parseHL7, parseCSV } from '../src';

describe('parseHL7', () => {
  it('parses ADT^A01 message', async () => {
    const message = `MSH|^~\\&|EPIC|...`;
    const event = await parseHL7(message);

    expect(event.type).toBe('patient_admit');
    expect(event.patient.mrn).toBeDefined();
  });
});

describe('parseCSV', () => {
  it('parses patient CSV', async () => {
    const csv = `mrn,first_name,last_name\n123,John,Doe`;
    const events = await parseCSV(csv, { eventType: 'patient' });

    expect(events).toHaveLength(1);
    expect(events[0].patient.mrn).toBe('123');
  });
});

Implementation Plan

Phase 1: Core SDK ✅

  • Package setup (package.json, tsconfig, vitest) - see sdk/typescript/
  • CLI wrapper utility with timeout support - see src/utils/cli.ts
  • Parse functions (HL7, CSV, auto-detect) - see src/parser.ts
  • Event type definitions with type guards - see src/types/events.ts
  • Basic tests - see tests/parser.test.ts
  • FiFhirError class with structured error info

Phase 2: Workflow Support ✅

  • Workflow class with load(), validate(), run() - see src/workflow.ts
  • Dry-run mode with route matching info
  • Error handling with FiFhirError
  • Workflow tests - see tests/workflow.test.ts
  • Streaming API (future)

Phase 3: Distribution 🔲

  • Platform-specific packages (optional dependencies)
  • npm publish workflow (GitLab npm registry) on tags
  • Streaming API (future)

Note: SDK is functional but distribution automation is not yet implemented.

See Also