UNPKG

fhir-package-explorer

Version:

Explore and resolve FHIR conformance resources across package contexts

450 lines (444 loc) 18.5 kB
'use strict'; var fhirPackageInstaller = require('fhir-package-installer'); var path = require('path'); var lruCache = require('lru-cache'); var fs = require('fs-extra'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var path__default = /*#__PURE__*/_interopDefault(path); var fs__default = /*#__PURE__*/_interopDefault(fs); // src/index.ts var sortPackages = (arr) => { return arr.slice().sort((a, b) => { const aKey = `${a.id}@${a.version}`; const bKey = `${b.id}@${b.version}`; return aKey < bKey ? -1 : aKey > bKey ? 1 : 0; }); }; var normalizePipedFilter = (filter) => { const newFilter = { ...filter }; const pipedKeys = ["url", "name", "id"]; for (const key of pipedKeys) { const val = filter[key]; if (typeof val === "string" && val.includes("|")) { const [left, right] = val.split("|"); newFilter[key] = left; newFilter.version = right; break; } } return newFilter; }; var matchesFilter = (entry, filter) => { for (const [key, value] of Object.entries(filter)) { if (key === "package") continue; if (entry[key] !== value) return false; } return true; }; var tryResolveDuplicates = async (matches, filter, fpi) => { if (filter.package) { const pkgIdentifier = await fpi.toPackageObject(filter.package); const filteredMatches = matches.filter((m) => m.__packageId === pkgIdentifier.id && m.__packageVersion === pkgIdentifier.version); if (filteredMatches.length === 1) return filteredMatches; } const isCorePackage = (packageId) => /^hl7\.fhir\.r\d+\.core$/.test(packageId); const isTerminologyPackage = (packageId) => /^hl7\.terminology\.r\d+$/.test(packageId); const isExtensionsPackage = (packageId) => /^hl7\.fhir\.uv\.extensions\.r\d+$/.test(packageId); const isImplicitPackage = (packageId) => isTerminologyPackage(packageId) || isExtensionsPackage(packageId); const isTerminologyResource = (resourceType) => ["ValueSet", "ConceptMap", "CodeSystem"].includes(resourceType); const extractFhirVersionFromImplicitPackageId = (packageId) => { const match = packageId.match(/\.r(\d+)$/); return match ? parseInt(match[1], 10) : 0; }; const compareSemver = (a, b) => { if (!a && !b) return 0; if (!a) return -1; if (!b) return 1; const parse = (v) => { const [core] = v.split("-"); const [major, minor, patch] = core.split(".").map(Number); return { major, minor, patch }; }; const aParts = parse(a); const bParts = parse(b); if (aParts.major !== bParts.major) return aParts.major - bParts.major; if (aParts.minor !== bParts.minor) return aParts.minor - bParts.minor; if (aParts.patch !== bParts.patch) return aParts.patch - bParts.patch; return 0; }; const coreMatches = matches.filter((m) => isCorePackage(m.__packageId)); const implicitMatches = matches.filter((m) => isImplicitPackage(m.__packageId)); if (implicitMatches.length > 0 && coreMatches.length > 0) { matches = implicitMatches; } else if (coreMatches.length === 1 && implicitMatches.length === 0) { return coreMatches; } else if (implicitMatches.length > 0 && coreMatches.length === 0) { matches = implicitMatches; } if (matches.length > 1 && matches.every((m) => isImplicitPackage(m.__packageId))) { const terminologyMatches = matches.filter((m) => isTerminologyPackage(m.__packageId)); const extensionsMatches = matches.filter((m) => isExtensionsPackage(m.__packageId)); if (terminologyMatches.length > 0 && extensionsMatches.length > 0) { if (filter.resourceType && isTerminologyResource(filter.resourceType)) { matches = terminologyMatches; } else { matches = extensionsMatches; } } } if (matches.length > 1 && matches.every((m) => isImplicitPackage(m.__packageId))) { matches.sort((a, b) => { const versionComparison = compareSemver(b.__packageVersion, a.__packageVersion); if (versionComparison !== 0) return versionComparison; return extractFhirVersionFromImplicitPackageId(b.__packageId) - extractFhirVersionFromImplicitPackageId(a.__packageId); }); return [matches[0]]; } const groupedByPkg = /* @__PURE__ */ new Map(); for (const entry of matches) { const pkg = entry.__packageId; const v = entry.version; if (!v || !/^\d+\.\d+\.\d+(-[\w.-]+)?$/.test(v)) return []; if (!groupedByPkg.has(pkg)) groupedByPkg.set(pkg, []); groupedByPkg.get(pkg).push(v); } if (groupedByPkg.size !== 1) return []; const [pkgId, versions] = Array.from(groupedByPkg.entries())[0]; const latest = versions.slice().sort(compareSemver).pop(); return matches.filter((m) => m.__packageId === pkgId && m.version === latest); }; var loadJson = async (filePath) => { return await fs__default.default.readJson(filePath); }; var fhirVersionMap = { "3.0.2": "STU3", "3.0": "STU3", "R3": "STU3", "STU3": "STU3", "4.0.1": "R4", "4.0": "R4", "R4": "R4", "4.3.0": "R4B", "4.3": "R4B", "R4B": "R4B", "5.0.0": "R5", "5.0": "R5", "R5": "R5" }; var fhirCorePackages = { "STU3": { id: "hl7.fhir.r3.core", version: "3.0.2" }, "R3": { id: "hl7.fhir.r3.core", version: "3.0.2" }, "R4": { id: "hl7.fhir.r4.core", version: "4.0.1" }, "R4B": { id: "hl7.fhir.r4b.core", version: "4.3.0" }, "R5": { id: "hl7.fhir.r5.core", version: "5.0.0" } }; var resolveFhirVersionToCorePackage = (fhirVersion) => { const fhirRelease = fhirVersionMap[fhirVersion]; if (!fhirRelease) { const supportedVersions = Object.keys(fhirVersionMap).join(", "); throw new Error(`Unsupported FHIR version: ${fhirVersion}. Supported versions: ${supportedVersions}`); } return fhirCorePackages[fhirRelease]; }; var getAllFastIndexKeys = (entry) => { const { __packageId, __packageVersion, resourceType, url, id, name, version, derivation } = entry; const keys = []; if (__packageId && __packageVersion && resourceType && id && derivation) keys.push(`pkg:${__packageId}#${__packageVersion}|resourceType:${resourceType}|id:${id}|derivation:${derivation}`); if (__packageId && __packageVersion && resourceType && url) keys.push(`pkg:${__packageId}#${__packageVersion}|resourceType:${resourceType}|url:${url}`); if (resourceType && url && version) keys.push(`resourceType:${resourceType}|url:${url}|version:${version}`); if (resourceType && url) keys.push(`resourceType:${resourceType}|url:${url}`); if (url && version) keys.push(`url:${url}|version:${version}`); if (url) keys.push(`url:${url}`); if (resourceType && name && version) keys.push(`resourceType:${resourceType}|name:${name}|version:${version}`); if (resourceType && id && version) keys.push(`resourceType:${resourceType}|id:${id}|version:${version}`); if (resourceType && name) keys.push(`resourceType:${resourceType}|name:${name}`); if (resourceType && id) keys.push(`resourceType:${resourceType}|id:${id}`); return keys; }; // src/index.ts var FhirPackageExplorer = class _FhirPackageExplorer { constructor(config) { this.contextPackages = []; this.normalizedRootPackages = []; this.dependencyRootByPackageKey = /* @__PURE__ */ new Map(); this.skipExamples = false; const { logger, registryUrl, registryToken, cachePath, skipExamples, contentCacheSize, indexCacheSize, fastIndexSize } = config || {}; this.fpi = new fhirPackageInstaller.FhirPackageInstaller({ logger, registryUrl, registryToken, cachePath, skipExamples }); this.logger = logger || { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } }; this.cachePath = this.fpi.getCachePath(); if (skipExamples) this.skipExamples = skipExamples; this.contentCache = new lruCache.LRUCache({ max: contentCacheSize ?? 500 }); this.indexCache = new lruCache.LRUCache({ max: indexCacheSize ?? 500 }); this.fastIndex = new lruCache.LRUCache({ max: fastIndexSize ?? 1e4 }); } static async create(config) { const instance = new _FhirPackageExplorer(config); let effectiveContext = config.context; if (config.fhirVersion) { await instance._loadContext(config.context); const hasCorePackage = instance.contextPackages.some( (pkg) => pkg.id.match(/^hl7\.fhir\.r\d+b?\.core$/) ); if (!hasCorePackage) { const corePackage = resolveFhirVersionToCorePackage(config.fhirVersion); instance.logger.warn?.( `No FHIR core package found in context. Auto-adding: ${corePackage.id}@${corePackage.version}` ); effectiveContext = [...config.context, corePackage]; await instance._loadContext(effectiveContext); } } else { await instance._loadContext(effectiveContext); } return instance; } getCachePath() { return this.cachePath; } getLogger() { return this.logger; } getContextPackages() { return this.contextPackages; } /** * Get the list of direct package dependencies for a given package. * @param pkg - The package to expand. Can be a string or a FhirPackageIdentifier object. * @returns - A promise that resolves to an array of FhirPackageIdentifier objects. */ async getDirectDependencies(pkg) { const pkgObj = typeof pkg === "string" ? await this.fpi.toPackageObject(pkg) : pkg; const dependencies = await this.fpi.getDependencies(pkgObj, { rootPackage: this._getDependencyRoot(pkgObj) }); return Object.entries(dependencies).map(([id, version]) => ({ id, version })); } /** * Expands the package into a list of packages including all transitive dependencies. * @param pkg - The package to expand. Can be a string or a FhirPackageIdentifier object. * @returns - A promise that resolves to an array of FhirPackageIdentifier objects representing the expanded packages. */ async expandPackageDependencies(pkg) { const pkgObj = typeof pkg === "string" ? await this.fpi.toPackageObject(pkg) : pkg; return sortPackages(await this._collectDependencyObjects(pkgObj)); } async lookup(filter = {}) { const meta = await this.lookupMeta(filter); const results = await Promise.all(meta.map(async (entry) => { const filePath = await this._getFilePath(entry); if (this.contentCache.has(filePath)) return this.contentCache.get(filePath); const content = await loadJson(filePath); const enriched = { __packageId: entry.__packageId, __packageVersion: entry.__packageVersion, __filename: entry.filename, ...content }; this.contentCache.set(filePath, enriched); return enriched; })); return results; } async lookupMeta(filter = {}) { const normalizedFilter = normalizePipedFilter(filter); const pkgIdentifiers = this.contextPackages; let allowedPackages = void 0; if (normalizedFilter.package) { const scopedPackage = await this.fpi.toPackageObject(normalizedFilter.package); allowedPackages = await this._collectDependencies(scopedPackage); } const resultMap = /* @__PURE__ */ new Map(); for (const pkg of pkgIdentifiers) { const pkgKey = `${pkg.id}#${pkg.version}`; if (allowedPackages && !allowedPackages.has(pkgKey)) continue; let index = this.indexCache.get(pkgKey); if (!index) { await this.fpi.install(pkg); const rawPkgIndex = await this.fpi.getPackageIndexFile(pkg); const rawIndex = rawPkgIndex.files ?? []; const newIndex = rawIndex.map((file) => ({ ...file, __packageId: pkg.id, __packageVersion: pkg.version })); this.indexCache.set(pkgKey, newIndex); this._buildFastIndex(newIndex); index = newIndex; } const fastKeys = getAllFastIndexKeys(normalizedFilter); const fastCandidates = fastKeys.flatMap((k) => this.fastIndex.get(k) ?? []); const candidates = fastCandidates.length > 0 ? fastCandidates : index; for (const entry of candidates) { const entryPkgKey = `${entry.__packageId}#${entry.__packageVersion}`; if (allowedPackages && !allowedPackages.has(entryPkgKey)) continue; if (!matchesFilter(entry, normalizedFilter)) continue; const compositeKey = `${entry.filename}|${entry.__packageId}|${entry.__packageVersion}`; if (!resultMap.has(compositeKey)) { resultMap.set(compositeKey, entry); } } } return Array.from(resultMap.values()); } async resolve(filter = {}) { const matches = await this.lookup(filter); if (matches.length === 0) throw new Error(`No matching resource found with filter: ${JSON.stringify(filter)}`); if (matches.length > 1) { const candidates = await tryResolveDuplicates(matches, filter, this.fpi); if (candidates.length !== 1) { const matchInfo = matches.map((m) => `${m.__packageId}@${m.__packageVersion}`).join(", "); throw new Error(`Multiple matching resources found with filter: ${JSON.stringify(filter)}. Found in packages: ${matchInfo}`); } return candidates[0]; } return matches[0]; } async resolveMeta(filter = {}) { const matches = await this.lookupMeta(filter); if (matches.length === 0) throw new Error(`No matching resource found with filter: ${JSON.stringify(filter)}`); if (matches.length > 1) { const candidates = await tryResolveDuplicates(matches, filter, this.fpi); if (candidates.length !== 1) { const matchInfo = matches.map((m) => `${m.__packageId}@${m.__packageVersion}`).join(", "); throw new Error(`Multiple matching resources found with filter: ${JSON.stringify(filter)}. Found in packages: ${matchInfo}`); } return candidates[0]; } return matches[0]; } /** * Get the manifest (package.json) for a given FHIR package. * Returns the parsed manifest object for the specified package, or throws if not found. * * @param pkg - The package to fetch the manifest for (string or FhirPackageIdentifier). * @returns A promise that resolves to the manifest (package.json) object for the package. */ async getPackageManifest(pkg) { const meta = await this.fpi.getManifest(pkg); if (!meta) throw new Error(`Failed to fetch manifest (package.json) for package: ${String(pkg)}`); return meta; } async _loadContext(context) { const rootMap = /* @__PURE__ */ new Map(); for (const entry of context) { const pkg = await this.fpi.toPackageObject(entry); rootMap.set(`${pkg.id}#${pkg.version}`, pkg); } const initialRoots = Array.from(rootMap.values()); const rootClosures = /* @__PURE__ */ new Map(); const keyToPkg = /* @__PURE__ */ new Map(); for (const root of initialRoots) { await this.fpi.install(root); const closurePackages = await this._collectDependencyObjects(root); const closure = new Set(closurePackages.map((pkg) => `${pkg.id}#${pkg.version}`)); rootClosures.set(`${root.id}#${root.version}`, closure); for (const pkg of closurePackages) { keyToPkg.set(`${pkg.id}#${pkg.version}`, pkg); } } const redundant = /* @__PURE__ */ new Set(); const allRootKeys = Array.from(rootClosures.keys()); for (const [rootKey, closure] of rootClosures.entries()) { for (const otherKey of allRootKeys) { if (rootKey === otherKey) continue; if (closure.has(otherKey)) { redundant.add(otherKey); } } } let minimalRoots = initialRoots.filter((r) => !redundant.has(`${r.id}#${r.version}`)); if (minimalRoots.length === 0 && initialRoots.length > 0) { minimalRoots = [sortPackages(initialRoots)[0]]; } const finalContextMap = /* @__PURE__ */ new Map(); for (const mr of minimalRoots) { const closure = rootClosures.get(`${mr.id}#${mr.version}`); for (const key of closure) { const pkgObj = keyToPkg.get(key); if (pkgObj) finalContextMap.set(key, pkgObj); } } this.normalizedRootPackages = sortPackages(minimalRoots); this.contextPackages = sortPackages(Array.from(finalContextMap.values())); this.dependencyRootByPackageKey.clear(); for (const root of this.normalizedRootPackages) { const closure = rootClosures.get(`${root.id}#${root.version}`); if (!closure) continue; for (const key of closure) { if (!this.dependencyRootByPackageKey.has(key)) { this.dependencyRootByPackageKey.set(key, root); } } } } _getDependencyRoot(pkg) { return this.dependencyRootByPackageKey.get(`${pkg.id}#${pkg.version}`) ?? pkg; } async _collectDependencies(pkg) { const visited = /* @__PURE__ */ new Set(); const rootPackage = this._getDependencyRoot(pkg); const visit = async (p) => { const key = `${p.id}#${p.version}`; if (visited.has(key)) return; visited.add(key); const deps = await this.fpi.getDependencies(p, { rootPackage }); for (const [id, version] of Object.entries(deps || {})) { if (this.skipExamples && id.includes("examples")) continue; await visit({ id, version }); } }; await visit(pkg); return visited; } async _collectDependencyObjects(pkg) { const keys = await this._collectDependencies(pkg); return sortPackages(Array.from(keys).map((key) => { const [id, version] = key.split("#", 2); return { id, version }; })); } async _getFilePath(entry) { const dir = await this.fpi.getPackageDirPath({ id: entry.__packageId, version: entry.__packageVersion }); return path__default.default.join(dir, "package", entry.filename); } _buildFastIndex(index) { for (const file of index) { for (const key of getAllFastIndexKeys(file)) { const entries = this.fastIndex.get(key) ?? []; entries.push(file); this.fastIndex.set(key, entries); } } } /** * Get the normalized minimal set of root packages from the context. * Returns only the root packages that are not dependencies of other root packages, * effectively removing redundant entries from the originally provided context. * * @returns An array of FhirPackageIdentifier objects representing the minimal root packages. */ getNormalizedRootPackages() { return this.normalizedRootPackages; } }; exports.FhirPackageExplorer = FhirPackageExplorer; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map