From 2e5d4d08d3b9d2693be43980d50c6e9410a1c4fd Mon Sep 17 00:00:00 2001 From: Chris Moesel Date: Thu, 24 Oct 2024 09:10:35 -0400 Subject: [PATCH] WIP --- fhir-package-loader-1.0.0.tgz | Bin 0 -> 29145 bytes src/cache/DiskBasedPackageCache.ts | 4 +- src/db/PackageDB.ts | 1 + src/db/SQLJSPackageDB.ts | 5 ++ src/loader/BasePackageLoader.ts | 53 ++++++++++++--- src/loader/PackageLoader.ts | 1 + src/registry/FHIRRegistryClient.ts | 15 ++--- src/registry/NPMRegistryClient.ts | 15 ++--- src/registry/RedundantRegistryClient.ts | 13 ++++ src/registry/RegistryClient.ts | 1 + src/registry/utils.ts | 20 +++++- src/virtual/DiskBasedVirtualPackage.ts | 63 +++++++++++++----- src/virtual/InMemoryVirtualPackage.ts | 8 ++- src/virtual/VirtualPackage.ts | 5 +- test/loader/BasePackageLoader.test.ts | 3 + test/registry/RedundantRegistryClient.test.ts | 7 +- 16 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 fhir-package-loader-1.0.0.tgz diff --git a/fhir-package-loader-1.0.0.tgz b/fhir-package-loader-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..efa1c16d7ddc5adee9778d334dd710f9ce9a712d GIT binary patch literal 29145 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 literal 0 HcmV?d00001 diff --git a/src/cache/DiskBasedPackageCache.ts b/src/cache/DiskBasedPackageCache.ts index 5febb53..c360bab 100644 --- a/src/cache/DiskBasedPackageCache.ts +++ b/src/cache/DiskBasedPackageCache.ts @@ -99,14 +99,14 @@ export class DiskBasedPackageCache implements PackageCache { return false; } else if (/\.json$/i.test(entry.name)) { return true; - } else if (/-spreadsheet.xml/i.test(entry.name)) { + } else if (/-spreadsheet\.xml$/i.test(entry.name)) { spreadSheetCount++; this.log( 'debug', `Skipped spreadsheet XML file: ${path.resolve(entry.path, entry.name)}` ); return false; - } else if (/\.xml/i.test(entry.name)) { + } else if (/\.xml$/i.test(entry.name)) { const xml = fs.readFileSync(path.resolve(entry.path, entry.name)).toString(); if (/<\?mso-application progid="Excel\.Sheet"\?>/m.test(xml)) { spreadSheetCount++; diff --git a/src/db/PackageDB.ts b/src/db/PackageDB.ts index dd72229..ad24052 100644 --- a/src/db/PackageDB.ts +++ b/src/db/PackageDB.ts @@ -9,4 +9,5 @@ export interface PackageDB { findResourceInfos(key: string, options?: FindResourceInfoOptions): ResourceInfo[]; findResourceInfo(key: string, options?: FindResourceInfoOptions): ResourceInfo | undefined; getPackageStats(name: string, version: string): PackageStats | undefined; + exportDB(): Promise<{ mimeType: string; data: Buffer }>; } diff --git a/src/db/SQLJSPackageDB.ts b/src/db/SQLJSPackageDB.ts index 24a37ff..8bd1102 100644 --- a/src/db/SQLJSPackageDB.ts +++ b/src/db/SQLJSPackageDB.ts @@ -318,6 +318,11 @@ export class SQLJSPackageDB implements PackageDB { }; } + exportDB(): Promise<{ mimeType: string; data: Buffer }> { + const data = this.db.export(); + return Promise.resolve({ mimeType: 'application/x-sqlite3', data: Buffer.from(data) }); + } + logPackageTable() { const res = this.db.exec('SELECT * FROM package'); console.log(util.inspect(res, false, 3, true)); diff --git a/src/loader/BasePackageLoader.ts b/src/loader/BasePackageLoader.ts index d4b1bb2..b50149d 100644 --- a/src/loader/BasePackageLoader.ts +++ b/src/loader/BasePackageLoader.ts @@ -31,6 +31,13 @@ export class BasePackageLoader implements PackageLoader { async loadPackage(name: string, version: string): Promise { let packageLabel = `${name}#${version}`; + const originalVersion = version; + version = await this.registryClient.resolveVersion(name, version); + if (version !== originalVersion) { + this.log('info', `Resolved ${packageLabel} to concrete version ${version}`); + packageLabel = `${name}#${version}`; + } + // If it's already loaded, then there's nothing to do if (this.getPackageLoadStatus(name, version) === LoadStatus.LOADED) { this.log('info', `${packageLabel} already loaded`); @@ -47,6 +54,7 @@ export class BasePackageLoader implements PackageLoader { packageLabel = `${name}#${version}`; } + let downloadErrorMessage: string; // If it's a "current" version, download the latest version from the build server (if applicable) if (isCurrentVersion(version)) { const branch = version.indexOf('$') !== -1 ? version.split('$')[1] : undefined; @@ -55,7 +63,7 @@ export class BasePackageLoader implements PackageLoader { const tarballStream = await this.currentBuildClient.downloadCurrentBuild(name, branch); await this.packageCache.cachePackageTarball(name, version, tarballStream); } catch { - this.log('error', `Failed to download ${packageLabel} from current builds`); + downloadErrorMessage = `Failed to download most recent ${packageLabel} from current builds`; } } } @@ -65,7 +73,7 @@ export class BasePackageLoader implements PackageLoader { const tarballStream = await this.registryClient.download(name, version); await this.packageCache.cachePackageTarball(name, version, tarballStream); } catch { - this.log('error', `Failed to download ${packageLabel} from registry`); + downloadErrorMessage = `Failed to download ${packageLabel} from the registry`; } } @@ -74,9 +82,17 @@ export class BasePackageLoader implements PackageLoader { try { stats = this.loadPackageFromCache(name, version); } catch { - this.log('error', `Failed to load ${name}#${version}`); + this.log( + 'error', + `Failed to load ${packageLabel}${downloadErrorMessage ? `: ${downloadErrorMessage}` : ''}` + ); return LoadStatus.FAILED; } + if (downloadErrorMessage) { + // Loading succeeded despite a download error. This might happen if a current build is stale, + // but the download fails, in which case the stale build will be loaded instead. + this.log('error', downloadErrorMessage); + } this.log('info', `Loaded ${stats.name}#${stats.version} with ${stats.resourceCount} resources`); return LoadStatus.LOADED; } @@ -117,12 +133,13 @@ export class BasePackageLoader implements PackageLoader { this.packageDB.savePackageInfo(info); // Register the resources - await pkg.registerResources((key: string, resource: any) => { + await pkg.registerResources((key: string, resource: any, allowNonResources?: boolean) => { this.loadResource( `virtual:${packageKey}:${key}`, resource, packageJSON.name, - packageJSON.version + packageJSON.version, + allowNonResources ); }); @@ -172,7 +189,10 @@ export class BasePackageLoader implements PackageLoader { this.loadResourceFromCache(resourcePath, packageName, packageVersion); } catch { // swallow this error because some JSON files will not be resources - this.log('debug', `JSON file at path ${resourcePath} was not FHIR resource`); + // and don't log it if it is package.json (since every package should have one) + if (path.basename(resourcePath) !== 'package.json') { + this.log('debug', `JSON file at path ${resourcePath} was not FHIR resource`); + } } }); } @@ -186,14 +206,21 @@ export class BasePackageLoader implements PackageLoader { resourcePath: string, resourceJSON: any, packageName?: string, - packageVersion?: string + packageVersion?: string, + allowNonResources = false ) { // We require at least a resourceType in order to know it is FHIR - if (typeof resourceJSON.resourceType !== 'string' || resourceJSON.resourceType === '') { - throw new InvalidResourceError(resourcePath, 'resource does not specify its resourceType'); + let resourceType = resourceJSON.resourceType; + if (typeof resourceType !== 'string' || resourceType === '') { + if (allowNonResources) { + // SUSHI needs to support registering local logical models, but some code expects resourceType + resourceType = 'Unknown'; + } else { + throw new InvalidResourceError(resourcePath, 'resource does not specify its resourceType'); + } } - const info: ResourceInfo = { resourceType: resourceJSON.resourceType }; + const info: ResourceInfo = { resourceType }; if (typeof resourceJSON.id === 'string') { info.id = resourceJSON.id; } @@ -206,7 +233,7 @@ export class BasePackageLoader implements PackageLoader { if (typeof resourceJSON.version === 'string') { info.version = resourceJSON.version; } - if (resourceJSON.resourceType === 'StructureDefinition') { + if (resourceType === 'StructureDefinition') { if (typeof resourceJSON.kind === 'string') { info.sdKind = resourceJSON.kind; } @@ -372,6 +399,10 @@ export class BasePackageLoader implements PackageLoader { } } + exportDB(): Promise<{ mimeType: string; data: Buffer }> { + return this.packageDB.exportDB(); + } + clear() { this.packageDB.clear(); } diff --git a/src/loader/PackageLoader.ts b/src/loader/PackageLoader.ts index 4430d79..53cba1a 100644 --- a/src/loader/PackageLoader.ts +++ b/src/loader/PackageLoader.ts @@ -18,5 +18,6 @@ export interface PackageLoader { findResourceInfo(key: string, options?: FindResourceInfoOptions): ResourceInfo | undefined; findResourceJSONs(key: string, options?: FindResourceInfoOptions): any[]; findResourceJSON(key: string, options?: FindResourceInfoOptions): any | undefined; + exportDB(): Promise<{ mimeType: string; data: Buffer }>; clear(): void; } diff --git a/src/registry/FHIRRegistryClient.ts b/src/registry/FHIRRegistryClient.ts index 57c83b1..3521191 100644 --- a/src/registry/FHIRRegistryClient.ts +++ b/src/registry/FHIRRegistryClient.ts @@ -1,8 +1,7 @@ import { Readable } from 'stream'; import { LogFunction, axiosGet } from '../utils'; import { RegistryClient, RegistryClientOptions } from './RegistryClient'; -import { IncorrectWildcardVersionFormatError } from '../errors'; -import { lookUpLatestVersion, lookUpLatestPatchVersion } from './utils'; +import { resolveVersion } from './utils'; export class FHIRRegistryClient implements RegistryClient { public endpoint: string; @@ -14,15 +13,13 @@ export class FHIRRegistryClient implements RegistryClient { this.log = options.log ?? (() => {}); } + async resolveVersion(name: string, version: string): Promise { + return resolveVersion(this.endpoint, name, version); + } + async download(name: string, version: string): Promise { // Resolve version if necessary - if (version === 'latest') { - version = await lookUpLatestVersion(this.endpoint, name); - } else if (/^\d+\.\d+\.x$/.test(version)) { - version = await lookUpLatestPatchVersion(this.endpoint, name, version); - } else if (/^\d+\.x$/.test(version)) { - throw new IncorrectWildcardVersionFormatError(name, version); - } + version = await this.resolveVersion(name, version); // Construct URL from endpoint, name, and version // See: https://confluence.hl7.org/pages/viewpage.action?pageId=97454344#FHIRPackageRegistryUserDocumentation-Download diff --git a/src/registry/NPMRegistryClient.ts b/src/registry/NPMRegistryClient.ts index 0787c11..4df0761 100644 --- a/src/registry/NPMRegistryClient.ts +++ b/src/registry/NPMRegistryClient.ts @@ -1,8 +1,7 @@ import { Readable } from 'stream'; import { LogFunction, axiosGet } from '../utils'; import { RegistryClient, RegistryClientOptions } from './RegistryClient'; -import { IncorrectWildcardVersionFormatError } from '../errors'; -import { lookUpLatestVersion, lookUpLatestPatchVersion } from './utils'; +import { resolveVersion } from './utils'; export class NPMRegistryClient implements RegistryClient { public endpoint: string; @@ -14,15 +13,13 @@ export class NPMRegistryClient implements RegistryClient { this.log = options.log ?? (() => {}); } + async resolveVersion(name: string, version: string): Promise { + return resolveVersion(this.endpoint, name, version); + } + async download(name: string, version: string): Promise { // Resolve version if necessary - if (version === 'latest') { - version = await lookUpLatestVersion(this.endpoint, name); - } else if (/^\d+\.\d+\.x$/.test(version)) { - version = await lookUpLatestPatchVersion(this.endpoint, name, version); - } else if (/^\d+\.x$/.test(version)) { - throw new IncorrectWildcardVersionFormatError(name, version); - } + version = await this.resolveVersion(name, version); // Get the manifest information about the package from the registry let url; diff --git a/src/registry/RedundantRegistryClient.ts b/src/registry/RedundantRegistryClient.ts index 7def1ae..613b03d 100644 --- a/src/registry/RedundantRegistryClient.ts +++ b/src/registry/RedundantRegistryClient.ts @@ -11,6 +11,19 @@ export class RedundantRegistryClient implements RegistryClient { this.log = options.log ?? (() => {}); } + async resolveVersion(name: string, version: string): Promise { + const packageLabel = `${name}#${version}`; + + for (const client of this.clients) { + try { + return await client.resolveVersion(name, version); + } catch { + // Do nothing. Fallback to the next one. + } + } + throw Error(`Failed to resolve version for ${packageLabel}`); + } + async download(name: string, version: string): Promise { const packageLabel = `${name}#${version}`; diff --git a/src/registry/RegistryClient.ts b/src/registry/RegistryClient.ts index 7d69be3..7246ec4 100644 --- a/src/registry/RegistryClient.ts +++ b/src/registry/RegistryClient.ts @@ -6,5 +6,6 @@ export type RegistryClientOptions = { }; export interface RegistryClient { + resolveVersion(name: string, version: string): Promise; download(name: string, version: string): Promise; } diff --git a/src/registry/utils.ts b/src/registry/utils.ts index 4aeedaa..47ef13a 100644 --- a/src/registry/utils.ts +++ b/src/registry/utils.ts @@ -2,7 +2,23 @@ import { IncorrectWildcardVersionFormatError, LatestVersionUnavailableError } fr import { axiosGet } from '../utils'; import { maxSatisfying } from 'semver'; -export async function lookUpLatestVersion(endpoint: string, name: string): Promise { +export async function resolveVersion( + endpoint: string, + name: string, + version: string +): Promise { + let resolvedVersion = version; + if (version === 'latest') { + resolvedVersion = await lookUpLatestVersion(endpoint, name); + } else if (/^\d+\.\d+\.x$/.test(version)) { + resolvedVersion = await lookUpLatestPatchVersion(endpoint, name, version); + } else if (/^\d+\.x$/.test(version)) { + throw new IncorrectWildcardVersionFormatError(name, version); + } + return resolvedVersion; +} + +async function lookUpLatestVersion(endpoint: string, name: string): Promise { try { const res = await axiosGet(`${endpoint}/${name}`, { responseType: 'json' @@ -17,7 +33,7 @@ export async function lookUpLatestVersion(endpoint: string, name: string): Promi } } -export async function lookUpLatestPatchVersion( +async function lookUpLatestPatchVersion( endpoint: string, name: string, version: string diff --git a/src/virtual/DiskBasedVirtualPackage.ts b/src/virtual/DiskBasedVirtualPackage.ts index b2eb4a2..9dd00c9 100644 --- a/src/virtual/DiskBasedVirtualPackage.ts +++ b/src/virtual/DiskBasedVirtualPackage.ts @@ -5,24 +5,34 @@ import { PackageJSON } from '../package/PackageJSON'; import { LogFunction } from '../utils/logger'; import { VirtualPackage, VirtualPackageOptions } from './VirtualPackage'; +export type DiskBasedVirtualPackageOptions = VirtualPackageOptions & { + recursive?: boolean; +}; + export class DiskBasedVirtualPackage implements VirtualPackage { private log: LogFunction; + private allowNonResources: boolean; + private recursive: boolean; private fhirConverter: FHIRConverter; constructor( private packageJSON: PackageJSON, private paths: string[] = [], - options: VirtualPackageOptions = {} + options: DiskBasedVirtualPackageOptions = {} ) { this.log = options.log ?? (() => {}); + this.allowNonResources = options.allowNonResources ?? false; + this.recursive = options.recursive ?? false; this.fhirConverter = new FHIRConverter(); } - async registerResources(register: (key: string, resource: any) => void): Promise { + async registerResources( + register: (key: string, resource: any, allowNonResources?: boolean) => void + ): Promise { const spreadSheetCounts = new Map(); const invalidFileCounts = new Map(); - const filePaths = getFilePaths(this.paths); + const filePaths = getFilePaths(this.paths, this.recursive); for (const filePath of filePaths) { try { const name = path.basename(filePath); @@ -30,24 +40,32 @@ export class DiskBasedVirtualPackage implements VirtualPackage { // Is it a potential resource? if (/\.json$/i.test(name)) { // TODO: Error handling? - register(filePath, this.getResourceByKey(filePath)); - } else if (/-spreadsheet.xml/i.test(name)) { + register(filePath, this.getResourceByKey(filePath), this.allowNonResources); + } else if (/-spreadsheet\.xml$/i.test(name)) { spreadSheetCounts.set(parent, (spreadSheetCounts.get(parent) ?? 0) + 1); this.log('debug', `Skipped spreadsheet XML file: ${filePath}`); - } else if (/\.xml/i.test(name)) { + } else if (/\.xml$/i.test(name)) { const xml = fs.readFileSync(filePath).toString(); if (/<\?mso-application progid="Excel\.Sheet"\?>/m.test(xml)) { spreadSheetCounts.set(parent, (spreadSheetCounts.get(parent) ?? 0) + 1); this.log('debug', `Skipped spreadsheet XML file: ${filePath}`); } // TODO: Error handling? - register(filePath, this.getResourceByKey(filePath)); + register(filePath, this.getResourceByKey(filePath), this.allowNonResources); } else { invalidFileCounts.set(parent, (invalidFileCounts.get(parent) ?? 0) + 1); this.log('debug', `Skipped non-JSON / non-XML file: ${filePath}`); } - } catch { - this.log('error', `Failed to load resource from path: ${filePath}`); + } catch (e) { + if (/convert XML .* Unknown resource type/.test(e.message)) { + // Skip unknown FHIR resource types. When we have instances of Logical Models, + // the resourceType will not be recognized as a known FHIR resourceType, but that's okay. + } else { + this.log('error', `Failed to load resource from path: ${filePath}`); + if (e.stack) { + this.log('debug', e.stack); + } + } } } @@ -76,12 +94,18 @@ export class DiskBasedVirtualPackage implements VirtualPackage { getResourceByKey(key: string) { let resource: any; if (/.xml$/i.test(key)) { + let xml: string; try { - const xml = fs.readFileSync(key).toString(); - resource = this.fhirConverter.xmlToObj(xml); + xml = fs.readFileSync(key).toString(); } catch { throw new Error(`Failed to get XML resource at path ${key}`); } + try { + // TODO: Support other versions of FHIR during conversion + resource = this.fhirConverter.xmlToObj(xml); + } catch (e) { + throw new Error(`Failed to convert XML resource at path ${key}: ${e.message}`); + } } else if (/.json$/i.test(key)) { try { resource = fs.readJSONSync(key); @@ -95,20 +119,23 @@ export class DiskBasedVirtualPackage implements VirtualPackage { } } -function getFilePaths(paths: string[]): string[] { - const filePaths: string[] = []; +function getFilePaths(paths: string[], recursive: boolean): string[] { + const filePaths = new Set(); paths.forEach(p => { const stat = fs.statSync(p); if (stat.isFile()) { - filePaths.push(path.resolve(p)); + filePaths.add(path.resolve(p)); } else if (stat.isDirectory()) { fs.readdirSync(p, { withFileTypes: true }).forEach(entry => { - // NOTE: This is not a recursive crawl, so we only care about files - if (entry.isFile) { - filePaths.push(path.resolve(entry.parentPath, entry.name)); + if (entry.isFile()) { + filePaths.add(path.resolve(entry.parentPath, entry.name)); + } else if (recursive && entry.isDirectory()) { + getFilePaths([path.resolve(entry.parentPath, entry.name)], recursive).forEach(fp => + filePaths.add(fp) + ); } }); } }); - return filePaths; + return Array.from(filePaths); } diff --git a/src/virtual/InMemoryVirtualPackage.ts b/src/virtual/InMemoryVirtualPackage.ts index 8898cc4..e7374e8 100644 --- a/src/virtual/InMemoryVirtualPackage.ts +++ b/src/virtual/InMemoryVirtualPackage.ts @@ -4,6 +4,7 @@ import { VirtualPackage, VirtualPackageOptions } from './VirtualPackage'; export class InMemoryVirtualPackage implements VirtualPackage { private log: LogFunction; + private allowNonResources: boolean; constructor( private packageJSON: PackageJSON, @@ -11,11 +12,14 @@ export class InMemoryVirtualPackage implements VirtualPackage { options: VirtualPackageOptions = {} ) { this.log = options.log ?? (() => {}); + this.allowNonResources = options.allowNonResources ?? false; } - async registerResources(register: (key: string, resource: any) => void): Promise { + async registerResources( + register: (key: string, resource: any, allowNonResources?: boolean) => void + ): Promise { // TODO: Error handling? - this.resources.forEach((resource, key) => register(key, resource)); + this.resources.forEach((resource, key) => register(key, resource, this.allowNonResources)); } getPackageJSON(): PackageJSON { diff --git a/src/virtual/VirtualPackage.ts b/src/virtual/VirtualPackage.ts index 1504fcb..b32a8d5 100644 --- a/src/virtual/VirtualPackage.ts +++ b/src/virtual/VirtualPackage.ts @@ -2,11 +2,14 @@ import { LogFunction } from '../utils/logger'; import { PackageJSON } from '../package/PackageJSON'; export interface VirtualPackage { - registerResources(register: (key: string, resource: any) => void): Promise; + registerResources( + register: (key: string, resource: any, allowNonResources?: boolean) => void + ): Promise; getPackageJSON(): PackageJSON; getResourceByKey(key: string): any; } export type VirtualPackageOptions = { log?: LogFunction; + allowNonResources?: boolean; }; diff --git a/test/loader/BasePackageLoader.test.ts b/test/loader/BasePackageLoader.test.ts index 5b13d30..05df2d4 100644 --- a/test/loader/BasePackageLoader.test.ts +++ b/test/loader/BasePackageLoader.test.ts @@ -28,6 +28,9 @@ describe('BasePackageLoader', () => { BasePackageLoader.prototype as any, 'loadPackageFromCache' ); + registryClientMock.resolveVersion.mockImplementation((name, version) => + Promise.resolve(version) + ); loader = new BasePackageLoader( packageDBMock, packageCacheMock, diff --git a/test/registry/RedundantRegistryClient.test.ts b/test/registry/RedundantRegistryClient.test.ts index ca4656e..bba5517 100644 --- a/test/registry/RedundantRegistryClient.test.ts +++ b/test/registry/RedundantRegistryClient.test.ts @@ -1,13 +1,18 @@ import { RedundantRegistryClient } from '../../src/registry/RedundantRegistryClient'; +import { RegistryClient } from '../../src/registry/RegistryClient'; import { loggerSpy } from '../testhelpers'; import { Readable } from 'stream'; -class MyMockClient { +class MyMockClient implements RegistryClient { public endpoint: string; constructor(endpoint: string) { this.endpoint = endpoint; } + async resolveVersion(name: string, version: string): Promise { + return version; + } + async download(name: string, version: string): Promise { // to mimic failure of download if (this.endpoint == 'failed.to.download') throw new Error('Failed to download');