fhir-snapshot-generator
Version:
Generate snapshots for FHIR Profiles
979 lines (953 loc) • 38.7 kB
JavaScript
;
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