UNPKG

fume-fhir-converter

Version:

FHIR-Utilized Mapping Engine - Community

1,353 lines (1,345 loc) 2.06 MB
'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