UNPKG

fhir-snapshot-generator

Version:
979 lines (953 loc) 38.7 kB
'use strict'; var path = require('path'); 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/utils/element/applyDiffs.ts var applySingleDiff = (elements, diffElement) => { const index = elements.findIndex((el) => el.id === diffElement.id); if (index === -1) { throw new Error(`Element with id "${diffElement.id}" not found`); } const baseElement = { ...elements[index] }; const mergedElement = mergeElement(baseElement, diffElement); if (mergedElement.sliceName && !mergedElement.id.endsWith(`:${mergedElement.sliceName}`)) { delete mergedElement.sliceName; } const updatedElements = [...elements.slice(0, index), mergedElement, ...elements.slice(index + 1)]; return updatedElements; }; var rewrite = (original, kind, pathRewriteMap) => { for (const [illegalPrefix, rewrite2] of pathRewriteMap.entries()) { if (original === illegalPrefix || original.startsWith(illegalPrefix + ".")) { return original.replace(illegalPrefix, rewrite2[kind]); } } return original; }; var applyDiffs = async (elements, diffs, fetcher, logger) => { let updatedElements = [...elements]; const pathRewriteMap = /* @__PURE__ */ new Map(); if (updatedElements[0].extension) { delete updatedElements[0].extension; } if (diffs.length === 0) return updatedElements; for (const diff of diffs) { if (!elementExists(updatedElements, diff.id)) { updatedElements = await ensureBranch(updatedElements, diff.id, fetcher, logger, pathRewriteMap); } const rewrittenDiff = { ...diff, id: rewrite(diff.id, "id", pathRewriteMap), path: rewrite(diff.path, "path", pathRewriteMap) }; updatedElements = applySingleDiff(updatedElements, rewrittenDiff); } return updatedElements; }; // src/utils/element/ensureChild.ts var ensureChild = async (elements, parentId, childId, fetcher, pathRewriteMap) => { const parentElementBlock = elements.filter((element) => element.id === parentId || element.id.startsWith(`${parentId}.`)); if (parentElementBlock.length === 0) { throw new Error(`Parent element '${parentId}' not found in the working snapshot array`); } let parentNode = toTree(parentElementBlock); if (isNodeSliceable(parentNode)) { parentNode = parentNode.children[0]; } const isExpanded = parentNode.children.length > 0; if (!isExpanded) { parentNode = await expandNode(parentNode, fetcher); elements = injectElementBlock(elements, parentId, fromTree(parentNode)); } const [elementName, sliceName] = childId.split(":"); const childElement = parentNode.children.find((element) => element.id.endsWith(`.${elementName}`)); if (!childElement) { const match = findMonopolyShortcutTarget(parentId, elementName, parentNode.children); if (match) { const canonicalId = `${parentId}.${match.rewrittenSegment}`; const elementDefinition = elements.find((e) => e.id === canonicalId); const canonicalPath = elementDefinition?.path ?? canonicalId; pathRewriteMap.set(`${parentId}.${elementName}`, { id: canonicalId, path: canonicalPath }); const virtualDiff = { id: canonicalId, path: canonicalPath, type: [{ code: match.type }] }; elements = applySingleDiff(elements, virtualDiff); return elements; } throw new Error(`Element '${childId}' is illegal under '${parentId}'.`); } if (!sliceName) return elements; if (!isNodeSliceable(childElement)) { const aliasId = `${childElement.id}:${sliceName}`; pathRewriteMap.set(aliasId, { id: childElement.id, path: childElement.path }); return elements; } const slice = childElement.children.find((slice2) => slice2.sliceName === sliceName); if (slice) { return elements; } if (!slice) { if (elementName.endsWith("[x]") && childElement.children[0].definition?.type?.length === 1) { const onlyType = childElement.children[0].definition.type[0].code; const monopolySliceName = `${elementName.slice(0, -3)}${initCap(onlyType)}`; if (sliceName === monopolySliceName) { const aliasId = `${childElement.id}:${sliceName}`; const aliasPath = childElement.path; pathRewriteMap.set(aliasId, { id: childElement.id, path: aliasPath }); return elements; } } const headSlice = childElement.children[0]; const newId = `${headSlice.id}:${sliceName}`; const newSlice = rewriteNodePaths(headSlice, newId, headSlice.id); newSlice.nodeType = "slice"; delete newSlice.definition?.slicing; delete newSlice.definition?.mustSupport; newSlice.sliceName = sliceName; if (newSlice.definition) { newSlice.definition.sliceName = sliceName; } childElement.children.push(newSlice); elements = injectElementBlock(elements, childElement.id, fromTree(childElement)); } return elements; }; // src/utils/element/ensureBranch.ts var ensureBranch = async (elements, targetElementId, fetcher, logger, pathRewriteMap) => { const idSegments = targetElementId.split("."); const rootId = idSegments[0]; let updatedElements = elements; if (elements[0].id !== rootId) { throw new Error(`Root element '${rootId}' not found in the working snapshot array`); } if (rootId === targetElementId) { return updatedElements; } let canonicalParentId = rootId; for (let i = 1; i < idSegments.length; i++) { const rawChildSegment = idSegments[i]; const rewrite2 = pathRewriteMap.get(canonicalParentId); if (rewrite2) { canonicalParentId = rewrite2.id; } updatedElements = await ensureChild( updatedElements, canonicalParentId, rawChildSegment, fetcher, pathRewriteMap ); canonicalParentId = `${canonicalParentId}.${rawChildSegment}`; } return updatedElements; }; // src/utils/element/toNodeType.ts var toNodeType = (element) => { if (element.id.endsWith("[x]")) { return "poly"; } if (element.sliceName) { if (element.slicing) return "resliced"; return "slice"; } const base = element.base; if (base?.max && (base.max === "*" || parseInt(base.max) > 1)) { return "array"; } return "element"; }; // src/utils/element/injectElementBlock.ts var injectElementBlock = (elements, injectionPoint, elementBlock) => { const index = elements.findIndex((el) => el.id === injectionPoint); if (index === -1) throw new Error(`Element with id "${injectionPoint}" not found`); const before = elements.slice(0, index); const after = elements.slice(index + 1).filter((element) => !element.id.startsWith(`${injectionPoint}.`) && !element.id.startsWith(`${injectionPoint}:`)); const results = [...before, ...elementBlock, ...after]; return results; }; // src/utils/element/mergeElement.ts var mergeElement = (base, diff) => { if (diff.id !== base.id) { throw new Error(`Element ID mismatch. Tried to apply "${diff.id}" onto "${base.id}".`); } const mergedElement = { ...base }; for (const key of Object.keys(diff)) { if (["constraint", "condition", "mapping"].includes(key)) { const baseArr = base[key] || []; const diffArr = diff[key] || []; if (key === "constraint") { mergedElement.constraint = [...baseArr, ...diffArr]; } else if (key === "condition") { const ids = Array.from(/* @__PURE__ */ new Set([...baseArr, ...diffArr])); mergedElement.condition = ids; } else { const seen = /* @__PURE__ */ new Set(); const serialize = (obj) => JSON.stringify(Object.entries(obj).sort()); const combined = [...baseArr, ...diffArr]; mergedElement.mapping = combined.filter((m) => { const s = serialize(m); return seen.has(s) ? false : seen.add(s); }); } } else if (key !== "id" && key !== "path") { if (diff[key] !== void 0) { mergedElement[key] = diff[key]; } } } return mergedElement; }; // src/utils/element/elementExists.ts var elementExists = (elements, targetElementId) => { return elements.some((element) => element.id === targetElementId); }; // src/utils/element/rewriteElementPaths.ts var rewriteElementPaths = (elements, newPrefix, oldPrefix) => { const oldPrefixDot = oldPrefix.endsWith(".") ? oldPrefix : oldPrefix + "."; const newPrefixDot = newPrefix.endsWith(".") ? newPrefix : newPrefix + "."; const removeSlices = (elementIdPart) => { const segments = elementIdPart.split("."); const cleanedSegments = segments.map((segment) => { const sliceIndex = segment.indexOf(":"); return sliceIndex !== -1 ? segment.slice(0, sliceIndex) : segment; }); return cleanedSegments.join("."); }; const replaceId = (str) => str === oldPrefix ? newPrefix : str.startsWith(oldPrefixDot) ? newPrefixDot + str.slice(oldPrefixDot.length) : str; const replacePath = (elementPath) => { const newPathPrefix = removeSlices(newPrefixDot); const oldPathPrefix = removeSlices(oldPrefixDot); return elementPath === oldPathPrefix ? newPathPrefix : elementPath.startsWith(oldPathPrefix) ? newPathPrefix + elementPath.slice(oldPathPrefix.length) : elementPath; }; return elements.map((el) => ({ ...el, id: replaceId(el.id), path: replacePath(el.path) })); }; // src/utils/element/migrateElements.ts var markdownKeys = ["definition", "comment", "requirements", "meaningWhenMissing"]; var ignoredExtensions = [ "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm", "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm-no-warnings", "http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy", "http://hl7.org/fhir/StructureDefinition/structuredefinition-interface", "http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version", "http://hl7.org/fhir/StructureDefinition/structuredefinition-applicable-version", "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", "http://hl7.org/fhir/StructureDefinition/structuredefinition-codegen-super", "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", "http://hl7.org/fhir/StructureDefinition/structuredefinition-summary", "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", "http://hl7.org/fhir/StructureDefinition/replaces", "http://hl7.org/fhir/StructureDefinition/resource-approvalDate", "http://hl7.org/fhir/StructureDefinition/resource-effectivePeriod", "http://hl7.org/fhir/StructureDefinition/resource-lastReviewDate" ]; var replaceRelativeLinks = (markdown) => { return markdown.replace( /\[([^\]]+)\]\((?!https?:\/\/)([^)]+)\)/g, (_match, text, url) => `[${text}](http://hl7.org/fhir/${url})` ); }; var filterExtensions = (extensions) => { const result = extensions.filter( (ext) => !ignoredExtensions.includes(ext.url) ); return result.length > 0 ? result : void 0; }; var addSourceToConstraints = (constraints, sourceUrl) => { return constraints.map((constraint) => ({ source: sourceUrl, ...constraint })); }; var migrateElements = (elements, sourceUrl) => { return elements.map((el, index) => { const updated = { ...el }; if (index === 0 && el.extension) { const filteredExtensions = filterExtensions(el.extension); if (filteredExtensions) { updated.extension = filteredExtensions; } else { delete updated.extension; } } if (sourceUrl.startsWith("http://hl7.org/fhir")) { for (const key of markdownKeys) { const value = el[key]; if (typeof value === "string") { updated[key] = replaceRelativeLinks(value); } } } if (el.constraint) { updated.constraint = addSourceToConstraints(el.constraint, sourceUrl); } return updated; }); }; // src/utils/element/findMonopolyShortcutTarget.ts var findMonopolyShortcutTarget = (parentId, missingSegment, elements) => { const prefix = `${parentId}.`; const childElements = elements.filter( (el) => el.id.startsWith(prefix) && el.id !== parentId && el.id.endsWith("[x]") ); for (const el of childElements) { const idSegment = el.id.slice(prefix.length); const base = idSegment.slice(0, -3); for (const type of el?.children[0]?.definition?.type ?? []) { const shortcut = base + initCap(type.code); if (shortcut === missingSegment) { return { rewrittenSegment: `${base}[x]`, type: type.code }; } } } return null; }; // src/utils/tree/expandNode.ts var expandNode = async (node, fetcher) => { if (isNodeSliceable(node)) { throw new Error(`Node '${node.id}' is sliceable. Expand node must be called on a specific slice, headslice or a non-sliceable node.`); } if (node.children.length > 0) return node; if (!node.definition) { throw new Error(`Node '${node.id}' has no ElementDefinition. Cannot expand.`); } const definition = node.definition; if ((!definition.type || definition.type.length === 0) && !definition.contentReference) { throw new Error(`Node '${node.id}' has no type or contentReference defined. Cannot expand.`); } let snapshotElements = []; if (definition.contentReference) { snapshotElements = await fetcher.getContentReference(definition.contentReference); if (!snapshotElements || snapshotElements.length === 0) { throw new Error(`Snapshot for contentReference '${definition.contentReference}' is empty or missing.`); } delete definition.contentReference; } else if (definition.type && definition.type.length > 1) { snapshotElements = await fetcher.getBaseType("Element"); if (!snapshotElements || snapshotElements.length === 0) { throw new Error("Snapshot for base type 'Element' is empty or missing."); } } else if (definition.type && definition.type.length === 1) { const elementType = definition.type[0]; if (elementType.profile && elementType.profile.length === 1) { const url = elementType.profile[0]; snapshotElements = await fetcher.getByUrl(url); if (!snapshotElements || snapshotElements.length === 0) { throw new Error(`Snapshot for type '${url}' is empty or missing.`); } } else { const id = elementType.code; if (id.startsWith("http://hl7.org/fhirpath/System.")) { throw new Error(`Cannot expand node '${node.id}', type '${id}' can never have children.`); } snapshotElements = await fetcher.getBaseType(id); if (!snapshotElements || snapshotElements.length === 0) { throw new Error(`Snapshot for type '${id}' is empty or missing.`); } } } if (!snapshotElements || snapshotElements.length === 0) { throw new Error(`Error expanding node '${node.id}' - the snapshot contains no elements.`); } const oldPrefix = snapshotElements[0].id; const newPrefix = node.id; const rewrittenElements = rewriteElementPaths(snapshotElements, newPrefix, oldPrefix); const expandedSubtree = toTree(rewrittenElements); const clonedNode = { ...node, children: expandedSubtree.children }; return clonedNode; }; // src/utils/tree/fromTree.ts var fromTree = (tree) => { const result = []; function visit(node) { if (["element", "slice", "headslice"].includes(node.nodeType)) { if (!node.definition) { throw new Error(`Node ${node.id} of type ${node.nodeType} is missing its definition`); } result.push(node.definition); } for (const child of node.children) { visit(child); } } visit(tree); return result; }; // src/utils/tree/toTree.ts var toTree = (elements) => { if (elements.length === 0) { throw new Error("Element array is empty"); } const idToNode = /* @__PURE__ */ new Map(); const makeSegments = (fullId) => { const idSegments = fullId.split("."); const pathSegments = idSegments.map((segment) => segment.split(":")[0]); return { idSegments, pathSegments }; }; const createNode = (element, forceNodeType) => { const { idSegments, pathSegments } = makeSegments(element.id); const nodeType = forceNodeType || toNodeType(element); const node = { id: element.id, path: element.path, idSegments, pathSegments, nodeType, children: [] }; if (isNodeSliceable(node)) { const headSlice = { id: element.id, path: element.path, idSegments, pathSegments, nodeType: "headslice", definition: element, children: [] }; node.children.push(headSlice); } else { node.definition = element; if (element.sliceName) { node.sliceName = element.sliceName; } } return node; }; const rootElement = elements[0]; const rootNode = createNode(rootElement, "element"); idToNode.set(rootNode.id, rootNode); for (let i = 1; i < elements.length; i++) { const element = elements[i]; const { idSegments } = makeSegments(element.id); const lastSegment = idSegments[idSegments.length - 1]; const isSlice = lastSegment.includes(":"); const lastSegmentWithoutSlice = isSlice ? lastSegment.split(":")[0] : lastSegment; const parentPath = isSlice ? idSegments.slice(0, -1).join(".") + `.${lastSegmentWithoutSlice}` : idSegments.slice(0, -1).join("."); const parentNode = idToNode.get(parentPath); if (!parentNode) { throw new Error(`Parent node not found for element ${element.id}`); } const newNode = createNode(element); if (isNodeSliceable(parentNode)) { if (isSlice) { parentNode.children.push(newNode); } else { parentNode.children[0].children.push(newNode); } } else { parentNode.children.push(newNode); } idToNode.set(newNode.id, newNode); } return rootNode; }; // src/utils/tree/rewriteNodePaths.ts var rewriteNodePaths = (node, newPrefix, oldPrefix) => { const flattened = fromTree(node); const rewritten = rewriteElementPaths(flattened, newPrefix, oldPrefix); const newNode = toTree(rewritten); return newNode; }; // src/utils/tree/isNodeSliceable.ts var isNodeSliceable = (node) => { return ["array", "poly", "resliced"].includes(node.nodeType); }; // src/utils/misc/logger.ts var defaultLogger = { info: (msg) => console.log(msg), warn: (msg) => console.warn(msg), error: (msg) => console.error(msg) }; var defaultPrethrow = (msg) => { if (msg instanceof Error) { return msg; } const error = new Error(msg); return error; }; var customPrethrower = (logger) => { return (msg) => { if (msg instanceof Error) { logger.error(msg); return msg; } const error = new Error(msg); logger.error(error); return error; }; }; // src/utils/misc/resolveFhirVersion.ts var fhirVersionMap = { "3.0.2": "STU3", "3.0": "STU3", "R3": "STU3", "4.0.0": "R4", "4.0.1": "R4", "4.0": "R4", "4.3.0": "R4B", "4.3": "R4B", "5.0.0": "R5", "5.0": "R5" }; var fhirCorePackages = { "STU3": "hl7.fhir.r3.core@3.0.2", "R3": "hl7.fhir.r3.core@3.0.2", "R4": "hl7.fhir.r4.core@4.0.1", "R4B": "hl7.fhir.r4b.core@4.3.0", "R5": "hl7.fhir.r5.core@5.0.0" }; var resolveFhirVersion = (version2, toPackage) => { const canonicalVersion = fhirVersionMap[version2] || version2; const corePackage = fhirCorePackages[canonicalVersion]; if (!corePackage) { throw new Error(`Unsupported FHIR version: ${version2}. Supported versions are: ${Object.keys(fhirCorePackages).join(", ")}`); } if (toPackage) { const [id, version3] = corePackage.split("@"); return { id, version: version3 }; } return canonicalVersion; }; // src/utils/misc/resolveBasePackage.ts var findCorePackage = async (pkg, fpe) => { if (Object.values(fhirCorePackages).includes(`${pkg.id}@${pkg.version}`)) { return [{ id: pkg.id, version: pkg.version }]; } const deps = await fpe.getDirectDependencies(pkg); return deps.filter((dep) => Object.values(fhirCorePackages).includes(`${dep.id}@${dep.version}`)); }; var resolveBasePackage = async (packageId, packageVersion, fpe, logger) => { const corePackages = await findCorePackage({ id: packageId, version: packageVersion }, fpe); if (corePackages.length === 0) { logger.warn(`No base FHIR package dependency found for ${packageId}@${packageVersion}.`); const pkgManifest = await fpe.getPackageManifest({ id: packageId, version: packageVersion }); if (pkgManifest["fhir-version-list"] && !pkgManifest.fhirVersions) { pkgManifest.fhirVersions = pkgManifest["fhir-version-list"]; } if (pkgManifest.fhirVersions && pkgManifest.fhirVersions.length > 0) { pkgManifest.fhirVersions.map((fhirVersion) => { logger.info(`Resolving core package for FHIR version ${fhirVersion}`); const corePackageId = resolveFhirVersion(fhirVersion, true); if (corePackageId) { const { id, version: version3 } = corePackageId; corePackages.push( { id, version: version3 } ); } else { logger.warn(`Unknown FHIR version ${version2} in package ${packageId}@${packageVersion}.`); } }); if (corePackages.length === 0) return void 0; } } if (corePackages.length > 1) { logger.warn(`Multiple base FHIR packages found for ${packageId}@${packageVersion}: ${corePackages.map((pkg) => `${pkg.id}@${pkg.version}`).join(", ")}.`); return void 0; } const version2 = corePackages[0].id === "hl7.fhir.r4.core" && corePackages[0].version === "4.0.0" ? "4.0.1" : corePackages[0].version; return `${corePackages[0].id}@${version2}`; }; // src/utils/misc/definitionFetcher.ts var DefinitionFetcher5 = class { constructor(sourcePackage, corePackage, fpe, snapshotFetcher) { // A map to cache previously fetched element arrays for base types, profiles and contentReference. this.elementCache = /* @__PURE__ */ new Map(); this.sourcePackage = sourcePackage; this.corePackage = corePackage; this.snapshotFetcher = snapshotFetcher; this.fpe = fpe; } /** * Get the definition for one of the base FHIR types. * @param type The type ID (e.g., "CodeableConcept", "Quantity", etc.). */ async getBaseType(type) { let elements = this.elementCache.get(type); if (elements) { return elements; } const definition = await this.fpe.resolve({ resourceType: "StructureDefinition", id: type, package: this.corePackage, derivation: ["Element", "Resource"].includes(type) ? void 0 : "specialization" }); if (!definition) { throw new Error(`FHIR type '${type}' not found in base package '${this.corePackage}'`); } if (!definition.snapshot || !definition.snapshot.element || definition.snapshot.element.length === 0) { throw new Error(`FHIR type '${type}' in base package '${this.corePackage.id}@${this.corePackage.version}' does not have a snapshot`); } elements = migrateElements(definition.snapshot.element, definition.url); this.elementCache.set(type, elements); return elements; } /** * Get the structure of a contentReference element. * @param identifier The identifier of the contentReference (e.g., "#Observation.referenceRange"). */ async getContentReference(identifier) { if (!identifier.startsWith("#")) { throw new Error(`Invalid contentReference identifier '${identifier}'. Must start with '#'`); } const elements = this.elementCache.get(identifier); if (elements) { return elements; } const elementId = identifier.substring(1); const baseType = elementId.split(".")[0]; const allElements = await this.getBaseType(baseType); const matchingElements = allElements.filter((e) => e?.id === elementId || String(e?.id).startsWith(elementId + ".")); if (matchingElements.length === 0) { throw new Error(`No matching elements found for contentReference '${identifier}'`); } this.elementCache.set(identifier, matchingElements); return matchingElements; } /** * When a profile references a type using a URL, the target may be either a profile or a base type. * This method resolves the URL, and if it is a base type (derivation=specialization), returns its snapshot. * If it is a profile, it returns the snapshot of the profile using the injected snapshotFetcher(). * The snapshotFetcher is expected to return a pre-generated snapshot from the cache or generate a new one. * @param url Canonical URL of the type or profile. * @returns The snapshot elements of the resolved type or profile. */ async getByUrl(url) { const elements = this.elementCache.get(url); if (elements) { return elements; } const metadata = await this.fpe.resolve({ resourceType: "StructureDefinition", url, package: this.sourcePackage }); if (!metadata) { throw new Error(`StructureDefinition '${url}' not found in package '${this.sourcePackage.id}@${this.sourcePackage.version}'`); } if (metadata.derivation === "specialization") { const sd = await this.fpe.resolve({ filename: metadata.filename, package: { id: metadata.__packageId, version: metadata.__packageVersion } }); const elements2 = migrateElements(sd.snapshot?.element, url); if (!elements2 || elements2.length === 0) { throw new Error(`StructureDefinition '${url}' does not have a snapshot`); } this.elementCache.set(url, elements2); return elements2; } if (metadata?.derivation === "constraint") { const elements2 = migrateElements(await this.snapshotFetcher(url), url); if (!elements2 || elements2.length === 0) { throw new Error(`Profile '${url}' does not have a snapshot`); } this.elementCache.set(url, elements2); return elements2; } throw new Error(`StructureDefinition '${url}' is neither a base type nor a profile`); } }; // src/utils/misc/initCap.ts var initCap = (s) => s.charAt(0).toUpperCase() + s.slice(1); // package.json var version = "2.0.0"; // src/utils/misc/getVersionedCacheDir.ts var versionedCacheDir = `v${version.split(".").slice(0, 2).join(".")}.x`; var FhirSnapshotGenerator = class _FhirSnapshotGenerator { // cache for resolved base packages constructor(fpe, cacheMode, fhirVersion, logger) { this.resolvedBasePackages = /* @__PURE__ */ new Map(); this.logger = logger; this.prethrow = customPrethrower(this.logger); this.cacheMode = cacheMode; this.fhirVersion = fhirVersion; this.fhirCorePackage = resolveFhirVersion(fhirVersion, true); this.fpe = fpe; this.cachePath = fpe.getCachePath(); } /** * Creates a new instance of the FhirSnapshotGenerator class. * @param config - the configuration object including the FhirPackageExplorer instance * @returns - a promise that resolves to a new instance of the FhirSnapshotGenerator class */ static async create(config) { const logger = config.logger || defaultLogger; const prethrow = config.logger ? customPrethrower(logger) : defaultPrethrow; try { const { fpe } = config; if (!fpe) { throw new Error("FhirPackageExplorer instance is required in config.fpe"); } const cacheMode = config.cacheMode || "lazy"; const fhirVersion = resolveFhirVersion(config.fhirVersion); const fsg = new _FhirSnapshotGenerator(fpe, cacheMode, fhirVersion, logger); let precache = false; if (cacheMode === "rebuild") { precache = true; const packageList = fpe.getContextPackages().map((pkg) => path__default.default.join(fpe.getCachePath(), `${pkg.id}#${pkg.version}`, ".fsg.snapshots", versionedCacheDir)); for (const snapshotCacheDir of packageList) { if (await fs__default.default.exists(snapshotCacheDir)) { fs__default.default.removeSync(snapshotCacheDir); } } } if (cacheMode === "ensure") precache = true; if (precache) { logger.info(`Pre-caching snapshots in '${cacheMode}' mode...`); const allSds = await fpe.lookupMeta({ resourceType: "StructureDefinition", derivation: "constraint" }); const errors = []; for (const sd of allSds) { const { filename, __packageId: packageId, __packageVersion: packageVersion, url } = sd; try { await fsg.ensureSnapshotCached(filename, packageId, packageVersion); } catch (e) { errors.push( `Failed to ${cacheMode} snapshot for '${url}' in package '${packageId}@${packageVersion}': ${e instanceof Error ? e.message : String(e)}` ); } } if (errors.length > 0) { logger.error(`Errors during pre-caching snapshots (${errors.length} total): ${errors.join("\n")}`); } else { logger.info(`Pre-caching snapshots in '${cacheMode}' mode completed successfully.`); } } return fsg; } catch (e) { throw prethrow(e); } } getLogger() { return this.logger; } getCachePath() { return this.cachePath; } getCacheMode() { return this.cacheMode; } getFhirVersion() { return this.fhirVersion; } getFpe() { return this.fpe; } /** * Get the core FHIR package for a specific FHIR package. * Will try to resolve the core package based on the direct dependencies of the source package or its fhirVersions array. * Defaults to the FHIR package of this instance's fhirVersion if no base package can be determined. * @param sourcePackage The source package identifier (e.g., { id: 'hl7.fhir.us.core', version: '6.1.0' }). * @returns The core FHIR package identifier (e.g., { id: 'hl7.fhir.r4.core', version: '4.0.1' }). */ async getCorePackage(sourcePackage) { let baseFhirPackage = this.resolvedBasePackages.get(`${sourcePackage.id}@${sourcePackage.version}`); if (!baseFhirPackage) { try { baseFhirPackage = await resolveBasePackage(sourcePackage.id, sourcePackage.version, this.fpe, this.logger); } catch (e) { this.logger.warn(`Failed to resolve base FHIR package for '${sourcePackage.id}@${sourcePackage.version}': ${e instanceof Error ? e.message : String(e)}`); } } if (!baseFhirPackage) { this.logger.warn(`Defaulting to core package ${this.fhirCorePackage.id}@${this.fhirCorePackage.version} for resolving FHIR types within '${sourcePackage.id}@${sourcePackage.version}'.`); baseFhirPackage = `${this.fhirCorePackage.id}@${this.fhirCorePackage.version}`; } if (!baseFhirPackage) { throw new Error(`No base FHIR package found for '${sourcePackage.id}@${sourcePackage.version}'.`); } this.resolvedBasePackages.set(`${sourcePackage.id}@${sourcePackage.version}`, baseFhirPackage); const [id, version2] = baseFhirPackage.split("@"); return { id, version: version2 }; } /** * Get an original StructureDefinition from a specific package by filename. * After resolving the metadata using any other identifier (id, url, name), filename (combind with the package id and version) * is the most reliable way to get to a specific StructureDefinition, and is also used as the basis for the cache. */ async getStructureDefinitionByFileName(filename, packageId, packageVersion) { return await this.fpe.resolve({ filename, package: { id: packageId, version: packageVersion } }); } getCacheFilePath(filename, packageId, packageVersion) { return path__default.default.join(this.cachePath, `${packageId}#${packageVersion}`, ".fsg.snapshots", versionedCacheDir, filename); } /** * Try to get an existing cached StructureDefinition snapshot. If not found, return undefined. */ async getSnapshotFromCache(filename, packageId, packageVersion) { const cacheFilePath = this.getCacheFilePath(filename, packageId, packageVersion); if (await fs__default.default.exists(cacheFilePath)) { return await fs__default.default.readJSON(cacheFilePath); } else { return void 0; } } async saveSnapshotToCache(filename, packageId, packageVersion, snapshot) { const cacheFilePath = this.getCacheFilePath(filename, packageId, packageVersion); await fs__default.default.ensureDir(path__default.default.dirname(cacheFilePath)); await fs__default.default.writeJSON(cacheFilePath, snapshot); } /** * Fetch StructureDefinition metadata by any identifier (id, url, name) - FSH style. */ async getMetadata(identifier, packageFilter) { const errors = []; if (identifier.startsWith("http:") || identifier.startsWith("https:") || identifier.includes(":")) { try { const match = await this.fpe.resolveMeta({ resourceType: "StructureDefinition", url: identifier, package: packageFilter }); return match; } catch (e) { errors.push(e); } } try { const match = await this.fpe.resolveMeta({ resourceType: "StructureDefinition", id: identifier, package: packageFilter }); return match; } catch (e) { errors.push(e); } try { const match = await this.fpe.resolveMeta({ resourceType: "StructureDefinition", name: identifier, package: packageFilter }); return match; } catch (e) { errors.push(e); } errors.map((e) => this.logger.error(e)); throw new Error(`Failed to resolve StructureDefinition '${identifier}'`); } /** * Generate a snapshot for a StructureDefinition. */ async generate(filename, packageId, packageVersion) { this.logger.info(`Generating snapshot for '${filename}' in package '${packageId}@${packageVersion}'...`); const sd = await this.getStructureDefinitionByFileName(filename, packageId, packageVersion); if (!sd) { throw new Error(`File '${filename}' not found in package '${packageId}@${packageVersion}'`); } if (!sd.baseDefinition) { throw new Error(`StructureDefinition '${sd?.url}' does not have a baseDefinition`); } const baseFhirPackage = await this.getCorePackage({ id: packageId, version: packageVersion }); const snapshotFetcher = (async (url) => { let metadata; try { metadata = await this.fpe.resolveMeta({ resourceType: "StructureDefinition", url, package: { id: packageId, version: packageVersion } }); } catch (e) { this.logger.warn(`Failed to resolve metadata for '${url}' in package '${packageId}@${packageVersion}': ${e instanceof Error ? e.message : String(e)}`); metadata = await this.fpe.resolveMeta({ resourceType: "StructureDefinition", url }); } return (await this.getSnapshotByMeta(metadata)).snapshot?.element; }).bind(this); const fetcher = new DefinitionFetcher5( { id: packageId, version: packageVersion }, baseFhirPackage, this.fpe, snapshotFetcher ); const diffs = sd.differential?.element; if (!diffs || diffs.length === 0) { throw new Error(`StructureDefinition '${filename}' does not have a differential`); } let baseSnapshot; try { baseSnapshot = await snapshotFetcher(sd.baseDefinition); if (!baseSnapshot || baseSnapshot.length === 0) { throw new Error(`Base definition '${sd.baseDefinition}' does not have a snapshot`); } const migratedBaseSnapshot = migrateElements(baseSnapshot, sd.baseDefinition); const generated = await applyDiffs(migratedBaseSnapshot, diffs, fetcher, this.logger); return { __corePackage: baseFhirPackage, ...sd, snapshot: { element: generated } }; } catch (e) { this.logger.warn(`Failed to generate snapshot for '${sd.url}': ${e instanceof Error ? e.message : String(e)} Using the original StructureDefinition from source package.`); if (!sd.snapshot || !sd.snapshot.element || sd.snapshot.element.length === 0) { throw new Error(`The original StructureDefinition '${sd.url}' does not have a snapshot`); } return { __corePackage: baseFhirPackage, ...sd }; } } /** * Get snapshot by metadata. */ async getSnapshotByMeta(metadata) { const { derivation, filename, __packageId: packageId, __packageVersion: packageVersion } = metadata; if (!derivation || derivation === "specialization") { const sd = await this.getStructureDefinitionByFileName(filename, packageId, packageVersion); const elements = sd?.snapshot?.element; if (!elements || elements.length === 0) { throw new Error(`StructureDefinition '${metadata.url}' does not have a snapshot`); } return { __corePackage: { id: packageId, version: packageVersion }, ...sd }; } const cached = this.cacheMode !== "none" ? await this.getSnapshotFromCache( filename, packageId, packageVersion ) : void 0; if (cached) return cached; const generated = await this.generate(filename, packageId, packageVersion); if (this.cacheMode !== "none") { await this.saveSnapshotToCache(filename, packageId, packageVersion, generated); } return generated; } async ensureSnapshotCached(filename, packageId, packageVersion) { const cacheFilePath = this.getCacheFilePath(filename, packageId, packageVersion); try { await fs__default.default.access(cacheFilePath); return; } catch { const generated = await this.generate(filename, packageId, packageVersion); await this.saveSnapshotToCache(filename, packageId, packageVersion, generated); } } /** * Get snapshot by any FSH style identifier (id, url or name), or by a metadata object. */ async getSnapshot(identifier, packageFilter) { try { let metadata; if (typeof identifier === "string") { metadata = await this.getMetadata(identifier, packageFilter); if (!metadata) { throw new Error(`StructureDefinition '${identifier}' not found in context. Could not get or generate a snapshot.`); } } else { metadata = identifier; if (!metadata) { throw new Error(`StructureDefinition with metadata: ${JSON.stringify(identifier, null, 2)} not found in context. Could not get or generate a snapshot.`); } } return await this.getSnapshotByMeta(metadata); } catch (e) { throw this.prethrow(e); } } }; exports.FhirSnapshotGenerator = FhirSnapshotGenerator; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map