Skip to content

Outburn-IL/fume-mapping-provider

Repository files navigation

@outburn/fume-mapping-provider

A TypeScript module for managing pre-defined, named FUME expressions (mappings) from multiple sources: files, FHIR servers, and FHIR packages.

Features

  • 🚀 Lightning-fast user mappings: In-memory cache for file and server mappings
  • 📁 File mappings: Load from local .fume files (or custom extensions)
  • 🌐 Server mappings: Load from FHIR servers with automatic pagination
  • 📦 Package mappings: Load from FHIR packages via fhir-package-explorer
  • 🔄 Smart collision handling: File mappings override server mappings
  • 🛰️ Automatic change tracking: Poll files and FHIR server resources with incremental updates
  • 🔍 Flexible search: Find package mappings by URL, ID, or name
  • 📝 Structured logging: Optional logger interface support
  • FUME validation: Filters StructureMaps by FUME-specific extensions

Architecture

The provider separates mappings into two categories:

User Mappings (File + Server)

  • Fast access: Loaded once, cached in memory
  • Key-based lookup: Use unique keys for instant retrieval
  • Collision resolution: File mappings override server mappings with warnings
  • Focused refresh by key: Re-fetch a single mapping from its real source

Package Mappings

  • Always fresh: Queried from FPE on demand (FPE handles caching)
  • Immutable: Packages never change once installed
  • Smart identifier resolution: Try URL → ID → name automatically
  • Package filtering: Filter by package context

Installation

npm install @outburn/fume-mapping-provider

# Dependencies based on usage:
npm install fhir-package-explorer  # For package mappings
npm install @outburn/fhir-client    # For server mappings

Quick Start

import { FumeMappingProvider } from '@outburn/fume-mapping-provider';
import { FhirPackageExplorer } from 'fhir-package-explorer';
import { FhirClient } from '@outburn/fhir-client';

// Setup dependencies
const fhirClient = new FhirClient({ baseUrl: 'https://hapi.fhir.org/baseR4' });
const packageExplorer = await FhirPackageExplorer.create({
  context: ['hl7.fhir.us.core@6.1.0']
});

// Create provider
const provider = new FumeMappingProvider({
  mappingsFolder: './mappings',
  fileExtension: '.fume', // Optional, default is '.fume'
  fhirClient: fhirClient,
  packageExplorer: packageExplorer,
  aliasConceptMapId: 'my-aliases-cm-id', // Optional, skips alias ConceptMap search
  canonicalBaseUrl: 'http://example.com', // Optional, default is 'http://example.com'
  filePollingIntervalMs: 5000,   // Optional, default 5000 (set <= 0 to disable)
  serverPollingIntervalMs: 30000, // Optional, default 30000 (set <= 0 to disable)
  forcedResyncIntervalMs: 3600000, // Optional, default 1 hour (set <= 0 to disable)
  logger: console // Optional
});

// Initialize (loads user mappings into cache)
await provider.initialize();

User Mappings API

Load and Cache

// Initialize loads all user mappings (file + server) into memory
await provider.initialize();

// Reload all user mappings
await provider.reloadUserMappings();

// Refresh specific mapping by key (fetches from source)
await provider.refreshUserMapping('my-mapping-key');

Static JSON Values (*.json)

If mappingsFolder is configured, the provider also loads *.json files from that folder as static JSON values.

  • The key is the filename without .json
  • The value is the parsed JSON value (object/array/string/number/boolean/null)
  • The reserved filename aliases.json is never treated as a static value
  • Static JSON values are not treated as mappings and do not override *.fume mappings; they are exposed via a separate API.
// Reload all static JSON values
await provider.reloadStaticJsonValues();

// Refresh a specific static JSON value by key
await provider.refreshStaticJsonValue('myStaticKey');

// Read from cache
const value = provider.getStaticJsonValue('myStaticKey');
// Returns: StaticJsonValue | undefined

Get User Mappings (Lightning Fast ⚡)

// Get all user mappings
const mappings = provider.getUserMappings();
// Returns: UserMapping[]

// Get all user mapping keys
const keys = provider.getUserMappingKeys();
// Returns: string[]

// Get metadata only (without expressions)
const metadata = provider.getUserMappingsMetadata();
// Returns: UserMappingMetadata[]

// Get specific mapping by key
const mapping = provider.getUserMapping('my-key');
// Returns: UserMapping | undefined

Automatic Change Tracking

On initialize(), the provider automatically starts polling sources to keep the in-memory cache aligned with files and server resources.

  • File polling (default: 5s): detects changes in mapping files, static JSON value files (*.json), and aliases.json incrementally.
  • Server polling (default: 30s):
    • Aliases: conditional read of the alias ConceptMap (ETag/Last-Modified).
    • Mappings: StructureMap search with _lastUpdated.
  • Forced resync (default: 1h): full refresh of aliases + mappings, applied incrementally.

Disable any polling loop by setting its interval to <= 0.

UserMapping Structure

interface UserMapping {
  key: string;                    // Unique identifier
  expression: string;             // FUME expression
  sourceType: 'file' | 'server';  // Origin
  source: string;                 // Absolute file path or full server URL
  name?: string;                  // StructureMap.name
  url?: string;                   // StructureMap.url
}

interface StaticJsonValue {
  key: string;               // Filename without `.json`
  value: unknown;            // Parsed JSON value
  sourceType: 'file';        // Origin
  source: string;            // Absolute file path
}

Package Mappings API

Get Package Mappings

// Get all package mappings
const mappings = await provider.getPackageMappings();

// Filter by package context
const mappings = await provider.getPackageMappings({
  packageContext: 'hl7.fhir.us.core@6.1.0'
});

// Get metadata only (without expressions)
const metadata = await provider.getPackageMappingsMetadata();

Aliases API

Overview

Aliases are simple key-value string mappings stored in a special ConceptMap resource on the FHIR server. Unlike mappings, aliases are:

  • Server + File: Loaded from the FHIR server and/or an optional aliases.json file in mappingsFolder
  • Consolidated: Always served as a single object
  • Cached: Kept fresh via automatic change tracking

When both server and file sources are configured:

  • File aliases override server aliases on key collision (a warning is logged if a logger is provided)
  • Server aliases override built-in aliases

Alias Resource Structure

FUME aliases are stored in a ConceptMap resource with this specific useContext:

{
  code: {
    system: "http://snomed.info/sct",
    code: "706594005"
  },
  valueCodeableConcept: {
    coding: [{
      system: "http://codes.fume.health",
      code: "fume"
    }]
  }
}

The server is queried with: GET [baseUrl]/ConceptMap?context=http://codes.fume.health|fume&name=FumeAliases

Get Aliases

// Get all aliases (fast, cached)
const aliases = provider.getAliases();
// Returns: { [key: string]: string }

// Get all aliases with per-alias metadata (source + sourceType)
const aliasesWithMeta = provider.getAliasesWithMetadata();
// Returns: { [key: string]: { value: string; sourceType: 'file'|'server'|'builtIn'; source: string } }

// Get the ConceptMap id used for server aliases (if loaded)
// Downstream consumers can use this id when updating the alias ConceptMap
const aliasResourceId = provider.getAliasResourceId();
// Returns: string | undefined

// Example:
// {
//   "patientSystemUrl": "http://example.com/patients",
//   "defaultLanguage": "en-US",
//   "apiVersion": "v1"
// }

File Aliases (aliases.json)

If mappingsFolder is configured, the provider will look for a special aliases.json file inside it.

Rules:

  • If mappingsFolder is not set, file aliases are not supported.
  • If aliases.json is missing, no file aliases are loaded.
  • If aliases.json exists but is invalid, a warning is logged and the file is ignored.

Example aliases.json:

{
  "patientSystemUrl": "http://example.com/patients",
  "defaultLanguage": "en-US",
  "apiVersion": "v1"
}

Validation:

  • Must be a JSON object
  • Keys must match ^[A-Za-z0-9_]+$ (no whitespace or operators like - or .)
  • Values must be strings

Reload Aliases

// Reload from configured sources (server and/or mappingsFolder)
await provider.reloadAliases();

Transforming Alias Resources

// ConceptMap → Alias Object
const aliases = provider.conceptMapToAliasObject(conceptMap);

// Alias Object → ConceptMap
const conceptMap = provider.aliasObjectToConceptMap(aliases, existingConceptMap);

Package Mappings API

Get by Identifier

Automatically tries URL → ID → name in order, returns first match:

// Try to find by URL, ID, or name
const mapping = await provider.getPackageMapping('patient-transform');
// First tries as URL, then ID, then name

// With package filtering
const mapping = await provider.getPackageMapping('patient-transform', {
  packageContext: 'hl7.fhir.us.core@6.1.0'
});

PackageMapping Structure

interface PackageMapping {
  id: string;                // StructureMap.id
  expression: string;        // FUME expression
  packageId: string;         // Package ID
  packageVersion: string;    // Package version
  filename: string;          // File in package
  name?: string;             // StructureMap.name
  url?: string;              // StructureMap.url
}

Collision Handling

When a file mapping has the same key as a server mapping:

  1. File mapping wins (overrides server mapping)
  2. Warning is logged (if logger provided)
  3. Refresh checks file first, falls back to server if file deleted
// File: ./mappings/my-mapping.fume
// expression: InstanceOf: Patient

// Server: StructureMap with id="my-mapping"
// expression: InstanceOf: Patient

// After initialize():
const mapping = provider.getUserMapping('my-mapping');
// mapping.sourceType === 'file'
// mapping.expression === 'InstanceOf: Patient'
// Warning logged: "File mapping 'my-mapping' overrides server mapping"

Examples

File-Only Setup

const provider = new FumeMappingProvider({
  mappingsFolder: './mappings'
});

await provider.initialize();

const keys = provider.getUserMappingKeys();
// ['mapping1', 'mapping2', 'mapping3']

const mapping = provider.getUserMapping('mapping1');
// { key: 'mapping1', expression: '...', sourceType: 'file', source: '/abs/path/mapping1.fume' }

Server-Only Setup

const provider = new FumeMappingProvider({
  fhirClient: new FhirClient({ baseUrl: 'https://fhir.server.com' })
});

await provider.initialize();

const mappings = provider.getUserMappings();
// All StructureMaps from server (with FUME extensions)

Package-Only Setup

const explorer = await FhirPackageExplorer.create({
  context: ['hl7.fhir.us.core@6.1.0']
});

const provider = new FumeMappingProvider({
  packageExplorer: explorer
});

// No initialize needed for package-only
const mappings = await provider.getPackageMappings();

Combined Setup

const provider = new FumeMappingProvider({
  mappingsFolder: './mappings',
  fhirClient: fhirClient,
  packageExplorer: explorer,
  logger: console
});

await provider.initialize(); // Loads user mappings only

// User mappings (fast, cached)
const userKeys = provider.getUserMappingKeys();
const userMapping = provider.getUserMapping('my-key');

// Package mappings (on-demand)
const pkgMappings = await provider.getPackageMappings();
const pkgMapping = await provider.getPackageMapping('patient-transform');

API Reference

FumeMappingProvider

Constructor

new FumeMappingProvider(config: FumeMappingProviderConfig)

Methods

Initialization:

  • initialize(): Promise<void> - Load caches and start automatic change tracking
  • reloadUserMappings(): Promise<void> - Reload all user mappings
  • refreshUserMapping(key: string): Promise<UserMapping | null> - Refresh specific user mapping
  • startAutomaticChangeTracking(): void - Start polling + forced resync
  • stopAutomaticChangeTracking(): void - Stop polling + forced resync

User Mappings (Cached, Fast):

  • getUserMappings(): UserMapping[] - Get all user mappings
  • getUserMappingKeys(): string[] - Get all user mapping keys
  • getUserMappingsMetadata(): UserMappingMetadata[] - Get metadata only
  • getUserMapping(key: string): UserMapping | undefined - Get specific mapping

Package Mappings (On-Demand):

  • getPackageMappings(options?: GetPackageMappingOptions): Promise<PackageMapping[]> - Get all package mappings
  • getPackageMappingsMetadata(options?: GetPackageMappingOptions): Promise<PackageMappingMetadata[]> - Get metadata only
  • getPackageMapping(identifier: string, options?: GetPackageMappingOptions): Promise<PackageMapping | null> - Get by identifier

Aliases (Cached, Fast):

  • reloadAliases(): Promise<void> - Reload all aliases from server
  • getAliases(): AliasObject - Get all cached aliases as single object
  • getAliasesWithMetadata(): AliasObjectWithMetadata - Get all cached aliases with metadata
  • getAliasResourceId(): string | undefined - Get ConceptMap id for server aliases (if loaded)

Converters:

  • getCanonicalBaseUrl(): string - Get canonical base URL used for generated resources
  • structureMapToExpression(structureMap: StructureMap): string | null - Extract FUME expression from StructureMap
  • expressionToStructureMap(mappingId: string, expression: string): StructureMap - Create StructureMap from expression (uses canonical base URL)
  • conceptMapToAliasObject(conceptMap: ConceptMap): AliasObject - Transform ConceptMap to alias object
  • aliasObjectToConceptMap(aliases: AliasObject, existingConceptMap?: ConceptMap): ConceptMap - Transform alias object to ConceptMap (uses canonical base URL)

Configuration

interface FumeMappingProviderConfig {
  mappingsFolder?: string;           // Path to .fume files
  fileExtension?: string;            // Default: '.fume' ('.json' is reserved for aliases.json)
  fhirClient?: any;                  // FHIR client instance
  packageExplorer?: any;             // FPE instance
  logger?: Logger;                   // Optional logger
  aliasConceptMapId?: string;        // Optional ConceptMap id for aliases (skips search)
  canonicalBaseUrl?: string;         // Default: 'http://example.com'
  filePollingIntervalMs?: number;    // Default: 5000 (set <= 0 to disable)
  serverPollingIntervalMs?: number;  // Default: 30000 (set <= 0 to disable)
  forcedResyncIntervalMs?: number;   // Default: 3600000 (set <= 0 to disable)
}

License

MIT
© Outburn Ltd. 2022–2025. All Rights Reserved.

About

Manage pre-defined, named FUME expressions (mappings) from FHIR packages, servers, or files

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors