Skip to main content

@fhir-dsl/utils

Overview

@fhir-dsl/utils is the shared toolbox: the cross-package error contract (FhirDslError + Result<T, E> + helpers), a tiny leveled logger, string naming helpers (toPascalCase, toCamelCase, toKebabCase, …), and the searchParamTypeToTs mapper used by the generator. Every other @fhir-dsl/* package imports from here so error shapes and naming stay identical across the monorepo.

Installation

npm install @fhir-dsl/utils

Exports

NameKindOne-liner
Errors
FhirDslErrorabstract classCommon base for every error in @fhir-dsl/*kind discriminator, structured context, ES2022 cause, toJSON().
SerializedFhirDslErrortypeThe shape toJSON() emits — transport-safe across MCP, logs, error-trackers.
isFhirDslErrorfunctionType-guard that narrows unknown (e.g. a catch param) to FhirDslError.
formatErrorChainfunctionWalk the cause chain into a one-line log string, cycle-guarded.
Result toolkit
Result<T, E>typeOk<T> | Err<E> discriminated by literal ok boolean.
Ok<T> / Err<E>typesThe two variants.
Resultconst{ ok, err, isOk, isErr } constructor + guard helpers.
tryAsyncfunctionLift a throwing async function into a Result.
trySyncfunctionSync variant of tryAsync.
mapErrfunctionTransform the error channel without disturbing a successful value.
mapOkfunctionTransform the success channel without disturbing the error.
matchfunctionDispatch by variant — the union-of-callbacks-return type mirrors Effect.
Logger
LogLeveltype"debug" | "info" | "warn" | "error".
LoggerclassLevel-gated console wrapper.
loggerconstDefault singleton (level info).
Naming helpers
toPascalCasefunction"observation_category""ObservationCategory".
toCamelCasefunction"Observation-Category""observationCategory".
toKebabCasefunction"ObservationCategory""observation-category".
fhirTypeToFileNamefunction"ObservationCategory""observation-category.ts".
fhirPathToPropertyNamefunction"Patient.name""name".
capitalizeFirstfunction"patient""Patient".
searchParamTypeToTsfunction"string"'{ type: "string"; value: string }'.

Error API

FhirDslError

Signature

abstract class FhirDslError<TKind extends string = string, TContext = unknown> extends Error {
abstract readonly kind: TKind;
readonly context: TContext;
// standard Error: name, message, stack, cause (ES2022)
toJSON(): SerializedFhirDslError;
}

interface SerializedFhirDslError {
name: string;
kind: string;
message: string;
context: unknown;
stack?: string;
cause?: SerializedFhirDslError | { name: string; message: string; stack?: string };
}

Notes

  • Subclasses MUST set kind to a unique string literal (e.g. "core.request", "fhirpath.evaluation").
  • Pass relevant fields through context so callers can pattern-match on values instead of parsing message strings.
  • cause is the native ES2022 chain — propagated via the optional second arg to super() and walked by toJSON() and formatErrorChain().

Example

import { FhirDslError } from "@fhir-dsl/utils";

interface IngestErrorContext {
bundleId: string;
failureIndex: number;
}

class IngestError extends FhirDslError<"my.ingest", IngestErrorContext> {
readonly kind = "my.ingest" as const;
}

throw new IngestError(
"bundle ingest failed at entry 7",
{ bundleId: "b-1", failureIndex: 7 },
{ cause: originalError },
);

See the error-handling guide for the full table of built-in subclasses and their context shapes.


isFhirDslError

Signature

function isFhirDslError(value: unknown): value is FhirDslError;

Example

import { isFhirDslError } from "@fhir-dsl/utils";

try {
await fhir.create("Patient", patient);
} catch (err) {
if (isFhirDslError(err)) {
console.error(err.kind, err.context);
} else {
throw err;
}
}

formatErrorChain

Signature

function formatErrorChain(err: unknown): string;

Returns"Name: message ← Name: message ← …". Cycle-guarded; non-Error throws are stringified.

Example

import { formatErrorChain } from "@fhir-dsl/utils";

formatErrorChain(err);
// → "FhirRequestError: 503 Service Unavailable ← TypeError: fetch failed"

Result API

Result<T, E> and constructors

Signature

type Result<T, E = FhirDslError> = Ok<T> | Err<E>;

interface Ok<T> { readonly ok: true; readonly value: T; }
interface Err<E> { readonly ok: false; readonly error: E; }

const Result: {
ok<T>(value: T): Ok<T>;
err<E>(error: E): Err<E>;
isOk<T, E>(r: Result<T, E>): r is Ok<T>;
isErr<T, E>(r: Result<T, E>): r is Err<E>;
};

Notes — Discriminated by the literal ok boolean, so if (r.ok) narrows in TypeScript without an extra import.


tryAsync / trySync

Signature

function tryAsync<T, E = unknown>(
fn: () => Promise<T>,
mapError?: (err: unknown) => E,
): Promise<Result<T, E>>;

function trySync<T, E = unknown>(
fn: () => T,
mapError?: (err: unknown) => E,
): Result<T, E>;

Example

import { tryAsync } from "@fhir-dsl/utils";
import { FhirRequestError } from "@fhir-dsl/core";

const r = await tryAsync<Patient, FhirRequestError>(() => fhir.read("Patient", "123"));
if (r.ok) console.log(r.value);
else console.error("status:", r.error.status);

mapError is the boundary-coercion hook — convert arbitrary throws into a domain error before they leave your module:

const r = await tryAsync(
() => maybeThrowingThirdParty(),
(cause) => new MyDomainError("upstream failed", { op: "...", }, { cause }),
);

mapErr / mapOk / match

Signature

function mapErr<T, E, F>(r: Result<T, E>, fn: (e: E) => F): Result<T, F>;
function mapOk<T, E, U>(r: Result<T, E>, fn: (t: T) => U): Result<U, E>;
function match<T, E, R1, R2>(
r: Result<T, E>,
handlers: { ok: (value: T) => R1; err: (error: E) => R2 },
): R1 | R2;

Example

import { mapErr, mapOk, match } from "@fhir-dsl/utils";

const r = await tryAsync(() => fhir.read("Patient", id));
const idOnly = mapOk(r, (p) => p.id);
const domain = mapErr(r, (e) => ({ kind: "domain.fail" as const, source: e }));
const summary = match(r, {
ok: (p) => `read ${p.id}`,
err: (e) => `failed: ${e.message}`,
});

Logger

LogLevel / Logger / logger

Signature

type LogLevel = "debug" | "info" | "warn" | "error";

class Logger {
constructor(level?: LogLevel);
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
}

const logger: Logger;

Notes — The default singleton is at level info. To see generator diagnostics, construct your own new Logger("debug") — the singleton's level is set at construction with no public setter.

Example

import { Logger, logger } from "@fhir-dsl/utils";

logger.info("generating types");
const quiet = new Logger("error");
quiet.debug("dropped");
quiet.error("only errors escape", { attempt: 3 });

Naming helpers

Signature

function toPascalCase(str: string): string;
function toCamelCase(str: string): string;
function toKebabCase(str: string): string;
function fhirTypeToFileName(typeName: string): string;
function fhirPathToPropertyName(path: string): string;
function capitalizeFirst(str: string): string;

Example

import {
toPascalCase,
toCamelCase,
toKebabCase,
fhirTypeToFileName,
fhirPathToPropertyName,
} from "@fhir-dsl/utils";

toPascalCase("observation-category"); // "ObservationCategory"
toCamelCase("observation_category"); // "observationCategory"
toKebabCase("ObservationCategory"); // "observation-category"
fhirTypeToFileName("Observation"); // "observation.ts"
fhirPathToPropertyName("Patient.name.given"); // "name.given"

NotestoPascalCase does not dedupe consecutive caps; "ID""ID", not "Id". fhirPathToPropertyName strips the leading resource type but preserves remaining dots.


searchParamTypeToTs

Signature

function searchParamTypeToTs(paramType: string): string;

ParametersparamType is one of "string", "token", "date", "reference", "quantity", "number", "uri", "composite", "special".

Returns — A TypeScript literal type string such as '{ type: "string"; value: string }', matching exactly what the generator emits into search-params/*.ts.

Example

import { searchParamTypeToTs } from "@fhir-dsl/utils";

searchParamTypeToTs("string");
// → '{ type: "string"; value: string }'