fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
1,383 lines (1,378 loc) • 2.06 MB
JavaScript
import cors from 'cors';
import express3 from 'express';
import { FhirClient } from '@outburn/fhir-client';
import { FormatConverter } from '@outburn/format-converter';
import { FumeMappingProvider } from '@outburn/fume-mapping-provider';
import { FhirStructureNavigator } from '@outburn/structure-navigator';
import { FhirPackageExplorer } from 'fhir-package-explorer';
import { FhirSnapshotGenerator } from 'fhir-snapshot-generator';
import { FhirTerminologyRuntime } from 'fhir-terminology-runtime';
import fumifier, { FumifierError } from 'fumifier';
import { LRUCache } from 'lru-cache';
import { z } from 'zod';
import fs from 'fs';
import yaml from 'js-yaml';
import path from 'path';
import { createHash, randomUUID } from 'crypto';
import * as dotenv from 'dotenv';
// src/server.ts
// package.json
var version = "3.1.1";
var normalizeRegistryUrl = (value) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (trimmed.toLowerCase() === "n/a") return "n/a";
return trimmed;
};
var normalizeOptionalUrl = (value) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (trimmed === "") return void 0;
if (trimmed.toLowerCase() === "n/a") return "n/a";
return trimmed;
};
var normalizeOptionalPath = (value) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (trimmed === "") return void 0;
if (trimmed.toLowerCase() === "n/a") return "n/a";
return trimmed;
};
var normalizeOptionalCacheDir = (value) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (trimmed === "") return void 0;
if (trimmed.toLowerCase() === "n/a") return void 0;
return trimmed;
};
var normalizeOptionalConnectionsFile = (value) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (trimmed === "") return void 0;
if (trimmed.toLowerCase() === "n/a") return void 0;
return trimmed;
};
var FumeConfigSchema = z.object({
SERVER_PORT: z.preprocess((a) => typeof a === "string" ? parseInt(a) : a, z.number().int("Must be an integer").nonnegative("Must be non-negative").default(42420)),
FUME_REQUEST_BODY_LIMIT: z.string().min(1).default("400mb"),
FUME_EVAL_THROW_LEVEL: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer").nonnegative("Must be non-negative").default(30)
),
FUME_EVAL_LOG_LEVEL: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer").nonnegative("Must be non-negative").default(40)
),
FUME_EVAL_DIAG_COLLECT_LEVEL: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer").nonnegative("Must be non-negative").default(70)
),
FUME_EVAL_VALIDATION_LEVEL: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer").nonnegative("Must be non-negative").default(30)
),
FHIR_SERVER_BASE: z.preprocess(
normalizeOptionalUrl,
z.string().min(1).url().or(z.literal("n/a")).default("")
),
FHIR_SERVER_AUTH_TYPE: z.string().default("NONE"),
FHIR_SERVER_UN: z.string().default(""),
FHIR_SERVER_PW: z.string().default(""),
FHIR_CONNECTIONS_FILE: z.preprocess(normalizeOptionalConnectionsFile, z.string().min(1).optional()),
FHIR_CONNECTIONS_URL_POOL_SIZE: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer").positive("Must be positive").default(10)
),
FHIR_SERVER_TIMEOUT: z.preprocess((a) => typeof a === "string" ? parseInt(a) : a, z.number().int("Must be an integer").positive("Must be positive").default(3e4)),
FHIR_VERSION: z.string().min(1).default("4.0.1"),
FHIR_PACKAGES: z.string().default(""),
FHIR_PACKAGE_REGISTRY_URL: z.preprocess(
normalizeRegistryUrl,
z.string().url().or(z.literal("n/a"))
).optional(),
FHIR_PACKAGE_REGISTRY_TOKEN: z.string().optional(),
FHIR_PACKAGE_CACHE_DIR: z.preprocess(normalizeOptionalCacheDir, z.string().min(1).optional()),
MAPPINGS_FOLDER: z.preprocess(
normalizeOptionalPath,
z.string().min(1).or(z.literal("n/a")).optional()
),
MAPPINGS_FILE_EXTENSION: z.preprocess(
normalizeOptionalPath,
z.string().min(1).optional()
),
MAPPINGS_FILE_POLLING_INTERVAL_MS: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer")
).optional(),
MAPPINGS_SERVER_POLLING_INTERVAL_MS: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer")
).optional(),
MAPPINGS_FORCED_RESYNC_INTERVAL_MS: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer")
).optional(),
FUME_COMPILED_EXPR_CACHE_MAX_ENTRIES: z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
z.number().int("Must be an integer").positive("Must be positive").default(1e3)
)
});
var defaultConfig = FumeConfigSchema.parse({});
var parseFumeConfig = (value) => FumeConfigSchema.parse(value);
var envPlaceholderPattern = /\$\{([A-Z0-9_]+)\}/g;
var base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
var ConnectionConfigSchema = z.object({
name: z.string().min(1),
baseUrl: z.string().url(),
fhirVersion: z.string().min(1).optional(),
authType: z.enum(["NONE", "BASIC"]).optional().default("NONE"),
username: z.string().min(1).optional(),
password: z.string().min(1).optional(),
timeout: z.number().int().positive().optional()
}).superRefine((connection, ctx) => {
if (connection.authType === "BASIC") {
if (!connection.username) {
ctx.addIssue({
code: "custom",
path: ["username"],
message: "username is required when authType is BASIC"
});
}
if (!connection.password) {
ctx.addIssue({
code: "custom",
path: ["password"],
message: "password is required when authType is BASIC"
});
}
}
if (connection.authType === "NONE") {
if (connection.username) {
ctx.addIssue({
code: "custom",
path: ["username"],
message: "username must not be provided when authType is NONE"
});
}
if (connection.password) {
ctx.addIssue({
code: "custom",
path: ["password"],
message: "password must not be provided when authType is NONE"
});
}
}
});
var ConnectionsFileSchema = z.object({
fhir: z.array(ConnectionConfigSchema)
}).superRefine((document, ctx) => {
const duplicateNames = document.fhir.map((connection) => connection.name).filter((name, index, names) => names.indexOf(name) !== index).filter((name, index, names) => names.indexOf(name) === index);
if (duplicateNames.length > 0) {
ctx.addIssue({
code: "custom",
path: ["fhir"],
message: `Duplicate FHIR connection name(s): ${duplicateNames.join(", ")}. Connection names must be unique.`
});
}
});
var resolveConnectionsFilePath = (config2) => {
const configuredPath = config2.FHIR_CONNECTIONS_FILE?.trim();
if (!configuredPath) {
return path.resolve(process.cwd(), "./connections.yml");
}
return path.resolve(process.cwd(), configuredPath);
};
var isPathLikeValue = (value) => {
const normalized = value.trim();
return path.isAbsolute(normalized) || normalized.startsWith("./") || normalized.startsWith("../") || normalized.startsWith(".\\") || normalized.startsWith("..\\") || normalized.startsWith("/") || normalized.startsWith("\\") || normalized.endsWith(".yml") || normalized.endsWith(".yaml");
};
var tryDecodeBase64 = (value) => {
const normalized = value.trim();
if (normalized.length === 0 || !base64Pattern.test(normalized)) {
return null;
}
const remainder = normalized.length % 4;
if (remainder === 1) {
return null;
}
const padded = normalized + "=".repeat((4 - remainder) % 4);
try {
const decoded = Buffer.from(padded, "base64").toString("utf8");
const reEncoded = Buffer.from(decoded, "utf8").toString("base64").replace(/=+$/u, "");
if (reEncoded !== normalized.replace(/=+$/u, "")) {
return null;
}
return decoded;
} catch {
return null;
}
};
var resolveRawConnectionsSource = (config2) => {
const configuredValue = config2.FHIR_CONNECTIONS_FILE?.trim();
if (!configuredValue) {
const defaultFilePath = resolveConnectionsFilePath(config2);
if (!fs.existsSync(defaultFilePath)) {
return {
raw: "",
sourceKind: "missing-file",
sourceDescription: defaultFilePath
};
}
return {
raw: fs.readFileSync(defaultFilePath, "utf8"),
sourceKind: "default-file",
sourceDescription: defaultFilePath
};
}
const resolvedPath = resolveConnectionsFilePath(config2);
if (fs.existsSync(resolvedPath)) {
return {
raw: fs.readFileSync(resolvedPath, "utf8"),
sourceKind: "configured-file",
sourceDescription: resolvedPath
};
}
const decodedBase64 = tryDecodeBase64(configuredValue);
if (decodedBase64 !== null) {
return {
raw: decodedBase64,
sourceKind: "inline-base64",
sourceDescription: "inline base64 value from FHIR_CONNECTIONS_FILE"
};
}
if (isPathLikeValue(configuredValue)) {
return {
raw: "",
sourceKind: "missing-file",
sourceDescription: resolvedPath
};
}
throw new Error("FHIR_CONNECTIONS_FILE must be either an existing file path or a base64-encoded YAML document.");
};
var resolveEnvPlaceholders = (raw, sourceDescription) => {
return raw.replace(envPlaceholderPattern, (match, variableName) => {
const envValue = process.env[variableName];
if (typeof envValue !== "string") {
throw new Error(`Missing environment variable "${variableName}" referenced in ${sourceDescription}`);
}
return envValue;
});
};
var parseConnectionsYaml = (raw) => {
const parsedYaml = yaml.load(raw);
if (parsedYaml && typeof parsedYaml === "object" && !Array.isArray(parsedYaml) && "connections" in parsedYaml && !("fhir" in parsedYaml)) {
throw new Error('Named FHIR connections now use a top-level "fhir" node instead of "connections".');
}
const parsed = ConnectionsFileSchema.parse(parsedYaml);
return parsed.fhir;
};
var ConnectionsLoader = class _ConnectionsLoader {
static load(config2) {
return _ConnectionsLoader.loadWithMetadata(config2).connections;
}
static loadWithMetadata(config2) {
const source = resolveRawConnectionsSource(config2);
if (source.sourceKind === "missing-file") {
return {
connections: [],
sourceKind: source.sourceKind,
sourceDescription: source.sourceDescription
};
}
const withResolvedEnv = resolveEnvPlaceholders(source.raw, source.sourceDescription);
return {
connections: parseConnectionsYaml(withResolvedEnv),
sourceKind: source.sourceKind,
sourceDescription: source.sourceDescription
};
}
};
var getPoolCacheKey = (url, config2) => {
const credentialTuple = `${config2.username ?? ""}:${config2.password ?? ""}`;
const credentialHash = createHash("sha256").update(credentialTuple).digest("hex");
return `${url}||${config2.authType}||${credentialHash}||${config2.fhirVersion}||${config2.timeout}`;
};
var normalizeConnectionConfig = (url, config2, globalFhirVersion, globalTimeout) => {
if (typeof config2?.authType !== "undefined" && config2.authType !== "NONE" && config2.authType !== "BASIC") {
throw new Error(`Invalid authType for inline FHIR connection config for "${url}": expected "NONE" or "BASIC".`);
}
if (typeof config2?.timeout !== "undefined" && (!Number.isInteger(config2.timeout) || config2.timeout <= 0)) {
throw new Error(`Invalid timeout for inline FHIR connection config for "${url}": expected a positive integer.`);
}
if (typeof config2?.fhirVersion !== "undefined" && (typeof config2.fhirVersion !== "string" || config2.fhirVersion.length === 0)) {
throw new Error(`Invalid fhirVersion for inline FHIR connection config for "${url}": expected a non-empty string.`);
}
if (typeof config2?.username !== "undefined" && (typeof config2.username !== "string" || config2.username.length === 0)) {
throw new Error(`Invalid username for inline FHIR connection config for "${url}": expected a non-empty string.`);
}
if (typeof config2?.password !== "undefined" && (typeof config2.password !== "string" || config2.password.length === 0)) {
throw new Error(`Invalid password for inline FHIR connection config for "${url}": expected a non-empty string.`);
}
const hasUsername = typeof config2?.username !== "undefined";
const hasPassword = typeof config2?.password !== "undefined";
const hasCredentials = hasUsername || hasPassword;
const effectiveAuthType = config2?.authType ?? (hasCredentials ? "BASIC" : "NONE");
if (config2?.authType === "NONE" && hasCredentials) {
throw new Error(`Inline FHIR connection config for "${url}" cannot provide username or password when authType is "NONE".`);
}
if (effectiveAuthType === "BASIC" && (!hasUsername || !hasPassword)) {
throw new Error(`Inline FHIR connection config for "${url}" requires both username and password when authType is "BASIC".`);
}
return {
authType: effectiveAuthType,
username: effectiveAuthType === "BASIC" ? config2?.username : void 0,
password: effectiveAuthType === "BASIC" ? config2?.password : void 0,
fhirVersion: config2?.fhirVersion ?? globalFhirVersion,
timeout: config2?.timeout ?? globalTimeout
};
};
var FhirClientPool = class {
constructor(maxSize, globalFhirVersion, globalTimeout) {
const normalizedMaxSize = Number.isInteger(maxSize) && maxSize > 0 ? maxSize : 10;
this.cache = new LRUCache({
max: normalizedMaxSize,
allowStale: false
});
this.globalFhirVersion = globalFhirVersion;
this.globalTimeout = globalTimeout;
}
get(url, config2) {
const effectiveConfig = normalizeConnectionConfig(url, config2, this.globalFhirVersion, this.globalTimeout);
const cacheKey = getPoolCacheKey(url, effectiveConfig);
const existingClient = this.cache.get(cacheKey);
if (existingClient) {
return existingClient;
}
const client = new FhirClient({
baseUrl: url,
fhirVersion: effectiveConfig.fhirVersion,
timeout: effectiveConfig.timeout,
auth: effectiveConfig.authType === "BASIC" ? { username: effectiveConfig.username, password: effectiveConfig.password } : void 0
});
this.cache.set(cacheKey, client);
return client;
}
};
// src/utils/logging.ts
var levelRank = {
debug: 10,
info: 20,
warn: 30,
error: 40,
silent: 100
};
function parseLogLevel(value) {
if (typeof value !== "string") return void 0;
switch (value.trim().toLowerCase()) {
case "silent":
case "none":
case "off":
return "silent";
case "debug":
return "debug";
case "info":
return "info";
case "warn":
return "warn";
case "warning":
return "warn";
case "error":
return "error";
case "err":
return "error";
default:
return void 0;
}
}
function createNullLogger() {
return {
info: () => {
},
warn: () => {
},
error: () => {
}
};
}
function createConsoleLogger() {
const useColors = !!process.stdout.isTTY && process.env.NO_COLOR !== "1";
const stringifyArg = (value) => {
if (typeof value === "string") return value;
if (value instanceof Error) return value.stack ?? value.message;
if (value === null) return "null";
if (value === void 0) return "undefined";
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
if (typeof value === "symbol") return value.toString();
if (typeof value === "function") return "[Function]";
if (typeof value === "object") {
try {
return JSON.stringify(value);
} catch {
return "[Object]";
}
}
return String(value);
};
const formatArgs = (args) => args.map(stringifyArg).join(" ");
const color = (ansi, text) => useColors ? `${ansi}${text}\x1B[0m` : text;
const dim = (text) => color("\x1B[90m", text);
const levelTag = (level) => {
const tag = level.toUpperCase().padEnd(5, " ");
switch (level) {
case "debug":
return color("\x1B[90m", `[${tag}]`);
case "info":
return color("\x1B[36m", `[${tag}]`);
case "warn":
return color("\x1B[33m", `[${tag}]`);
case "error":
return color("\x1B[31m", `[${tag}]`);
}
};
const formatLine = (level, args) => {
const ts = (/* @__PURE__ */ new Date()).toISOString();
const msg = formatArgs(args);
return `${dim(ts)} ${levelTag(level)} ${msg}`;
};
const writeStdout = (line) => {
process.stdout.write(line + "\n");
};
const writeStderr = (line) => {
process.stderr.write(line + "\n");
};
return {
debug: (...args) => writeStdout(formatLine("debug", args)),
info: (...args) => writeStdout(formatLine("info", args)),
warn: (...args) => writeStderr(formatLine("warn", args)),
error: (...args) => writeStderr(formatLine("error", args))
};
}
function withPrefix(logger, prefix) {
const prefixToken = prefix.length === 0 ? "" : prefix;
const prefixedLogger = {
info: (...args) => logger.info(prefixToken, ...args),
warn: (...args) => logger.warn(prefixToken, ...args),
error: (...args) => logger.error(prefixToken, ...args)
};
if (logger.debug) {
prefixedLogger.debug = (...args) => logger.debug?.(prefixToken, ...args);
}
return prefixedLogger;
}
function withLevelFilter(logger, minLevel) {
const minRank = levelRank[minLevel];
const isEnabled = (level) => levelRank[level] >= minRank;
const filteredLogger = {
info: (...args) => {
if (!isEnabled("info")) return;
logger.info(...args);
},
warn: (...args) => {
if (!isEnabled("warn")) return;
logger.warn(...args);
},
error: (...args) => {
if (!isEnabled("error")) return;
logger.error(...args);
}
};
if (logger.debug && isEnabled("debug")) {
filteredLogger.debug = (...args) => {
logger.debug?.(...args);
};
}
return filteredLogger;
}
// src/engine.ts
var hasInlineUrlConfig = (config2) => {
if (!config2) {
return false;
}
return typeof config2.authType !== "undefined" || typeof config2.username !== "undefined" || typeof config2.password !== "undefined" || typeof config2.fhirVersion !== "undefined" || typeof config2.timeout !== "undefined";
};
var defaultGlobalFhirContext = () => ({
navigator: null,
generator: null,
terminologyRuntime: null,
contextPackages: [],
normalizedPackages: [],
fhirVersion: "4.0.1",
isInitialized: false
});
var FumeEngine = class _FumeEngine {
constructor(init) {
this.namedConnectionNames = [];
this.namedClients = /* @__PURE__ */ new Map();
this.globalFhirContext = defaultGlobalFhirContext();
this.compiledExpressionCacheMaxEntries = typeof defaultConfig.FUME_COMPILED_EXPR_CACHE_MAX_ENTRIES === "number" && defaultConfig.FUME_COMPILED_EXPR_CACHE_MAX_ENTRIES > 0 ? Math.floor(defaultConfig.FUME_COMPILED_EXPR_CACHE_MAX_ENTRIES) : 1e3;
this.compiledExpressionCache = new LRUCache({
max: this.compiledExpressionCacheMaxEntries,
allowStale: false
});
this.startupTime = Date.now();
this.config = Object.freeze({ ...init.config });
this.logger = init.logger;
this.astCache = init.astCache;
this.bindings = init.bindings ?? {};
}
static async create(options) {
const logger = options.logger ?? createNullLogger();
const deprecatedStateless = options.config?.SERVER_STATELESS !== void 0;
const parsed = parseFumeConfig(options.config);
const normalizedFhirServerBase = _FumeEngine.normalizeFhirServerBase(parsed.FHIR_SERVER_BASE);
const normalizedConfig = {
...parsed,
// Preserve current runtime behavior: if disabled, store as empty string.
FHIR_SERVER_BASE: normalizedFhirServerBase ?? ""
};
const thresholdDefaults = {
throwLevel: typeof normalizedConfig.FUME_EVAL_THROW_LEVEL === "number" ? normalizedConfig.FUME_EVAL_THROW_LEVEL : 30,
logLevel: typeof normalizedConfig.FUME_EVAL_LOG_LEVEL === "number" ? normalizedConfig.FUME_EVAL_LOG_LEVEL : 40,
collectLevel: typeof normalizedConfig.FUME_EVAL_DIAG_COLLECT_LEVEL === "number" ? normalizedConfig.FUME_EVAL_DIAG_COLLECT_LEVEL : 70,
validationLevel: typeof normalizedConfig.FUME_EVAL_VALIDATION_LEVEL === "number" ? normalizedConfig.FUME_EVAL_VALIDATION_LEVEL : 30
};
const initialBindings = {
...thresholdDefaults,
...options.bindings ?? {}
};
const engine = new _FumeEngine({
config: normalizedConfig,
logger,
astCache: options.astCache,
bindings: initialBindings
});
await engine.initialize({ deprecatedStateless });
return engine;
}
getEngineLogger() {
return this.getChildLogger("[engine]");
}
getChildLogger(prefix) {
return withPrefix(this.logger, prefix);
}
getLogger() {
return this.logger;
}
registerBinding(key, binding) {
this.bindings[key] = binding;
}
getBindings() {
return this.bindings;
}
getFhirClient() {
if (!this.fhirClient) {
throw new Error("FHIR client not registered");
}
return this.fhirClient;
}
getMappingProvider() {
if (!this.mappingProvider) {
throw new Error("Mapping provider not initialized");
}
return this.mappingProvider;
}
getConfig() {
return this.config;
}
getFhirVersion() {
return this.config.FHIR_VERSION;
}
getGlobalFhirContext() {
return this.globalFhirContext;
}
getUptime() {
const totalSeconds = Math.floor((Date.now() - this.startupTime) / 1e3);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor(totalSeconds % 86400 / 3600);
const minutes = Math.floor(totalSeconds % 3600 / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`);
if (hours > 0) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
if (minutes > 0) parts.push(`${minutes} minute${minutes > 1 ? "s" : ""}`);
if (seconds > 0 || parts.length === 0) parts.push(`${seconds} second${seconds > 1 ? "s" : ""}`);
if (parts.length === 1) {
return parts[0];
}
return parts.slice(0, -1).join(", ") + " and " + parts[parts.length - 1];
}
getContextPackages() {
return this.globalFhirContext.contextPackages;
}
getNormalizedPackages() {
return this.globalFhirContext.normalizedPackages;
}
resetGlobalFhirContext() {
this.globalFhirContext = defaultGlobalFhirContext();
}
async initialize(meta) {
const log = this.getEngineLogger();
log.info("FUME initializing...");
if (meta?.deprecatedStateless) {
log.warn("SERVER_STATELESS is deprecated and ignored. Use FHIR_SERVER_BASE and/or MAPPINGS_FOLDER (set to n/a to disable).");
}
const compiledExprCacheConfigValue = this.config.FUME_COMPILED_EXPR_CACHE_MAX_ENTRIES;
const compiledExprCacheMaxEntries = typeof compiledExprCacheConfigValue === "number" && compiledExprCacheConfigValue > 0 ? Math.floor(compiledExprCacheConfigValue) : 1e3;
this.compiledExpressionCache = new LRUCache({
max: compiledExprCacheMaxEntries,
allowStale: false
});
const {
FHIR_SERVER_BASE,
MAPPINGS_FOLDER,
MAPPINGS_FILE_EXTENSION,
MAPPINGS_FILE_POLLING_INTERVAL_MS,
MAPPINGS_SERVER_POLLING_INTERVAL_MS,
MAPPINGS_FORCED_RESYNC_INTERVAL_MS
} = this.config;
const normalizedFhirServerBase = _FumeEngine.normalizeFhirServerBase(FHIR_SERVER_BASE);
const normalizedMappingsFolder = _FumeEngine.normalizeMappingsFolder(MAPPINGS_FOLDER);
const hasMappingSources2 = !!normalizedFhirServerBase || !!normalizedMappingsFolder;
if (normalizedFhirServerBase && !this.fhirClient) {
this.fhirClient = this.createFhirClient();
}
const loadedConnections = ConnectionsLoader.loadWithMetadata(this.config);
const namedConnections = loadedConnections.connections;
this.namedConnectionNames = namedConnections.map((connection) => connection.name);
this.namedClients = /* @__PURE__ */ new Map();
for (const connection of namedConnections) {
this.namedClients.set(connection.name, this.createNamedFhirClient(connection));
}
this.urlClientPool = new FhirClientPool(
this.config.FHIR_CONNECTIONS_URL_POOL_SIZE ?? 10,
this.getFhirVersion(),
this.config.FHIR_SERVER_TIMEOUT
);
log.debug?.("Initialized URL-based FHIR client pool.", { initialized: this.urlClientPool !== void 0 });
log.info(`Loaded ${this.namedClients.size} named FHIR connection(s) from ${loadedConnections.sourceKind}: ${loadedConnections.sourceDescription}.`);
await this.initializeGlobalFhirContext();
log.info("Global FHIR context initialized");
if (!hasMappingSources2) {
log.info("No mapping sources configured. Skipping mapping provider initialization.");
return;
}
log.info("Loading FUME mappings from configured sources into cache...");
const mappingsFolder = normalizedMappingsFolder;
const mappingsFileExtension = MAPPINGS_FILE_EXTENSION?.trim();
const normalizedFileExtension = mappingsFileExtension && mappingsFileExtension.toLowerCase() !== "n/a" ? mappingsFileExtension : void 0;
const mappingProviderConfig = {
logger: this.getChildLogger("[mapping-provider]"),
// Intentionally uses default FHIR client - must not be connection-aware.
...this.fhirClient ? { fhirClient: this.fhirClient } : {},
...mappingsFolder ? { mappingsFolder } : {},
...normalizedFileExtension ? { fileExtension: normalizedFileExtension } : {},
...typeof MAPPINGS_FILE_POLLING_INTERVAL_MS === "number" ? { filePollingIntervalMs: MAPPINGS_FILE_POLLING_INTERVAL_MS } : {},
...typeof MAPPINGS_SERVER_POLLING_INTERVAL_MS === "number" ? { serverPollingIntervalMs: MAPPINGS_SERVER_POLLING_INTERVAL_MS } : {},
...typeof MAPPINGS_FORCED_RESYNC_INTERVAL_MS === "number" ? { forcedResyncIntervalMs: MAPPINGS_FORCED_RESYNC_INTERVAL_MS } : {}
};
this.mappingProvider = new FumeMappingProvider(mappingProviderConfig);
await this.mappingProvider.initialize();
log.info("FumeMappingProvider initialized");
const recacheResult = await this.recacheFromServer();
if (recacheResult) {
log.info("Successfully loaded cache");
}
}
static normalizeFhirServerBase(value) {
if (!value) {
return void 0;
}
const trimmed = value.trim();
if (trimmed === "" || trimmed.toLowerCase() === "n/a") {
return void 0;
}
return trimmed;
}
static normalizeMappingsFolder(value) {
if (!value) {
return void 0;
}
const trimmed = value.trim();
if (trimmed === "" || trimmed.toLowerCase() === "n/a") {
return void 0;
}
return trimmed;
}
getOrCreateFormatConverter() {
if (!this.formatConverter) {
this.formatConverter = new FormatConverter(this.getChildLogger("[format-converter]"));
}
return this.formatConverter;
}
async convertInputToJson(input, contentType) {
const log = this.getEngineLogger();
if (!contentType || contentType === "") {
log.debug?.("Content-Type is empty - defaulting to 'application/json'");
contentType = "application/json";
}
const converter = this.getOrCreateFormatConverter();
if (contentType.startsWith("x-application/hl7-v2+er7")) {
log.debug?.("Parsing HL7 V2.x message...", { contentType });
return await converter.hl7v2ToJson(input);
}
if (contentType.startsWith("text/csv")) {
log.debug?.("Parsing CSV to JSON...", { contentType });
return await converter.csvToJson(input);
}
if (contentType.startsWith("application/xml") || contentType.startsWith("application/fhir+xml")) {
log.debug?.("Parsing XML to JSON...", { contentType });
return await converter.xmlToJson(input);
}
if (contentType.startsWith("application/json") || contentType.startsWith("application/fhir+json") || contentType.startsWith("text/json")) {
log.debug?.("Using JSON input as-is", { contentType, inputType: typeof input });
return input;
}
throw new Error(`Unsupported Content-Type: '${contentType}'`);
}
createFhirClient() {
const { FHIR_SERVER_BASE, FHIR_SERVER_TIMEOUT, FHIR_SERVER_AUTH_TYPE, FHIR_SERVER_UN, FHIR_SERVER_PW } = this.config;
const auth = FHIR_SERVER_AUTH_TYPE === "BASIC" && FHIR_SERVER_UN && FHIR_SERVER_PW ? { username: FHIR_SERVER_UN, password: FHIR_SERVER_PW } : void 0;
const client = new FhirClient({
baseUrl: FHIR_SERVER_BASE,
fhirVersion: this.getFhirVersion(),
timeout: FHIR_SERVER_TIMEOUT,
auth
});
this.getEngineLogger().info(`Using FHIR server: ${FHIR_SERVER_BASE}`);
return client;
}
createNamedFhirClient(connection) {
if (connection.authType === "BASIC" && (!connection.username || !connection.password)) {
throw new Error(
`Invalid FHIR connection "${connection.name}": authType is "BASIC" but credentials are missing. Configure both username and password in connections.yml for this connection.`
);
}
const auth = connection.authType === "BASIC" && connection.username && connection.password ? { username: connection.username, password: connection.password } : void 0;
return new FhirClient({
baseUrl: connection.baseUrl,
fhirVersion: connection.fhirVersion ?? this.getFhirVersion(),
timeout: typeof connection.timeout === "number" ? connection.timeout : this.config.FHIR_SERVER_TIMEOUT,
auth
});
}
async initializeGlobalFhirContext() {
const {
FHIR_VERSION,
FHIR_PACKAGES,
FHIR_PACKAGE_CACHE_DIR,
FHIR_PACKAGE_REGISTRY_URL,
FHIR_PACKAGE_REGISTRY_TOKEN,
MAPPINGS_SERVER_POLLING_INTERVAL_MS
} = this.config;
const cachePathOverride = typeof FHIR_PACKAGE_CACHE_DIR === "string" && FHIR_PACKAGE_CACHE_DIR.trim() !== "" ? FHIR_PACKAGE_CACHE_DIR.trim() : void 0;
const cacheMode = "lazy";
const packageList = (typeof FHIR_PACKAGES === "string" ? FHIR_PACKAGES : "").split(",").map((pkg) => pkg.trim()).filter((pkg) => pkg.length > 0).map((pkg) => {
const [id, versionRaw] = pkg.includes("@") ? pkg.split("@", 2) : [pkg, void 0];
const version2 = typeof versionRaw === "string" && versionRaw.trim() !== "" ? versionRaw.trim() : void 0;
return { id: id.trim(), version: version2 };
}).filter((pkg) => pkg.id.length > 0);
this.getEngineLogger().info({
packageContext: packageList,
fhirVersion: FHIR_VERSION,
cachePath: cachePathOverride,
registryUrl: FHIR_PACKAGE_REGISTRY_URL
});
const fpe = await FhirPackageExplorer.create({
context: packageList,
...cachePathOverride ? { cachePath: cachePathOverride } : {},
fhirVersion: FHIR_VERSION,
skipExamples: true,
logger: this.getChildLogger("[package-explorer]"),
registryUrl: FHIR_PACKAGE_REGISTRY_URL,
registryToken: FHIR_PACKAGE_REGISTRY_TOKEN
});
const generator = await FhirSnapshotGenerator.create({
fpe,
fhirVersion: FHIR_VERSION,
cacheMode,
logger: this.getChildLogger("[snapshot-generator]")
});
const navigator = new FhirStructureNavigator(generator, this.getChildLogger("[structure-navigator]"));
const terminologyRuntime = await FhirTerminologyRuntime.create({
fpe,
fhirVersion: FHIR_VERSION,
cacheMode,
logger: this.getChildLogger("[terminology-runtime]"),
// Intentionally uses default FHIR client - must not be connection-aware.
fhirClient: this.fhirClient,
...typeof MAPPINGS_SERVER_POLLING_INTERVAL_MS === "number" ? { serverConceptMapPollingIntervalMs: MAPPINGS_SERVER_POLLING_INTERVAL_MS } : {}
});
const contextPackages = fpe.getContextPackages();
const normalizedPackages = fpe.getNormalizedRootPackages();
this.globalFhirContext = {
navigator,
generator,
terminologyRuntime,
normalizedPackages,
contextPackages,
fhirVersion: this.getFhirVersion(),
cachePath: fpe.getCachePath(),
registryUrl: FHIR_PACKAGE_REGISTRY_URL,
registryToken: FHIR_PACKAGE_REGISTRY_TOKEN,
isInitialized: true
};
}
createMappingCache() {
return {
getKeys: async () => {
if (!this.mappingProvider) {
return [];
}
return this.mappingProvider.getUserMappingKeys();
},
get: async (key) => {
if (!this.mappingProvider) {
throw new Error("Mapping provider not initialized");
}
const mapping = this.mappingProvider.getUserMapping(key);
if (!mapping) {
throw new Error(`Mapping '${key}' not found`);
}
if (typeof mapping.expression !== "string") {
throw new Error(`Mapping '${key}' does not contain a valid expression`);
}
return mapping.expression;
}
};
}
createConnectionResolver() {
return (target, config2) => {
if (!target) {
if (!this.fhirClient) {
throw new Error("Default FHIR client is not initialized.");
}
return this.fhirClient;
}
if (/^https?:\/\//i.test(target)) {
if (!this.urlClientPool) {
throw new Error("FHIR URL client pool is not initialized.");
}
return this.urlClientPool.get(target, config2);
}
if (hasInlineUrlConfig(config2)) {
throw new Error(`Inline FHIR connection config is only supported for URL targets. Configure named connection "${target}" in connections.yml.`);
}
const namedClient = this.namedClients.get(target);
if (!namedClient) {
throw new Error(`Unknown FHIR connection name: "${target}"`);
}
return namedClient;
};
}
async getFumifierOptions() {
const globalContext = this.globalFhirContext;
if (!globalContext.isInitialized || !globalContext.navigator) {
throw new Error("Global FHIR context is not initialized. This should be done during warmup.");
}
if (!globalContext.terminologyRuntime) {
throw new Error("Global terminology runtime is not initialized. This should be done during warmup.");
}
const options = {
mappingCache: this.createMappingCache(),
navigator: globalContext.navigator,
terminologyRuntime: globalContext.terminologyRuntime,
// Intentionally uses default FHIR client - must not be connection-aware.
fhirClient: this.fhirClient,
namedFhirConnectionNames: [...this.namedConnectionNames],
...this.astCache ? { astCache: this.astCache } : {}
};
options.connectionResolver = this.createConnectionResolver();
return options;
}
async compileExpression(expression) {
const cacheKey = this.getCompiledExpressionCacheKey(expression);
let compiled = this.compiledExpressionCache.get(cacheKey);
if (!compiled) {
const options = await this.getFumifierOptions();
compiled = await fumifier(expression, options);
compiled.setLogger(this.getChildLogger("[fumifier]"));
this.compiledExpressionCache.set(cacheKey, compiled);
}
return compiled;
}
getCompiledExpressionCacheKey(expression) {
const normalizedPackages = this.globalFhirContext.normalizedPackages ?? [];
const fhirContext = normalizedPackages.map((p) => `${p.id}@${p.version ?? ""}`).sort();
return JSON.stringify({
source: expression,
engineVersion: version,
fhirContext,
recover: false
});
}
getStaticValueBindings() {
if (!this.mappingProvider) {
return {};
}
const provider = this.mappingProvider;
if (provider.getStaticJsonValuesObject) {
const valuesObject = provider.getStaticJsonValuesObject();
return valuesObject ?? {};
}
if (provider.getStaticJsonValues) {
const values = provider.getStaticJsonValues();
if (Array.isArray(values)) {
return values.reduce((acc, entry) => {
if (entry && typeof entry.key === "string") {
acc[entry.key] = entry.value;
}
return acc;
}, {});
}
}
if (provider.getStaticJsonValueKeys && provider.getStaticJsonValue) {
const keys = provider.getStaticJsonValueKeys();
return keys.reduce((acc, key) => {
const entry = provider.getStaticJsonValue?.(key);
if (entry) {
acc[key] = entry.value;
}
return acc;
}, {});
}
return {};
}
getEvaluationBindings(extraBindings = {}) {
let bindings = {};
const converter = this.getOrCreateFormatConverter();
bindings.parseCsv = converter.csvToJson.bind(converter);
bindings.v2json = converter.hl7v2ToJson.bind(converter);
if (this.mappingProvider) {
const aliases = this.mappingProvider.getAliases();
const staticValues = this.getStaticValueBindings();
bindings = {
...aliases,
...staticValues,
...bindings,
...this.bindings,
...extraBindings
};
} else {
bindings = {
...bindings,
...this.bindings,
...extraBindings
};
}
return bindings;
}
async transformVerbose(input, expression, extraBindings = {}) {
const log = this.getEngineLogger();
const fumeHttpInvocation = extraBindings?.fumeHttpInvocation;
const invocationMeta = {
invocation: "engine.transformVerbose",
...typeof fumeHttpInvocation?.mappingId === "string" ? { mappingId: fumeHttpInvocation.mappingId } : {},
...typeof fumeHttpInvocation?.method === "string" ? { method: fumeHttpInvocation.method } : {},
...typeof fumeHttpInvocation?.subpath === "string" ? { subpath: fumeHttpInvocation.subpath } : {}
};
log.debug?.("Starting transformation (verbose)", invocationMeta);
const expr = await this.compileExpression(expression);
const bindings = this.getEvaluationBindings(extraBindings);
const report = await expr.evaluateVerbose(input, bindings);
log.debug?.("Finished transformation (verbose)", {
...invocationMeta,
executionId: report?.executionId,
ok: report?.ok,
status: report?.status
});
return report;
}
async transform(input, expression, extraBindings = {}) {
try {
const report = await this.transformVerbose(input, expression, extraBindings);
if (report.ok) {
return report.result;
}
const primary = report.diagnostics?.error?.[0] ?? report.diagnostics?.warning?.[0] ?? report.diagnostics?.debug?.[0];
const code = primary?.code ?? "EVALUATION_FAILED";
const message = primary?.message ?? "Evaluation failed";
const properties = {
executionId: report.executionId,
...primary?.position !== void 0 ? { position: primary.position } : {},
...primary?.start !== void 0 ? { start: primary.start } : {},
...primary?.line !== void 0 ? { line: primary.line } : {},
...typeof primary?.fhirParent === "string" ? { fhirParent: primary.fhirParent } : {},
...typeof primary?.fhirElement === "string" ? { fhirElement: primary.fhirElement } : {}
};
throw new FumifierError(code, message, properties);
} catch (error) {
this.getEngineLogger().error(error);
throw error;
}
}
async recacheFromServer() {
const log = this.getEngineLogger();
if (!this.mappingProvider) {
log.warn("Mapping provider not initialized. Cannot recache mappings.");
return false;
}
try {
const terminologyRuntime = this.globalFhirContext.terminologyRuntime;
if (terminologyRuntime) {
await terminologyRuntime.clearServerConceptMapsFromCache();
log.info("Cleared FHIR terminology ConceptMap cache.");
}
await this.mappingProvider.reloadAliases();
const aliasKeys = Object.keys(this.mappingProvider.getAliases());
if (aliasKeys.length > 0) {
log.info(`Updated cache with aliases: ${aliasKeys.join(", ")}.`);
}
const staticValueProvider = this.mappingProvider;
if (staticValueProvider.reloadStaticJsonValues) {
await staticValueProvider.reloadStaticJsonValues();
const staticValueKeys = Object.keys(this.getStaticValueBindings());
if (staticValueKeys.length > 0) {
log.info(`Updated cache with static values: ${staticValueKeys.join(", ")}.`);
}
}
await this.mappingProvider.reloadUserMappings();
const userMappings = this.mappingProvider.getUserMappings();
if (userMappings.length > 0) {
log.info(`Updated cache with mappings: ${userMappings.map((m) => m.key).join(", ")}.`);
}
return true;
} catch (e) {
log.error(e);
return false;
}
}
};
// src/openapi.generated.ts
var openApiSpec = Object.freeze({
"openapi": "3.0.3",
"info": {
"title": "FUME Community API",
"version": "3.1.1",
"description": "FHIR-Utilized Mapping Engine - Community Edition. \nEvaluate ad-hoc FUME expressions or execute saved StructureMap mappings.\n"
},
"tags": [
{
"name": "Server",
"description": "General server metadata endpoints."
},
{
"name": "Evaluation",
"description": "Ad-hoc evaluation endpoints."
},
{
"name": "Health",
"description": "Liveness/readiness endpoints."
},
{
"name": "Cache",
"description": "Cache and mapping refresh endpoints."
},
{
"name": "Mapping",
"description": "Saved mapping retrieval and execution endpoints."
}
],
"servers": [
{
"url": "/",
"description": "Current server"
}
],
"components": {
"schemas": {
"ServerInfo": {
"type": "object",
"properties": {
"fume_version": {
"type": "string",
"example": "FUME Community v3.0.0"
},
"fhir_server": {
"type": "string",
"example": "http://hapi-fhir:8080/fhir"
},
"uptime": {
"type": "string",
"example": "2 days, 15 hours, 57 minutes and 26 seconds"
},
"context_packages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "hl7.fhir.r4.core"
},
"version": {
"type": "string",
"example": "4.0.1"
}
}
}
}
}
},
"HealthResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "UP"
}
}
},
"DiagnosticsObject": {
"type": "object",
"properties": {
"error": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Diagnostic"
}
},
"warning": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Diagnostic"
}
},
"debug": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Diagnostic"
}
}
},
"required": [
"error",
"warning",
"debug"
]
},
"Diagnostic": {
"type": "object",
"description": "A single diagnostic entry emitted during evaluation/validation.",
"additionalProperties": false,
"properties": {
"code": {
"type": "string",
"example": "F5120"
},
"position": {
"type": "integer",
"example": 57
},
"start": {
"type": "integer",
"example": 52
},
"line": {
"type": "integer",
"example": 2
},
"instanceOf": {
"type": "string",
"example": "Patient"
},
"fhirElement": {
"type": "string",
"example": "extension[data-absent-reason].value"
},
"bindingStrength": {
"type": "string",
"example": "required"
},
"expansionMode": {
"type": "string",
"example": "full"
},
"value": {
"description": "The value that triggered the diagnostic (may be any JSON type)."
},
"message": {
"type": "string",
"example": 'Value "abc" for "extension[data-absent-reason].value" in "Patient" is not in the required ValueSet.'
},
"severity": {
"type": "integer",
"example": 12
},
"level": {
"type": "string",
"example": "invalid"
},
"timestamp": {
"type": "integer",
"format": "int64",
"example": 1771327039703
}
}
},
"VerboseReport": {
"type": "object",
"properties": {
"ok": {
"type": "boolean",
"example": true
},
"status": {
"type": "integer",
"description": "HTTP-like status code of the evaluation result (e.g. 200, 206)",
"example": 206
},
"result": {
"description": "The raw evaluation result"
},
"diagnostics": {
"$ref": "#/components/schemas/DiagnosticsObject"
},
"executionId": {
"type": "string"
}
}
},
"FumeError": {
"type": "object",
"description": "Structured FUME error payload returned on compilation/evaluation failures.",
"properties": {
"__isFumeError": {
"type": "boolean"
},
"__isFlashError": {
"type": "boolean"
},
"message": {
"type": "string"
},
"code": {
"type": "string"
},
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"token": {
"type": "string"
},
"cause": {
"type": "string"
},
"line": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"start": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"position": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
}
},
"example": {
"__isFumeError": true,
"__isFlashError": false,
"message": 'Missing expression after ":". Objects must contain only key-value pairs',
"code": "F1101",
"name": "",
"value": "",
"token": ":",
"cause": "",
"line": 1,
"start": 6,
"position": 7
}
},
"RecacheResponse": {
"type": "object",
"description": "Recache result describing what was loaded into cache.",
"properties": {
"message": {
"type": "string",
"example": "The following Mappings were loaded to cache"
},
"mappings": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of mapping keys loaded into cache"
}
},
"example": {
"message": "The following Mappings were loaded to cache",
"mappings": [
"Patient12",
"exampleForRepo"
]
}
},
"RecacheResponseDeprecated": {
"type": "object",
"description": "Recache result for the deprecated POST /recache endpoint, which includes a deprecation warning.",
"properties": {
"message": {
"type": "string",
"example": "The following Mappings were loaded to cache"
},
"mappings": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of mapping keys loaded into cache"
},
"deprecated": {
"type": "boolean",
"description": "Whether the recache endpoint is deprecated (will emit a warning if true)"
}
},
"example": {
"message": "The following Mappings were loaded to cache",
"mappings": [
"Patient12",
"exampleForRepo"
],
"deprecated": true
}
}
},
"parameters": {
"verbose": {
"name": "verbose",
"in": "query",
"required": false,
"description": "When set to `true` (case-insensitive) or `1`, returns the full evaluation report instead of the raw result.",
"schema": {
"type": "string"
}
}
}
},
"paths": {
"/": {
"get": {
"tags": [
"Server"
],
"summary": "Server info",
"description": "Returns basic server information including version, configured FHIR server, uptime, and loaded context packages.",
"operationId": "getServerInfo",
"respo