@outburn/structure-navigator
v1.9.4
Published
Navigate and resolve FHIR element definitions using FSH-like paths
Readme
@outburn/structure-navigator
Navigate and resolve FHIR ElementDefinitions from StructureDefinition snapshots using FSH-like paths.
This library wraps a fhir-snapshot-generator instance and adds:
- FSH-style path traversal (
a.b.c) - Slice selection (
extension[race]) - Polymorphic resolution shortcuts (
valueString,value[Quantity],value[x]) - Virtual slice / profile rebasing (
extension[us-core-race],value[SimpleQuantity]) contentReferencerebasing (e.g.Bundle.entry.link.url)
Installation
This package has peer dependencies:
npm i @outburn/structure-navigator fhir-snapshot-generator fhir-package-explorer @outburn/typesQuickstart
Create a FhirPackageExplorer, then a FhirSnapshotGenerator, then a FhirStructureNavigator.
import { FhirSnapshotGenerator } from 'fhir-snapshot-generator';
import { FhirPackageExplorer } from 'fhir-package-explorer';
import { FhirStructureNavigator } from '@outburn/structure-navigator';
const fpe = await FhirPackageExplorer.create({
context: ['[email protected]'],
cachePath: './.fhir-cache',
fhirVersion: '4.0.1'
});
const fsg = await FhirSnapshotGenerator.create({
fhirVersion: '4.0.1',
cacheMode: 'lazy',
fpe
});
const nav = new FhirStructureNavigator(fsg);
const el = await nav.getElement('Patient', 'identifier.assigner.display');
console.log(el.path); // "Reference.display"
const children = await nav.getChildren('Patient', 'identifier');
console.log(children.map(c => c.path));API
new FhirStructureNavigator(fsg, logger?, cacheOptions?)
fsg: aFhirSnapshotGenerator.logger(optional):{ debug, info, warn, error }.cacheOptions(optional): External cache implementations for DI (see Cache Architecture below).
Cache Architecture
The navigator implements a two-tier caching strategy:
- Inner LRU Layer: Fast, in-memory LRU cache for ultra-hot entries
- External Layer (optional): Pluggable external cache (e.g., LMDB) via dependency injection
Cache Types
Four independent cache interfaces can be injected:
interface NavigatorCacheOptions {
snapshotCache?: ICache<any>;
typeMetaCache?: ICache<FileIndexEntryWithPkg>;
elementCache?: ICache<EnrichedElementDefinition>;
childrenCache?: ICache<EnrichedElementDefinition[]>;
lruSizes?: {
snapshot?: number;
typeMeta?: number;
element?: number;
children?: number;
};
}Cache Interface
The ICache<T> interface supports array-based keys for LMDB compatibility:
interface ICache<T> {
get(key: (string | number)[]): Promise<T | undefined> | T | undefined;
set(key: (string | number)[], value: T): Promise<void> | void;
has(key: (string | number)[]): Promise<boolean> | boolean;
delete(key: (string | number)[]): Promise<boolean> | boolean;
clear(): Promise<void> | void;
}Keys are structured as arrays to avoid string concatenation/splitting overhead in LMDB implementations.
LRU Sizing
Each cache always has an in-memory LRU hot layer, regardless of whether an external cache is provided. Providing an external cache does not change the in-memory LRU sizing; it simply adds an optional "cold" layer that entries can be promoted from.
Default LRU sizes:
| Cache Type | Default LRU size | |------------|------------------| | Snapshot | 100 | | TypeMeta | 500 | | Element | 2000 | | Children | 500 |
You can override any of these (entry counts) via cacheOptions.lruSizes:
const nav = new FhirStructureNavigator(fsg, logger, {
lruSizes: {
// keep more element path resolutions hot in memory
element: 5000,
// keep fewer snapshots hot (lower memory footprint)
snapshot: 50
}
});Package Context Namespacing
Element and children caches include a package context namespace (from FPE.getNormalizedRootPackages()) in their keys. This ensures safe sharing of external caches between navigator instances with different package contexts.
The snapshot and typeMeta caches already include package information in their keys, so no additional namespacing is needed.
Example: Custom Cache Implementation
import { ICache } from '@outburn/structure-navigator';
class MyLMDBCache<T> implements ICache<T> {
async get(key: (string | number)[]): Promise<T | undefined> {
// Use key array directly with LMDB range queries
return await this.db.get(key);
}
async set(key: (string | number)[], value: T): Promise<void> {
await this.db.put(key, value);
}
// ... implement other methods
}
const nav = new FhirStructureNavigator(fsg, logger, {
snapshotCache: new MyLMDBCache(),
elementCache: new MyLMDBCache(),
childrenCache: new MyLMDBCache()
});getElement(snapshotId, fshPath)
Resolves a single element using an FSH-like path.
snapshotId: either- a string (StructureDefinition id or canonical url), e.g.
"us-core-patient","Patient","http://.../StructureDefinition/...", or - a
FileIndexEntryWithPkg(package id/version + filename), as used byfhir-package-explorer.
- a string (StructureDefinition id or canonical url), e.g.
fshPath: FSH-like path string (see below).
Returns an EnrichedElementDefinition.
getChildren(snapshotId, fshPath)
Returns the direct children of the resolved element.
- Use
"."to get children of the root element.
getFsg(), getFpe(), getLogger()
Access the underlying snapshot generator, package explorer, and logger.
Returned element shape
The navigator enriches each returned element with metadata useful for tooling:
__fromDefinition: canonical URL of the StructureDefinition the element ultimately came from__corePackage: the “core” package identifier used for resolving base types__packageId/__packageVersion: package that contributed the resolved snapshot__name: computed “FSH-ish” name(s)- For polymorphic
value[x],__nameis inferred likevalueString,valueQuantity, etc. - For
contentReferenceelements,__nameis inferred from the reference target.
- For polymorphic
type[].__kind: best-effort kind info (primitive-type,complex-type,resource,logical,system, …)
Note: the navigator also strips a set of verbose fields (like definition, comment, mapping, …) from elements when caching snapshots.
Path syntax (FSH-like)
Paths are dot-separated segments. Dots inside [...] are not treated as separators.
1) Normal element navigation
await nav.getElement('us-core-patient', 'gender');
await nav.getElement('Patient', 'address.city');2) Deep navigation across types (rebasing)
If an element’s type points to another StructureDefinition (base type or profile), traversal “rebases” into that snapshot.
Examples:
identifier.value.extensionrebases fromIdentifier.value(string) intoStructureDefinition/string.identifier.assigner.identifier.assigner.displayrebases throughReference/Identifierback and forth.
const el = await nav.getElement('us-core-patient', 'identifier.value.extension');
console.log(el.path); // "string.extension"3) Slices: element[sliceName]
FSH slice selection is supported:
await nav.getElement('us-core-patient', 'extension[race]');
await nav.getElement('us-core-patient', 'extension[race].url');4) Polymorphic elements ([x])
For polymorphic elements like Extension.value[x], you can select a type in several ways.
a) Shortcut suffix form: valueString, valueQuantity, …
const el = await nav.getElement('Extension', 'valueString');
// resolves Extension.value[x] and narrows type to stringb) Bracket type form: value[string], value[Quantity], value[CodeableConcept]
await nav.getElement('Extension', 'value[string]');
await nav.getElement('Extension', 'value[Quantity]');
await nav.getElement('Extension', 'value[CodeableConcept]');c) Base polymorphic element: value[x]
await nav.getElement('Extension', 'value[x]'); // returns the polymorphic head with all possible typesd) Traverse into a selected polymorphic type
await nav.getElement('Extension', 'valueQuantity.value');
await nav.getElement('Extension', 'value[Quantity].value');
await nav.getElement('Extension', 'valueReference.identifier.system');5) Real polymorphic slices
Some profiles define real slices on polymorphics, e.g. Extension.value[x]:valueString.
The navigator will return the real slice element when it exists:
const profile = 'http://example.org/StructureDefinition/ExtensionWithPolySlices';
await nav.getElement(profile, 'valueString');
await nav.getElement(profile, 'value[string]');
await nav.getElement(profile, 'value[valueString]');If a slice exists for some types but not others, selecting a non-sliced type still works and returns the narrowed head.
6) Virtual slices (profile rebasing): element[SomeProfile]
If the text inside brackets is not a real slice name, the navigator will try to resolve it as a StructureDefinition (by id in core package context, or by canonical URL).
If it resolves, it is treated as a “virtual slice” and traversal continues in that profile snapshot.
await nav.getElement('Patient', 'extension[us-core-race]');
await nav.getElement('Patient', 'extension[http://hl7.org/fhir/us/core/StructureDefinition/us-core-race]');
await nav.getElement('Patient', 'extension[us-core-race].url');Virtual slicing is also supported on polymorphics when the profile’s base type is allowed:
await nav.getElement('Observation', 'value[SimpleQuantity]');
await nav.getElement('Observation', 'value[SimpleQuantity].value');7) contentReference rebasing
FHIR allows elements to reference other elements via contentReference (e.g. Bundle.entry.link has #Bundle.link).
When encountered, traversal rebases through the referenced element and continues.
await nav.getElement('Bundle', 'entry.link');
await nav.getElement('Bundle', 'entry.link.url');
await nav.getElement('Questionnaire', 'item.item.item.item.linkId');Getting children
Use getChildren() to fetch direct children.
// Root children
const rootChildren = await nav.getChildren('us-core-patient', '.');
// Children of a resolved path
const idChildren = await nav.getChildren('us-core-patient', 'identifier');
// Children also work through rebasing/polymorphics/slices/contentReference
await nav.getChildren('Extension', 'valueString');
await nav.getChildren('Patient', 'extension[us-core-race]');
await nav.getChildren('Bundle', 'entry.link.extension');Errors and gotchas
- If a segment can’t be found,
getElement()throws. - If you use a virtual slice/profile whose base type is not allowed by the parent element’s
type[], it throws with an “Expected one of …” message.- Example:
Observation.value[bp]throws because the resolved profile type is not permitted.
- Example:
getChildren()throws for choice-type elements that still have multiple possible types (because children are ambiguous).getChildren('.', ...)is supported only viafshPath = "."(root).
License
Apache-2.0 (see LICENSE).
