Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
cmoesel committed Oct 24, 2024
1 parent 94c1d04 commit 2e5d4d0
Show file tree
Hide file tree
Showing 16 changed files with 159 additions and 55 deletions.
Binary file added fhir-package-loader-1.0.0.tgz
Binary file not shown.
4 changes: 2 additions & 2 deletions src/cache/DiskBasedPackageCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down
1 change: 1 addition & 0 deletions src/db/PackageDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;
}
5 changes: 5 additions & 0 deletions src/db/SQLJSPackageDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
53 changes: 42 additions & 11 deletions src/loader/BasePackageLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export class BasePackageLoader implements PackageLoader {
async loadPackage(name: string, version: string): Promise<LoadStatus> {
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`);
Expand All @@ -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;
Expand All @@ -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`;
}
}
}
Expand All @@ -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`;
}
}

Expand All @@ -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;
}
Expand Down Expand Up @@ -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
);
});

Expand Down Expand Up @@ -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`);
}
}
});
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -372,6 +399,10 @@ export class BasePackageLoader implements PackageLoader {
}
}

exportDB(): Promise<{ mimeType: string; data: Buffer }> {
return this.packageDB.exportDB();
}

clear() {
this.packageDB.clear();
}
Expand Down
1 change: 1 addition & 0 deletions src/loader/PackageLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
15 changes: 6 additions & 9 deletions src/registry/FHIRRegistryClient.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,15 +13,13 @@ export class FHIRRegistryClient implements RegistryClient {
this.log = options.log ?? (() => {});
}

async resolveVersion(name: string, version: string): Promise<string> {
return resolveVersion(this.endpoint, name, version);
}

async download(name: string, version: string): Promise<Readable> {
// 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
Expand Down
15 changes: 6 additions & 9 deletions src/registry/NPMRegistryClient.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,15 +13,13 @@ export class NPMRegistryClient implements RegistryClient {
this.log = options.log ?? (() => {});
}

async resolveVersion(name: string, version: string): Promise<string> {
return resolveVersion(this.endpoint, name, version);
}

async download(name: string, version: string): Promise<Readable> {
// 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;
Expand Down
13 changes: 13 additions & 0 deletions src/registry/RedundantRegistryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ export class RedundantRegistryClient implements RegistryClient {
this.log = options.log ?? (() => {});
}

async resolveVersion(name: string, version: string): Promise<string> {
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<Readable> {
const packageLabel = `${name}#${version}`;

Expand Down
1 change: 1 addition & 0 deletions src/registry/RegistryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export type RegistryClientOptions = {
};

export interface RegistryClient {
resolveVersion(name: string, version: string): Promise<string>;
download(name: string, version: string): Promise<Readable>;
}
20 changes: 18 additions & 2 deletions src/registry/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
export async function resolveVersion(
endpoint: string,
name: string,
version: string
): Promise<string> {
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<string> {
try {
const res = await axiosGet(`${endpoint}/${name}`, {
responseType: 'json'
Expand All @@ -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
Expand Down
Loading

0 comments on commit 2e5d4d0

Please sign in to comment.