fhir-package-explorer
Version:
Explore and resolve FHIR conformance resources across package contexts
450 lines (444 loc) • 18.5 kB
JavaScript
;
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