fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
1,353 lines (1,345 loc) • 2.06 MB
JavaScript
'use strict';
var fhirClient = require('@outburn/fhir-client');
var formatConverter = require('@outburn/format-converter');
var fumeMappingProvider = require('@outburn/fume-mapping-provider');
var structureNavigator = require('@outburn/structure-navigator');
var fhirPackageExplorer = require('fhir-package-explorer');
var fhirSnapshotGenerator = require('fhir-snapshot-generator');
var fhirTerminologyRuntime = require('fhir-terminology-runtime');
var fumifier = require('fumifier');
var lruCache = require('lru-cache');
var zod = require('zod');
var fs = require('fs');
var yaml = require('js-yaml');
var path = require('path');
var crypto = require('crypto');
var cors = require('cors');
var express3 = require('express');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var fumifier__default = /*#__PURE__*/_interopDefault(fumifier);
var fs__default = /*#__PURE__*/_interopDefault(fs);
var yaml__default = /*#__PURE__*/_interopDefault(yaml);
var path__default = /*#__PURE__*/_interopDefault(path);
var cors__default = /*#__PURE__*/_interopDefault(cors);
var express3__default = /*#__PURE__*/_interopDefault(express3);
// src/engine.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 = zod.z.object({
SERVER_PORT: zod.z.preprocess((a) => typeof a === "string" ? parseInt(a) : a, zod.z.number().int("Must be an integer").nonnegative("Must be non-negative").default(42420)),
FUME_REQUEST_BODY_LIMIT: zod.z.string().min(1).default("400mb"),
FUME_EVAL_THROW_LEVEL: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer").nonnegative("Must be non-negative").default(30)
),
FUME_EVAL_LOG_LEVEL: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer").nonnegative("Must be non-negative").default(40)
),
FUME_EVAL_DIAG_COLLECT_LEVEL: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer").nonnegative("Must be non-negative").default(70)
),
FUME_EVAL_VALIDATION_LEVEL: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer").nonnegative("Must be non-negative").default(30)
),
FHIR_SERVER_BASE: zod.z.preprocess(
normalizeOptionalUrl,
zod.z.string().min(1).url().or(zod.z.literal("n/a")).default("")
),
FHIR_SERVER_AUTH_TYPE: zod.z.string().default("NONE"),
FHIR_SERVER_UN: zod.z.string().default(""),
FHIR_SERVER_PW: zod.z.string().default(""),
FHIR_CONNECTIONS_FILE: zod.z.preprocess(normalizeOptionalConnectionsFile, zod.z.string().min(1).optional()),
FHIR_CONNECTIONS_URL_POOL_SIZE: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer").positive("Must be positive").default(10)
),
FHIR_SERVER_TIMEOUT: zod.z.preprocess((a) => typeof a === "string" ? parseInt(a) : a, zod.z.number().int("Must be an integer").positive("Must be positive").default(3e4)),
FHIR_VERSION: zod.z.string().min(1).default("4.0.1"),
FHIR_PACKAGES: zod.z.string().default(""),
FHIR_PACKAGE_REGISTRY_URL: zod.z.preprocess(
normalizeRegistryUrl,
zod.z.string().url().or(zod.z.literal("n/a"))
).optional(),
FHIR_PACKAGE_REGISTRY_TOKEN: zod.z.string().optional(),
FHIR_PACKAGE_CACHE_DIR: zod.z.preprocess(normalizeOptionalCacheDir, zod.z.string().min(1).optional()),
MAPPINGS_FOLDER: zod.z.preprocess(
normalizeOptionalPath,
zod.z.string().min(1).or(zod.z.literal("n/a")).optional()
),
MAPPINGS_FILE_EXTENSION: zod.z.preprocess(
normalizeOptionalPath,
zod.z.string().min(1).optional()
),
MAPPINGS_FILE_POLLING_INTERVAL_MS: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer")
).optional(),
MAPPINGS_SERVER_POLLING_INTERVAL_MS: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer")
).optional(),
MAPPINGS_FORCED_RESYNC_INTERVAL_MS: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.z.number().int("Must be an integer")
).optional(),
FUME_COMPILED_EXPR_CACHE_MAX_ENTRIES: zod.z.preprocess(
(a) => typeof a === "string" ? parseInt(a) : a,
zod.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 = zod.z.object({
name: zod.z.string().min(1),
baseUrl: zod.z.string().url(),
fhirVersion: zod.z.string().min(1).optional(),
authType: zod.z.enum(["NONE", "BASIC"]).optional().default("NONE"),
username: zod.z.string().min(1).optional(),
password: zod.z.string().min(1).optional(),
timeout: zod.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 = zod.z.object({
fhir: zod.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 = (config) => {
const configuredPath = config.FHIR_CONNECTIONS_FILE?.trim();
if (!configuredPath) {
return path__default.default.resolve(process.cwd(), "./connections.yml");
}
return path__default.default.resolve(process.cwd(), configuredPath);
};
var isPathLikeValue = (value) => {
const normalized = value.trim();
return path__default.default.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 = (config) => {
const configuredValue = config.FHIR_CONNECTIONS_FILE?.trim();
if (!configuredValue) {
const defaultFilePath = resolveConnectionsFilePath(config);
if (!fs__default.default.existsSync(defaultFilePath)) {
return {
raw: "",
sourceKind: "missing-file",
sourceDescription: defaultFilePath
};
}
return {
raw: fs__default.default.readFileSync(defaultFilePath, "utf8"),
sourceKind: "default-file",
sourceDescription: defaultFilePath
};
}
const resolvedPath = resolveConnectionsFilePath(config);
if (fs__default.default.existsSync(resolvedPath)) {
return {
raw: fs__default.default.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__default.default.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(config) {
return _ConnectionsLoader.loadWithMetadata(config).connections;
}
static loadWithMetadata(config) {
const source = resolveRawConnectionsSource(config);
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, config) => {
const credentialTuple = `${config.username ?? ""}:${config.password ?? ""}`;
const credentialHash = crypto.createHash("sha256").update(credentialTuple).digest("hex");
return `${url}||${config.authType}||${credentialHash}||${config.fhirVersion}||${config.timeout}`;
};
var normalizeConnectionConfig = (url, config, globalFhirVersion, globalTimeout) => {
if (typeof config?.authType !== "undefined" && config.authType !== "NONE" && config.authType !== "BASIC") {
throw new Error(`Invalid authType for inline FHIR connection config for "${url}": expected "NONE" or "BASIC".`);
}
if (typeof config?.timeout !== "undefined" && (!Number.isInteger(config.timeout) || config.timeout <= 0)) {
throw new Error(`Invalid timeout for inline FHIR connection config for "${url}": expected a positive integer.`);
}
if (typeof config?.fhirVersion !== "undefined" && (typeof config.fhirVersion !== "string" || config.fhirVersion.length === 0)) {
throw new Error(`Invalid fhirVersion for inline FHIR connection config for "${url}": expected a non-empty string.`);
}
if (typeof config?.username !== "undefined" && (typeof config.username !== "string" || config.username.length === 0)) {
throw new Error(`Invalid username for inline FHIR connection config for "${url}": expected a non-empty string.`);
}
if (typeof config?.password !== "undefined" && (typeof config.password !== "string" || config.password.length === 0)) {
throw new Error(`Invalid password for inline FHIR connection config for "${url}": expected a non-empty string.`);
}
const hasUsername = typeof config?.username !== "undefined";
const hasPassword = typeof config?.password !== "undefined";
const hasCredentials = hasUsername || hasPassword;
const effectiveAuthType = config?.authType ?? (hasCredentials ? "BASIC" : "NONE");
if (config?.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" ? config?.username : void 0,
password: effectiveAuthType === "BASIC" ? config?.password : void 0,
fhirVersion: config?.fhirVersion ?? globalFhirVersion,
timeout: config?.timeout ?? globalTimeout
};
};
var FhirClientPool = class {
constructor(maxSize, globalFhirVersion, globalTimeout) {
const normalizedMaxSize = Number.isInteger(maxSize) && maxSize > 0 ? maxSize : 10;
this.cache = new lruCache.LRUCache({
max: normalizedMaxSize,
allowStale: false
});
this.globalFhirVersion = globalFhirVersion;
this.globalTimeout = globalTimeout;
}
get(url, config) {
const effectiveConfig = normalizeConnectionConfig(url, config, this.globalFhirVersion, this.globalTimeout);
const cacheKey = getPoolCacheKey(url, effectiveConfig);
const existingClient = this.cache.get(cacheKey);
if (existingClient) {
return existingClient;
}
const client = new fhirClient.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
function createNullLogger() {
return {
info: () => {
},
warn: () => {
},
error: () => {
}
};
}
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;
}
// src/engine.ts
var hasInlineUrlConfig = (config) => {
if (!config) {
return false;
}
return typeof config.authType !== "undefined" || typeof config.username !== "undefined" || typeof config.password !== "undefined" || typeof config.fhirVersion !== "undefined" || typeof config.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.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.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.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.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.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.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.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.FhirSnapshotGenerator.create({
fpe,
fhirVersion: FHIR_VERSION,
cacheMode,
logger: this.getChildLogger("[snapshot-generator]")
});
const navigator = new structureNavigator.FhirStructureNavigator(generator, this.getChildLogger("[structure-navigator]"));
const terminologyRuntime = await fhirTerminologyRuntime.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, config) => {
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, config);
}
if (hasInlineUrlConfig(config)) {
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__default.default(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 fumifier.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",
"responses": {
"200": {
"description": "Server info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerInfo"
}
}
}
}
}
},
"post": {
"tags": [
"Evaluation"
],
"summary": "Evaluate ad-hoc expression",
"description": "Evaluate an ad-hoc FUME expression. The request body can be a JSON object with `fume` (required), `input`, and `contentType` fields.",
"operationId": "evaluateExpression",
"parameters": [
{
"$ref": "#/components/parameters/verbose"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"fume"
],
"properties": {
"fume": {
"type": "string",
"description": "FUME expression to evaluate",
"example": "{ resourceType: id }"
},
"input": {
"description": "Input data for the expression",
"example": {
"resourceType": "Patient",
"id": "123"
}
},
"contentType": {
"type": "string",
"description": "Content type for input conversion",
"example": "application/json"
}
}
},
"examples": {
"validRequest": {
"summary": "Valid request",
"value": {
"fume": "{ resourceType: id }",
"input": {
"resourceType": "Patient",
"id": "123"
},
"contentType": "application/json"
}
},
"invalidRequest": {
"summary": "Invalid request