Skip to main content

@fhir-dsl/runtime

Overview​

@fhir-dsl/runtime is the lower-level HTTP layer. FhirExecutor wraps a performRequest-based client with FHIR-specific concerns — cross-origin Authorization stripping on redirects (RFC 6750 §5.3), non-enumerable response metadata, and OperationOutcome-aware error construction. paginate() walks the Bundle.link[rel="next"] chain with cycle detection, and unwrapBundle() splits a searchset bundle into { data, included }.

Installation​

npm install @fhir-dsl/runtime

Exports​

NameKindOne-liner
FhirExecutorclassWraps fetch with FHIR error parsing, auth-strip on cross-origin redirect, and non-enumerable metadata.
ExecuteRequestOptionsinterface{ signal?: AbortSignal } — forwarded to fetch.
FhirClientConfiginterfaceRuntime-level client config (subset of the core config).
paginatefunctionAsync generator yielding pages of resources from a starting Bundle.
fetchAllPagesfunctionDrains paginate() into an array.
unwrapBundlefunctionSplits a searchset Bundle into { data, included, total, hasNext, nextUrl, raw }.
SearchResultinterfaceReturn shape of unwrapBundle.
FhirErrorclassError with status, statusText, operationOutcome, and responseText fallback.
OperationOutcome / OperationOutcomeIssueinterfaceFHIR outcome shape re-exported for convenience.

API​

FhirExecutor​

Signature

class FhirExecutor {
constructor(config: FhirClientConfig);
execute<T = unknown>(query: CompiledQuery, options?: ExecuteRequestOptions): Promise<T>;
executeUrl<T = unknown>(url: string, options?: ExecuteRequestOptions): Promise<T>;
}
interface ExecuteRequestOptions {
signal?: AbortSignal;
}
interface FhirClientConfig {
baseUrl: string;
auth?: AuthConfig;
headers?: Record<string, string>;
fetch?: typeof globalThis.fetch;
retry?: RetryConfig;
}

Parameters

  • config.baseUrl — used as the origin for execute() and as the trust boundary for executeUrl() (auth is stripped for any URL whose origin differs).
  • config.auth — static creds or a pluggable AuthProvider.
  • config.retry — transient-failure retries (429/503) honouring Retry-After.

Returns — Parsed JSON body, with response headers attached as three non-enumerable properties (headers, location, etag) so JSON.stringify(result) stays clean.

Example

import { FhirExecutor } from "@fhir-dsl/runtime";

const exec = new FhirExecutor({ baseUrl: "https://fhir.example/r4" });
const bundle = await exec.execute({ method: "GET", path: "Patient", params: [] });
const next = await exec.executeUrl(bundle.link?.find((l) => l.relation === "next")?.url ?? "");

Notes

  • Cross-origin Authorization strip (RFC 6750 §5.3). When executeUrl() is called with a URL whose origin differs from baseUrl, the auth provider and any pre-set Authorization header are dropped before the request. This prevents a server-controlled next link from exfiltrating bearer tokens to a third-party host.
  • Non-enumerable metadata. Parsed resources get Object.defineProperty(value, "headers", { enumerable: false, ... }), with Location, ETag, and Last-Modified surfaced when present. location and etag are also attached directly as non-enumerable properties.
  • Prefer: respond-async + 202 polling. The higher-level FhirClient in @fhir-dsl/core polls Content-Location on 202 responses; FhirExecutor itself does not poll — if you need async semantics, use the core client with async: AsyncPollingConfig.
  • Auto-POST upgrade. SearchQueryBuilder.usePost() forces POST; the builder also auto-upgrades GET to POST when the URL exceeds the default 1900-byte threshold (DEFAULT_AUTO_POST_THRESHOLD) — override per-builder via .getUrlByteLimit(n).

paginate / fetchAllPages​

Signature

function paginate<T extends Resource>(
executor: FhirExecutor,
firstBundle: Bundle,
options?: ExecuteRequestOptions,
): AsyncGenerator<T[], void, undefined>;

function fetchAllPages<T extends Resource>(
executor: FhirExecutor,
firstBundle: Bundle,
options?: ExecuteRequestOptions,
): Promise<T[]>;

Parameters

  • executor — the FhirExecutor to follow next links with.
  • firstBundle — the initial Bundle (usually the result of an execute()).
  • options.signal — abort signal propagated to every follow-up fetch.

Returns — paginate yields each page's resources as a T[]; fetchAllPages returns a single flat array.

Example

const first = await exec.execute<Bundle>({ method: "GET", path: "Patient", params: [{ name: "_count", value: 50 }] });
for await (const page of paginate<Patient>(exec, first)) {
console.log(`got ${page.length} patients`);
}

Notes — Visited next URLs are tracked in a Set; if the server produces a cycle, the loop terminates instead of issuing unbounded requests. paginate also peeks one link ahead and aborts early if the fetched bundle's own next points back into the visited set.


unwrapBundle​

Signature

function unwrapBundle<Primary extends Resource, Included extends Resource = never>(
bundle: Bundle,
): SearchResult<Primary, Included>;

interface SearchResult<Primary extends Resource, Included extends Resource = never> {
data: Primary[];
included: [Included] extends [never] ? [] : Included[];
total?: number;
hasNext: boolean;
nextUrl?: string;
raw: Bundle;
}

Parameters

  • bundle — a FHIR Bundle of type searchset. Entries with search.mode === "include" route to included.

Example

import { unwrapBundle } from "@fhir-dsl/runtime";
const { data, included, hasNext, nextUrl } = unwrapBundle<Patient, Organization>(bundle);

FhirError, OperationOutcome, OperationOutcomeIssue​

Signature

class FhirError extends FhirDslError<"runtime.fhir", FhirErrorContext> {
readonly kind: "runtime.fhir";
readonly status: number;
readonly statusText: string;
readonly operationOutcome?: OperationOutcome | null;
readonly responseText?: string; // raw body when it wasn't JSON
readonly issues: OperationOutcomeIssue[];
}
interface FhirErrorContext {
readonly status: number;
readonly statusText: string;
readonly operationOutcome?: OperationOutcome | null;
readonly responseText?: string;
readonly issues: readonly OperationOutcomeIssue[];
}

interface OperationOutcome { resourceType: "OperationOutcome"; issue: OperationOutcomeIssue[]; }
interface OperationOutcomeIssue {
severity: "fatal" | "error" | "warning" | "information";
code: string;
details?: { text?: string };
diagnostics?: string;
location?: string[];
expression?: string[];
}

Example

try {
await exec.execute({ method: "GET", path: "Patient/does-not-exist", params: [] });
} catch (err) {
if (err instanceof FhirError) {
console.error(err.status, err.issues[0]?.diagnostics, err.responseText);
}
}

Or with the Result toolkit, no try/catch:

import { tryAsync } from "@fhir-dsl/utils";
import { FhirError } from "@fhir-dsl/runtime";

const r = await tryAsync<unknown, FhirError>(() =>
exec.execute({ method: "GET", path: "Patient/does-not-exist", params: [] }),
);
if (!r.ok) console.error(r.error.kind, r.error.context.status, r.error.context.issues);

Notes — Extends FhirDslError with kind: "runtime.fhir". When the server returns a non-JSON body (HTML error page, text/plain from an auth proxy), operationOutcome stays null and responseText carries the raw text so diagnostics aren't lost. Pattern-match on kind and read context instead of parsing .message; toJSON() is transport-safe across MCP, logs, and error trackers.