From 43994de4b5033a22d4699d61485fb53ad25ab51a Mon Sep 17 00:00:00 2001 From: Chris Moesel Date: Thu, 22 Aug 2024 23:01:48 -0400 Subject: [PATCH] Remove all OLD fhir-package-loader code - Remove FHIRDefinitions - Remove load function - Remove old API (fpl function) - Remove other unused errors, classes, and functions - Update app.ts to use new classes - Update README --- README.md | 185 +-- src/FHIRDefinitions.ts | 509 ------ src/api.ts | 39 - src/app.ts | 37 +- src/current/BuildDotFhirDotOrgClient.ts | 3 +- src/errors/InvalidPackageError.ts | 8 - src/errors/PackageLoadError.ts | 13 - src/errors/index.ts | 2 - src/index.ts | 3 - src/load.ts | 490 ------ src/loader/DefaultPackageLoader.ts | 2 + src/utils/DoubleMap.ts | 87 -- src/utils/index.ts | 1 - src/utils/logger.ts | 21 - test/FHIRDefinitions.test.ts | 412 ----- test/api.test.ts | 186 --- test/current/BuildDotFhirDotOrgClient.test.ts | 94 +- test/load.test.ts | 1362 ----------------- test/utils/logger.test.ts | 50 - 19 files changed, 162 insertions(+), 3342 deletions(-) delete mode 100644 src/FHIRDefinitions.ts delete mode 100644 src/api.ts delete mode 100644 src/errors/InvalidPackageError.ts delete mode 100644 src/errors/PackageLoadError.ts delete mode 100644 src/load.ts delete mode 100644 src/utils/DoubleMap.ts delete mode 100644 test/FHIRDefinitions.test.ts delete mode 100644 test/api.test.ts delete mode 100644 test/load.test.ts delete mode 100644 test/utils/logger.test.ts diff --git a/README.md b/README.md index 43d2994..43cebdf 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,38 @@ # FHIR Package Loader -FHIR Package Loader is a utility that downloads published FHIR packages from the FHIR package registry. +FHIR Package Loader provides TypeScript/JavaScript classes for loading FHIR packages and querying them for FHIR resources. It can load FHIR packages from a local cache, the FHIR registry, an NPM registry, and/or the FHIR build server. # Usage -This tool can be used directly through a command line interface (CLI) or it can be used as a dependency in another JavaScript/TypeScript project to download FHIR packages and load the contents into memory. +FHIR Package Loader can be used directly via a command line interface (CLI) or it can be used as a dependency library in another JavaScript/TypeScript project. -FHIR Package Loader requires [Node.js](https://nodejs.org/) to be installed on the user's system. Users should install Node.js 18, although other current LTS versions are also expected to work. +The current implementation of FHIR Package Loader requires [Node.js](https://nodejs.org/) to be installed on the user's system. Users should install Node.js 18, although other current LTS versions are also expected to work. Future versions of FHIR Package Loader may provide web-friendly JavaScript implementations that do not require Node.js. -Once Node.js is installed, use either of the following methods to use the FHIR Package Loader. +## Using the FHIR Package Loader Command Line Interface (CLI) -## Command Line - -To download and unzip FHIR packages through the command line, you can run the following command directly: +To download and install FHIR packages through the command line, you can run the following command directly: ```sh -npx fhir-package-loader install # downloads specified FHIR packages +npx fhir-package-loader install # downloads specified FHIR packages ``` -_Note: `npx` comes with npm 5.2+ and higher._ +_Note: `package@version` is also supported to maintain backwards-compatibility with fhir-package-loader 1.x._ `npx` will ensure you are using the latest version and will allow you to run the CLI without needing to install and manage any dependency. -Alternatively, if you'd like to install the package, it can be installed globally and used as follows: +Alternately, if you'd like to install the fhir-package-loader package, it can be installed globally: ```sh -npm install -g fhir-package-loader # installs the package from npm +npm install -g fhir-package-loader # installs FHIR Package Loader utility from npm ``` -After installation, the `fhir-package-loader` command line will be available on your path: +After installation, the `fhir-package-loader` command line will be available on your path. Use the `fpl` command to invoke it: ```sh fpl --help # outputs information about using the command line fpl install --help -fpl install # downloads specified FHIR packages +fpl install # downloads specified FHIR packages ``` With both approaches, the same commands are available. The install command allows you to specify the FHIR packages to download, along with a few additional options: @@ -45,7 +43,7 @@ Usage: fpl install [options] download and unzip specified FHIR packages Arguments: - fhirPackages list of FHIR packages to load using the format packageId@packageVersion... + fhirPackages list of FHIR packages to load using the format packageId#packageVersion or packageId@packageVersion Options: -c, --cachePath where to save packages to and load definitions from (default is the local [FHIR cache](https://confluence.hl7.org/pages/viewpage.action?pageId=66928417#FHIRPackageCache-Location)) @@ -69,106 +67,106 @@ Commands: help [command] display help for command Examples: - fpl install hl7.fhir.us.core@current - fpl install hl7.fhir.us.core@4.0.0 hl7.fhir.us.mcode@2.0.0 --cachePath ./myProject + fpl install hl7.fhir.us.core#current + fpl install hl7.fhir.us.core#4.0.0 hl7.fhir.us.mcode@2.0.0 --cachePath ./myProject ``` -## API +## Using FHIR Package Loader as a Library + +FHIR Package Loader can be used as a library to download FHIR packages, query their contents, and retrieve FHIR resources. The primary interface of interest is the `PackageLoader`: + +```ts +export interface PackageLoader { + loadPackage(name: string, version: string): Promise; + getPackageLoadStatus(name: string, version: string): LoadStatus; + findPackageInfos(name: string): PackageInfo[]; + findPackageInfo(name: string, version: string): PackageInfo | undefined; + findPackageJSONs(name: string): any[]; + findPackageJSON(name: string, version: string): any | undefined; + findResourceInfos(key: string, options?: FindResourceInfoOptions): ResourceInfo[]; + findResourceInfo(key: string, options?: FindResourceInfoOptions): ResourceInfo | undefined; + findResourceJSONs(key: string, options?: FindResourceInfoOptions): any[]; + findResourceJSON(key: string, options?: FindResourceInfoOptions): any | undefined; + clear(): void; +} +``` -Additionally, FHIR Package Loader exposes functions that can be used to query and download packages. +> _NOTE: The FHIR Package Loader 1.x API is no longer supported. FHIR Package Loader 2.0 is a complete rewrite with an entirely different API._ -### fpl(fhirPackages[, options]) +### PackageLoader Implementations -The `fpl` function can be used to download FHIR packages and load their definitions. +The [default PackageLoader](src/loader/DefaultPackageLoader.ts) implementation provides the most common package loader approach: +* package and resource metadata is stored and queried in an in-memory [sql.js](https://github.com/sql-js/sql.js) database +* the standard FHIR cache is used for local storage of unzipped packages (`$USER_HOME/.fhir/packages`) +* the standard FHIR registry is used (`packages.fhir.org`) for downloading published packages, falling back to `packages2.fhir.org` when necessary + * unless an `FPL_REGISTRY` environment variable is defined, in which case its value is used as the URL for an NPM registry to use _instead_ of the standard FHIR registry +* the `build.fhir.org` build server is used for downloading _current_ builds of packages -#### Parameters +To instantiate the default `PackageLoader`, import the asynchronous `defaultPackageLoader` function and invoke it, optionally passing in an `options` object with a log method to use for logging: -`fhirPackages` - An array of strings (or a comma separated string) that specifies the FHIR packages and versions to load. These can be in the format of `package#version` or `package@version`. +```ts +import { defaultPackageLoader, LoadStatus } from 'fhir-package-loader'; -- For example: `'hl7.fhir.us.core@4.0.0, hl7.fhir.us.mcode@2.0.0'` or `['hl7.fhir.us.core@4.0.0', 'hl7.fhir.us.mcode@2.0.0']` +// somewhere in your code... +const log = (level: string, message: string) => console.log(`${level}: ${message}`); +const loader = await defaultPackageLoader({ log }); +const status = await loader.loadPackage('hl7.fhir.us.core', '6.1.0'); +if (status !== LoadStatus.LOADED) { + // ... +} +``` -`options` - An object which can have the following attributes: +To instantiate the default `PackageLoader` with a set of standalone JSON or XML resources that should be pre-loaded, use the `defaultPackageLoaderWithLocalResources` function instead, passing in an array of file paths to folders containing the resources to load. -- `cachePath` - A string that specifies where to look for already downloaded packages and download them to if they are not found. The default is the the local [FHIR cache](https://confluence.hl7.org/pages/viewpage.action?pageId=66928417#FHIRPackageCache-Location). +For more control over the `PackageLoader`, use the [BasePackageLoader](src/loader/BasePackageLoader.ts). This allows you to specify the [PackageDB](src/db), [PackageCache](src/cache), [RegistryClient](src/registry), and [CurrentBuildClient](src/current) you wish to use. FHIRPackageLoader comes with implementations of each of these, but you may also provide your own implementations that adhere to the relevant interfaces. -- `log` - A function that is responsible for logging information. It takes in two strings, a level and a message, and does not return anything. - - For example: `log: console.log` will pass in `console.log` as the logging function and the level and message will be logged as `console.log(level, message)` +### PackageLoader Functions -#### Return Value +The `PackageLoader` interface provides the following functions: -A `Promise` that resolves to an object with the following attributes: +#### `loadPackage(name: string, version: string): Promise` -- `defs` - A [`FHIRDefinitions`](./src/FHIRDefinitions.ts) class instances that contains any definitions loaded from the specified package(s). -- `errors` An array of strings containing any errors detected during package loading. -- `warnings` An array of strings containing any warnings detected during package loading. -- `failedPackages` An array of strings containing the `package#version` of any packages that encountered an error during download or load and were not properly loaded to `defs`. +Loads the specified package version. The version may be a specific version (e.g., `1.2.3`), a wildcard patch version (e.g., `1.2.x`), `dev` (to indicate the local development build in your FHIR cache), `current` (to indicate the current master/main build), `current$branchname` (to indicate the current build on a specific branch), or `latest` (to indicate the most recent published version). Returns the [LoadStatus](src/loader/PackageLoader.ts). -### getLatestVersion(packageName[, log]) +#### `getPackageLoadStatus(name: string, version: string): LoadStatus` -The `getLatestVersion` function can be used to query the latest version of a FHIR package. +Gets the [LoadStatus](src/loader/PackageLoader.ts) for the specified package version. The returned value will be `LoadStatus.LOADED` if it is already loaded, `LoadStatus.NOT_LOADED` if it has not yet been loaded, or `LoadStatus.FAILED` if it was attempted but failed to load. This function supports specific versions (e.g. `1.2.3`), `dev`, `current`, and `current$branchname`. It does _not_ support wildcard patch versions (e.g., `1.2.x`) nor does it support the `latest` version. -#### Parameters +#### `findPackageInfos(name: string): PackageInfo[]` -`packageName` - A string that specifies the FHIR package to query. +Finds loaded packages by name and returns the corresponding array of [PackageInfo](src/package/PackageInfo.ts) objects or an empty array if there are no matches. -`log` - A function that is responsible for logging information. It takes in two strings, a level and a message, and does not return anything. +#### `findPackageInfo(name: string, version: string): PackageInfo | undefined` -#### Return Value +Finds a loaded package by its name and version, and returns the corresponding [PackageInfo](src/package/PackageInfo.ts) or `undefined` is there is not a match. In the case of multiple matches, the info for last package loaded will be returned. This function supports specific versions (e.g. `1.2.3`), `dev`, `current`, and `current$branchname`. -A `Promise` that resolves to a string containing the latest version of the FHIR package. +#### `findPackageJSONs(name: string): any[]` -### Usage +Finds loaded packages by name and returns the corresponding array of `package.json` JSON objects from the packages, or an empty array if there are no matches. -To use the API, FHIR Package Loader must be installed as a dependency of your project. To add it as a dependency, navigate to your project directory and use `npm` to install the package: +#### `findPackageJSON(name: string, version: string): any | undefined` -```sh -cd myProject -npm install fhir-package-loader -``` +Finds a loaded package by name and version, and returns the corresponding `package.json` JSON object from the packages, or `undefined` if there is not a match. In the case of multiple matches, the `package.json` from the last package loaded will be returned. This function supports specific versions (e.g. `1.2.3`), `dev`, `current`, and `current$branchname`. -Once installed as a dependency, you can `import` and use the API for loading FHIR packages. This function provides the same functionality you get through the CLI, but you also have access to the in memory definitions from the packages. The following example shows two ways to use the function in a project: - -```javascript -import { fpl } from 'fhir-package-loader'; - -async function myApp() { - // Downloads and unzips packages to FHIR cache or other specified location (if not already present) - await fpl(['package@version, package2@version']) - .then(results => { - // handle results - }) - .catch(err => { - // handle thrown errors - }); - - // Similar to above, but uses options - await fpl(['package@version'], { - cachePath: '../myPackages', - log: console.log - }) - .then(results => { - // handle results - }) - .catch(err => { - // handle thrown errors - }); -} -``` +#### `findResourceInfos(key: string, options?: FindResourceInfoOptions): ResourceInfo[]` -## Mock Out in Unit Tests +Finds loaded resources by a key and returns the corresponding array of [ResourceInfo](src/package/ResourceInfo.ts) objects or an empty array if there are no matches. The key will be matched against resources by their `id`, `name`, or `url`. An [options](src/package/ResourceInfo.ts) object may also be passed in to scope the search to a specific set of resource types and/or a specific package, and/or to limit the number of results returned. -If you use `fhir-package-loader` as a dependency in your project, you can choose to mock any function from the package. This may be helpful for writing unit tests that do not need to download packages from the FHIR registry. One way to do this is using the following snippet: +#### `findResourceInfo(key: string, options?: FindResourceInfoOptions): ResourceInfo | undefined` -```javascript -jest.mock('fhir-package-loader', () => { - const original = jest.requireActual('fhir-package-loader'); - return { - ...original, - loadDependencies: jest.fn(), // can optionally include a mock implementation - // any other functions to be mocked out - } -} -``` +Finds a loaded resource by a key and returns the corresponding [ResourceInfo](src/package/ResourceInfo.ts) or `undefined` if there is not a match. The key will be matched against resources by their `id`, `name`, or `url`. An [options](src/package/ResourceInfo.ts) object may also be passed in to scope the search to a specific set of resource types and/or a specific package. If a set of resource types is specified in the options, then the order of the resource types determines which resource is returned in the case of multiple matches (i.e., the resource types are assumed to be in priority order). If there are still multiple matches, the info for the last resource loaded will be returned. + +#### `findResourceJSONs(key: string, options?: FindResourceInfoOptions): any[]` + +Finds loaded resources by a key and returns the corresponding array of JSON FHIR definitions or an empty array if there are no matches. The key will be matched against resources by their `id`, `name`, or `url`. An [options](src/package/ResourceInfo.ts) object may also be passed in to scope the search to a specific set of resource types and/or a specific package, and/or to limit the number of results returned. + +#### `findResourceJSON(key: string, options?: FindResourceInfoOptions): any | undefined` + +Finds a loaded resource by a key and returns the corresponding FHIR JSON definition or `undefined` if there is not a match. The key will be matched against resources by their `id`, `name`, or `url`. An [options](src/package/ResourceInfo.ts) object may also be passed in to scope the search to a specific set of resource types and/or a specific package. If a set of resource types is specified in the options, then the order of the resource types determines which resource is returned in the case of multiple matches (i.e., the resource types are assumed to be in priority order). If there are still multiple matches, the the last resource loaded will be returned. + +#### `clear(): void` + +Clears all packages and resources from the loader. # For Developers @@ -182,22 +180,9 @@ Once Node.js is installed, run the following command from this project's root fo npm install ``` -## Exposed functions - -While the CLI and API should be sufficient for the majority of use cases, FHIR Package Loader exposes a few additional functions and classes that can be used within JavaScript/TypeScript projects. Below are the key exports: - -| Export | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `fpl` | API function to download and load definitions for a provided list of packages. | -| `getLatestVersion` | API function to find the latest version of a package. | -| `loadDependencies` | Takes a list of FHIR packages, a path to a directory (optional, defaults to FHIR cache), a log function (optional) and returns FHIRDefinitions from the provided packages. | -| `mergeDependency` | Takes a package name, a package version, an instance of FHIRDefinitions, a path to a directory (optional, defaults to FHIR cache), a log function (optional) and returns FHIRDefinitions with definitions added directly from the package. | -| `loadFromPath` | Takes a path, a package and version (format: package#version), and an instance of FHIRDefinitions and loads the definitions from the provided package at the provided path into FHIRDefinitions. | -| `FHIRDefinitions` | A class for FHIRDefinitions for one or more packages. This could be extended if there are additional properties that are specific to your implementation. | - # License -Copyright 2022 The MITRE Corporation +Copyright 2022-2024 The MITRE Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/FHIRDefinitions.ts b/src/FHIRDefinitions.ts deleted file mode 100644 index e3b1631..0000000 --- a/src/FHIRDefinitions.ts +++ /dev/null @@ -1,509 +0,0 @@ -import { cloneDeep, isEqual, uniqWith, uniq } from 'lodash'; -import { DoubleMap } from './utils'; - -/** Class representing the FHIR definitions in one or more FHIR packages */ -export class FHIRDefinitions { - protected resources: Map; - protected logicals: Map; - protected profiles: Map; - protected extensions: Map; - protected types: Map; - protected valueSets: Map; - protected codeSystems: Map; - protected implementationGuides: Map; - protected packageJsons: Map; - childFHIRDefs: FHIRDefinitions[]; - package: string; - unsuccessfulPackageLoad: boolean; - - /** Create a FHIRDefinitions */ - constructor() { - this.package = ''; - this.resources = new DoubleMap(); - this.logicals = new DoubleMap(); - this.profiles = new DoubleMap(); - this.extensions = new DoubleMap(); - this.types = new DoubleMap(); - this.valueSets = new DoubleMap(); - this.codeSystems = new DoubleMap(); - this.implementationGuides = new DoubleMap(); - this.packageJsons = new DoubleMap(); - this.childFHIRDefs = []; - this.unsuccessfulPackageLoad = false; - } - - /** Get the total number of definitions */ - size(): number { - return ( - this.allResources().length + - this.allLogicals().length + - this.allProfiles().length + - this.allExtensions().length + - this.allTypes().length + - this.allValueSets().length + - this.allCodeSystems().length + - this.allImplementationGuides().length - ); - } - - // NOTE: These all return clones of the JSON to prevent the source values from being overwritten - - /** - * Get all resources. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of resources - */ - allResources(fhirPackage?: string): any[] { - if ( - (this.resources.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectResources(fhirPackage), isEqual); - } - return this.collectResources(fhirPackage); - } - - protected collectResources(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectResources(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let resources = this.resources; - if (fhirPackage) { - resources = new Map(); - if (this.package === fhirPackage) { - resources = this.resources; - } - } - return cloneJsonMapValues(resources).concat(childValues); - } - - /** - * Get all logicals. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of logicals - */ - allLogicals(fhirPackage?: string): any[] { - if ( - (this.logicals.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectLogicals(fhirPackage), isEqual); - } - return uniqWith(this.collectLogicals(fhirPackage), isEqual); - } - - protected collectLogicals(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectLogicals(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let logicals = this.logicals; - if (fhirPackage) { - logicals = new Map(); - if (this.package === fhirPackage) { - logicals = this.logicals; - } - } - return cloneJsonMapValues(logicals).concat(childValues); - } - - /** - * Get all profiles. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of profiles - */ - allProfiles(fhirPackage?: string): any[] { - if ( - (this.profiles.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectProfiles(fhirPackage), isEqual); - } - return this.collectProfiles(fhirPackage); - } - - protected collectProfiles(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectProfiles(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let profiles = this.profiles; - if (fhirPackage) { - profiles = new Map(); - if (this.package === fhirPackage) { - profiles = this.profiles; - } - } - return cloneJsonMapValues(profiles).concat(childValues); - } - - /** - * Get all extensions. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of extensions - */ - allExtensions(fhirPackage?: string): any[] { - if ( - (this.extensions.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectExtensions(fhirPackage), isEqual); - } - return this.collectExtensions(fhirPackage); - } - - protected collectExtensions(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectExtensions(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let extensions = this.extensions; - if (fhirPackage) { - extensions = new Map(); - if (this.package === fhirPackage) { - extensions = this.extensions; - } - } - return cloneJsonMapValues(extensions).concat(childValues); - } - - /** - * Get all types. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of types - */ - allTypes(fhirPackage?: string): any[] { - if ((this.types.size > 0 && this.childFHIRDefs.length > 0) || this.childFHIRDefs.length > 1) { - return uniqWith(this.collectTypes(fhirPackage), isEqual); - } - return this.collectTypes(fhirPackage); - } - - protected collectTypes(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectTypes(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let types = this.types; - if (fhirPackage) { - types = new Map(); - if (this.package === fhirPackage) { - types = this.types; - } - } - return cloneJsonMapValues(types).concat(childValues); - } - - /** - * Get all value sets. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of value sets - */ - allValueSets(fhirPackage?: string): any[] { - if ( - (this.valueSets.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectValueSets(fhirPackage), isEqual); - } - return this.collectValueSets(fhirPackage); - } - - protected collectValueSets(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectValueSets(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let valueSets = this.valueSets; - if (fhirPackage) { - valueSets = new Map(); - if (this.package === fhirPackage) { - valueSets = this.valueSets; - } - } - return cloneJsonMapValues(valueSets).concat(childValues); - } - - /** - * Get all code systems. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of code systems - */ - allCodeSystems(fhirPackage?: string): any[] { - if ( - (this.codeSystems.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectCodeSystems(fhirPackage), isEqual); - } - return this.collectCodeSystems(fhirPackage); - } - - protected collectCodeSystems(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectCodeSystems(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let codeSystems = this.codeSystems; - if (fhirPackage) { - codeSystems = new Map(); - if (this.package === fhirPackage) { - codeSystems = this.codeSystems; - } - } - return cloneJsonMapValues(codeSystems).concat(childValues); - } - - /** - * Get all implementation guides. The array will not contain duplicates. - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of implementation guides - */ - allImplementationGuides(fhirPackage?: string): any[] { - if ( - (this.implementationGuides.size > 0 && this.childFHIRDefs.length > 0) || - this.childFHIRDefs.length > 1 - ) { - return uniqWith(this.collectImplementationGuides(fhirPackage), isEqual); - } - return this.collectImplementationGuides(fhirPackage); - } - - protected collectImplementationGuides(fhirPackage?: string): any[] { - const childValues = this.childFHIRDefs - .map(def => def.collectImplementationGuides(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - let implementationGuides = this.implementationGuides; - if (fhirPackage) { - implementationGuides = new Map(); - if (this.package === fhirPackage) { - implementationGuides = this.implementationGuides; - } - } - return cloneJsonMapValues(implementationGuides).concat(childValues); - } - - /** - * Get a list of packages that encountered errors while downloaded and were not loaded - * @param {string} [fhirPackage] - The package (packageId#version) to search in. If not provided, searches all packages. - * @returns array of packages (packageId#version) that were not successfully loaded - */ - allUnsuccessfulPackageLoads(fhirPackage?: string): string[] { - return uniq(this.collectUnsuccessfulPackageLoads(fhirPackage)); - } - - protected collectUnsuccessfulPackageLoads(fhirPackage?: string): string[] { - const childValues = this.childFHIRDefs - .map(def => def.collectUnsuccessfulPackageLoads(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - if (fhirPackage) { - if (this.package === fhirPackage && this.unsuccessfulPackageLoad) { - return childValues.concat(this.package); - } - } else if (this.unsuccessfulPackageLoad) { - return childValues.concat(this.package); - } - return childValues; - } - - /** - * Get a list of all packages that are contained in this FHIRDefinitions - * @param {string} [fhirPackage] The package (packageId#version) to get all packages from. If not provided, all packages are returned. - * @returns array of packages (packageId#version) that are loaded - */ - allPackages(fhirPackage?: string): string[] { - return uniq(this.collectPackages(fhirPackage)); - } - - protected collectPackages(fhirPackage?: string): string[] { - const childValues = this.childFHIRDefs - .map(def => def.collectPackages(fhirPackage)) - .reduce((a, b) => a.concat(b), []); - if (fhirPackage) { - if (this.package === fhirPackage && this.package !== '') { - return childValues.concat(this.package); - } - } else if (this.package !== '') { - return childValues.concat(this.package); - } - return childValues; - } - - /** - * Add a definition - * @param definition - The definition to add - */ - add(definition: any): void { - if (definition.resourceType === 'StructureDefinition') { - if ( - definition.type === 'Extension' && - definition.baseDefinition !== 'http://hl7.org/fhir/StructureDefinition/Element' - ) { - addDefinitionToMap(definition, this.extensions); - } else if ( - definition.kind === 'primitive-type' || - definition.kind === 'complex-type' || - definition.kind === 'datatype' - ) { - addDefinitionToMap(definition, this.types); - } else if (definition.kind === 'resource') { - if (definition.derivation === 'constraint') { - addDefinitionToMap(definition, this.profiles); - } else { - addDefinitionToMap(definition, this.resources); - } - } else if (definition.kind === 'logical') { - if (definition.derivation === 'specialization') { - addDefinitionToMap(definition, this.logicals); - } else { - addDefinitionToMap(definition, this.profiles); - } - } - } else if (definition.resourceType === 'ValueSet') { - addDefinitionToMap(definition, this.valueSets); - } else if (definition.resourceType === 'CodeSystem') { - addDefinitionToMap(definition, this.codeSystems); - } else if (definition.resourceType === 'ImplementationGuide') { - addDefinitionToMap(definition, this.implementationGuides); - } - } - - /** - * Add a package.json - * @param {string} id - package id - * @param {string} definition - package JSON definition - */ - addPackageJson(id: string, definition: any): void { - this.packageJsons.set(id, definition); - } - - /** - * Get a package's package.json - * @param {string} id - package id - * @returns package.json definition - */ - getPackageJson(id: string): any { - return this.packageJsons.get(id); - } - - /** - * Private function for search through current FHIRDefinitions and all childFHIRDefs - * for a specified definition. Uses get for efficient retrieves. - * Breath-first search through childFHIRDefinitions for the item. - * @param item name, id, or url of definition to find - * @param map name of the map to search in - * @returns definition or undefined if it is not found - */ - private getDefinition(item: string, map: maps): any | undefined { - const defsToSearch: FHIRDefinitions[] = [this]; - while (defsToSearch.length > 0) { - const currentFHIRDefs = defsToSearch.shift(); - const [base, ...versionParts] = item?.split('|') ?? ['', '']; - const version = versionParts.join('|') || null; - const def = currentFHIRDefs[map].get(base); - if (def) { - if (version == null || version === def?.version) { - // Only return the found definition if the version matches (if provided) - return def; - } - } - if (currentFHIRDefs.childFHIRDefs.length > 0) { - defsToSearch.push(...currentFHIRDefs.childFHIRDefs); - } - } - - return; - } - - /** - * Search for a definition based on the type it could be - * @param {string} item - the item to search for - * @param {Type[]} types - the possible type the item could be - * @returns the definition that is returned or undefined if none is found - */ - fishForFHIR(item: string, ...types: Type[]): any | undefined { - // No types passed in means to search ALL supported types - if (types.length === 0) { - types = FISHING_ORDER; - } else { - types.sort((a, b) => FISHING_ORDER.indexOf(a) - FISHING_ORDER.indexOf(b)); - } - - for (const type of types) { - let def; - switch (type) { - case Type.Resource: - def = cloneDeep(this.getDefinition(item, 'resources')); - break; - case Type.Logical: - def = cloneDeep(this.getDefinition(item, 'logicals')); - break; - case Type.Type: - def = cloneDeep(this.getDefinition(item, 'types')); - break; - case Type.Profile: - def = cloneDeep(this.getDefinition(item, 'profiles')); - break; - case Type.Extension: - def = cloneDeep(this.getDefinition(item, 'extensions')); - break; - case Type.ValueSet: - def = cloneDeep(this.getDefinition(item, 'valueSets')); - break; - case Type.CodeSystem: - def = cloneDeep(this.getDefinition(item, 'codeSystems')); - break; - case Type.Instance: // don't support resolving to FHIR instances - default: - break; - } - if (def) { - return def; - } - } - } -} - -function addDefinitionToMap(def: any, defMap: Map): void { - if (def.id) { - defMap.set(def.id, def); - } - if (def.url) { - defMap.set(def.url, def); - } - if (def.name) { - defMap.set(def.name, def); - } -} - -function cloneJsonMapValues(map: Map): any { - return Array.from(map.values()).map(v => cloneDeep(v)); -} - -export enum Type { - Profile = 'Profile', - Extension = 'Extension', - ValueSet = 'ValueSet', - CodeSystem = 'CodeSystem', - Instance = 'Instance', - Invariant = 'Invariant', // NOTE: only defined in FSHTanks, not FHIR defs - RuleSet = 'RuleSet', // NOTE: only defined in FSHTanks, not FHIR defs - Mapping = 'Mapping', // NOTE: only defined in FSHTanks, not FHIR defs - Resource = 'Resource', - Type = 'Type', // NOTE: only defined in FHIR defs, not FSHTanks - Logical = 'Logical' -} - -export const FISHING_ORDER = [ - Type.Resource, - Type.Logical, - Type.Type, - Type.Profile, - Type.Extension, - Type.ValueSet, - Type.CodeSystem -]; - -// Type to represent the names of the FHIRDefinition maps of definitions -type maps = - | 'resources' - | 'logicals' - | 'profiles' - | 'extensions' - | 'types' - | 'valueSets' - | 'codeSystems'; diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index 7be9569..0000000 --- a/src/api.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { FHIRDefinitions } from './FHIRDefinitions'; -import { ErrorsAndWarnings, LogFunction, wrapLogger } from './utils'; -import { loadDependencies } from './load'; -export { lookUpLatestVersion as getLatestVersion } from './load'; - -export async function fpl( - fhirPackages: string | string[], - options: packageLoadOptions = {} -): Promise<{ - defs: FHIRDefinitions; - errors: ErrorsAndWarnings['errors']; - warnings: ErrorsAndWarnings['warnings']; - failedPackages: string[]; -}> { - // Track errors and warnings - const errorsAndWarnings = new ErrorsAndWarnings(); - const logWithTrack = wrapLogger(options.log, errorsAndWarnings); - - // Create list of packages - if (!Array.isArray(fhirPackages)) { - fhirPackages = fhirPackages.split(',').map(p => p.trim()); - } - fhirPackages = fhirPackages.map(dep => dep.replace('@', '#')); - const defs = await loadDependencies(fhirPackages, options.cachePath, logWithTrack); - - const failedPackages = defs.allUnsuccessfulPackageLoads(); - - return { - defs, - errors: errorsAndWarnings.errors, - warnings: errorsAndWarnings.warnings, - failedPackages - }; -} - -type packageLoadOptions = { - log?: LogFunction; - cachePath?: string; -}; diff --git a/src/app.ts b/src/app.ts index 09d05c7..62950af 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,16 @@ #!/usr/bin/env node -import { program, OptionValues } from 'commander'; import path from 'path'; import fs from 'fs-extra'; -import { loadDependencies } from './load'; +import os from 'os'; +import initSqlJs from 'sql.js'; import { logger } from './utils'; +import { program, OptionValues } from 'commander'; +import { BasePackageLoader } from './loader'; +import { SQLJSPackageDB } from './db'; +import { DiskBasedPackageCache } from './cache'; +import { DefaultRegistryClient } from './registry'; +import { BuildDotFhirDotOrgClient } from './current'; function getVersion(): string { const packageJSONPath = path.join(__dirname, '..', 'package.json'); @@ -18,21 +24,30 @@ function getVersion(): string { function getHelpText(): string { return ` Examples: - fpl install hl7.fhir.us.core@current - fpl install hl7.fhir.us.core@4.0.0 hl7.fhir.us.mcode@2.0.0 --cachePath ./myProject`; + fpl install hl7.fhir.us.core#current + fpl install hl7.fhir.us.core#4.0.0 hl7.fhir.us.mcode#2.0.0 --cachePath ./myProject`; } async function install(fhirPackages: string[], options: OptionValues) { if (options.debug) logger.level = 'debug'; - - const packages = fhirPackages.map(dep => dep.replace('@', '#')); - const cachePath = options.cachePath; - - const logMessage = (level: string, message: string) => { + const log = (level: string, message: string) => { logger.log(level, message); }; - await loadDependencies(packages, cachePath, logMessage); + const SQL = await initSqlJs(); + const packageDB = new SQLJSPackageDB(new SQL.Database()); + const fhirCache = options.cachePath ?? path.join(os.homedir(), '.fhir', 'packages'); + const packageCache = new DiskBasedPackageCache(fhirCache, [], { log }); + const registryClient = new DefaultRegistryClient({ log }); + const buildClient = new BuildDotFhirDotOrgClient({ log }); + const loader = new BasePackageLoader(packageDB, packageCache, registryClient, buildClient, { + log + }); + + for (const pkg of fhirPackages) { + const [name, version] = pkg.split(/[#@]/, 2); + await loader.loadPackage(name, version); + } } async function app() { @@ -48,7 +63,7 @@ async function app() { .usage(' [options]') .argument( '', - 'list of FHIR packages to load using the format packageId@packageVersion...' + 'list of FHIR packages to load using the format packageId#packageVersion or packageId@packageVersion' ) .option( '-c, --cachePath ', diff --git a/src/current/BuildDotFhirDotOrgClient.ts b/src/current/BuildDotFhirDotOrgClient.ts index b0b72d9..5ade78a 100644 --- a/src/current/BuildDotFhirDotOrgClient.ts +++ b/src/current/BuildDotFhirDotOrgClient.ts @@ -1,5 +1,6 @@ import { Readable } from 'stream'; import { LogFunction, axiosGet } from '../utils'; +import { CurrentPackageLoadError } from '../errors'; import { CurrentBuildClient, CurrentBuildClientOptions } from './CurrentBuildClient'; export class BuildDotFhirDotOrgClient implements CurrentBuildClient { @@ -12,7 +13,7 @@ export class BuildDotFhirDotOrgClient implements CurrentBuildClient { const version = branch ? `current$${branch}` : 'current'; const baseURL = await this.getCurrentBuildBaseURL(name, branch); if (!baseURL) { - throw new Error(`Failed to download ${name}#${version}`); + throw new CurrentPackageLoadError(`${name}#${version}`); } const url = `${baseURL}/package.tgz`; this.log('info', `Attempting to download ${name}#${version} from ${url}`); diff --git a/src/errors/InvalidPackageError.ts b/src/errors/InvalidPackageError.ts deleted file mode 100644 index 0756290..0000000 --- a/src/errors/InvalidPackageError.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class InvalidPackageError extends Error { - constructor( - public packagePath: string, - public reason: string - ) { - super(`The package at ${packagePath} is not a valid FHIR package: ${reason}.`); - } -} diff --git a/src/errors/PackageLoadError.ts b/src/errors/PackageLoadError.ts deleted file mode 100644 index 7ac0caf..0000000 --- a/src/errors/PackageLoadError.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class PackageLoadError extends Error { - specReferences = ['https://confluence.hl7.org/display/FHIR/NPM+Package+Specification']; - constructor( - public fullPackageName: string, - public customRegistry?: string - ) { - super( - `The package ${fullPackageName} could not be loaded locally or from the ${ - customRegistry ? 'custom ' : '' - }FHIR package registry${customRegistry ? ` ${customRegistry}` : ''}.` - ); - } -} diff --git a/src/errors/index.ts b/src/errors/index.ts index ccb9e9f..37ddfc6 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,6 +1,4 @@ export * from './CurrentPackageLoadError'; export * from './IncorrectWildcardVersionFormatError'; -export * from './InvalidPackageError'; export * from './InvalidResourceError'; export * from './LatestVersionUnavailableError'; -export * from './PackageLoadError'; diff --git a/src/index.ts b/src/index.ts index cdb3c5d..6a2f5ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,4 @@ -export * from './api'; export * from './errors'; -export * from './FHIRDefinitions'; -export * from './load'; export * from './utils'; export * from './cache'; export * from './current'; diff --git a/src/load.ts b/src/load.ts deleted file mode 100644 index cbf7db3..0000000 --- a/src/load.ts +++ /dev/null @@ -1,490 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import os from 'os'; -import { maxSatisfying } from 'semver'; -import tar from 'tar'; -import temp from 'temp'; -import { - PackageLoadError, - CurrentPackageLoadError, - IncorrectWildcardVersionFormatError -} from './errors'; -import { FHIRDefinitions } from './FHIRDefinitions'; -import { LogFunction } from './utils'; -import { axiosGet } from './utils/axiosUtils'; -import { AxiosResponse } from 'axios'; -import { LatestVersionUnavailableError } from './errors/LatestVersionUnavailableError'; - -/** - * Loads multiple dependencies from a directory (the user FHIR cache or a specified directory) or from online - * @param {string[]} fhirPackages - An array of FHIR packages to download and load definitions from (format: packageId#version) - * @param {string} [cachePath=path.join(os.homedir(), '.fhir', 'packages')] - Path to look for the package and download to if not already present. Defaults to local FHIR cache. - * @param {LogFunction} [log=() => {}] - A function for logging. Defaults to no-op. - * @returns {Promise} the loaded FHIRDefinitions - */ -export async function loadDependencies( - fhirPackages: string[], - cachePath: string = path.join(os.homedir(), '.fhir', 'packages'), - log: LogFunction = () => {} -): Promise { - const promises = fhirPackages.map(fhirPackage => { - const [fhirPackageId, fhirPackageVersion] = fhirPackage.split('#'); - const fhirDefs = new FHIRDefinitions(); - // Testing Hack: Use exports.mergeDependency instead of mergeDependency so that this function - // calls the mocked mergeDependency in unit tests. In normal (non-test) use, this should - // have no negative effects. - return exports - .mergeDependency(fhirPackageId, fhirPackageVersion, fhirDefs, cachePath, log) - .catch((e: Error) => { - let message = `Failed to load ${fhirPackageId}#${fhirPackageVersion}: ${e.message}`; - if (/certificate/.test(e.message)) { - message += - '\n\nSometimes this error occurs in corporate or educational environments that use proxies and/or SSL ' + - 'inspection.\nTroubleshooting tips:\n' + - ' 1. If a non-proxied network is available, consider connecting to that network instead.\n' + - ' 2. Set NODE_EXTRA_CA_CERTS as described at https://bit.ly/3ghJqJZ (RECOMMENDED).\n' + - ' 3. Disable certificate validation as described at https://bit.ly/3syjzm7 (NOT RECOMMENDED).\n'; - } - log('error', message); - fhirDefs.unsuccessfulPackageLoad = true; - fhirDefs.package = `${fhirPackageId}#${fhirPackageVersion}`; - return fhirDefs; - }); - }); - return await Promise.all(promises).then(fhirDefs => { - if (fhirDefs.length > 1) { - const mainFHIRDefs = new FHIRDefinitions(); - fhirDefs.forEach(d => mainFHIRDefs.childFHIRDefs.push(d)); - return mainFHIRDefs; - } - return fhirDefs[0]; - }); -} - -/** - * Downloads a dependency from a directory (the user FHIR cache or a specified directory) or from online. - * The definitions from the package are added to their own FHIRDefinitions instance, which is then added to - * the provided FHIRDefs childDefs. If the provided FHIRDefs does not yet have any children, a wrapper FHIRDefinitions - * instance is created and both the original packages and the new package are added to childDefs. - * @param {string} packageName - The name of the package to load - * @param {string} version - The version of the package to load - * @param {FHIRDefinitions} FHIRDefs - The FHIRDefinitions to load the dependencies into - * @param {string} [cachePath=path.join(os.homedir(), '.fhir', 'packages')] - The path to load the package into (default: user FHIR cache) - * @returns {Promise} the loaded FHIRDefs - * @throws {PackageLoadError} when the desired package can't be loaded - */ -export async function loadDependency( - packageName: string, - version: string, - FHIRDefs: FHIRDefinitions, - cachePath: string = path.join(os.homedir(), '.fhir', 'packages'), - log: LogFunction = () => {} -): Promise { - const newFHIRDefs = new FHIRDefinitions(); - // Testing Hack: Use exports.mergeDependency instead of mergeDependency so that this function - // calls the mocked mergeDependency in unit tests. In normal (non-test) use, this should - // have no negative effects. - await exports.mergeDependency(packageName, version, newFHIRDefs, cachePath, log); - if (FHIRDefs.childFHIRDefs.length === 0) { - const wrapperFHIRDefs = new FHIRDefinitions(); - wrapperFHIRDefs.childFHIRDefs.push(FHIRDefs, newFHIRDefs); - return wrapperFHIRDefs; - } - FHIRDefs.childFHIRDefs.push(newFHIRDefs); - return FHIRDefs; -} - -/** - * Downloads a dependency from a directory (the user FHIR cache or a specified directory) or from online - * and then loads it into the FHIRDefinitions class provided - * Note: You likely want to use loadDependency, which adds the package to its own FHIRDefinitions class instance - * before appending that package to the provided FHIRDefinitions.childDefs array. This maintains the same structure - * that is created with loadDependencies. - * @param {string} packageName - The name of the package to load - * @param {string} version - The version of the package to load - * @param {FHIRDefinitions} FHIRDefs - The FHIRDefinitions to load the dependencies into - * @param {string} [cachePath=path.join(os.homedir(), '.fhir', 'packages')] - The path to load the package into (default: user FHIR cache) - * @returns {Promise} the loaded FHIRDefs - * @throws {PackageLoadError} when the desired package can't be loaded - */ -export async function mergeDependency( - packageName: string, - version: string, - FHIRDefs: FHIRDefinitions, - cachePath: string = path.join(os.homedir(), '.fhir', 'packages'), - log: LogFunction = () => {} -): Promise { - if (version === 'latest') { - // using the exported function here to allow for easier mocking in tests - version = await exports.lookUpLatestVersion(packageName, log); - } else if (/^\d+\.\d+\.x$/.test(version)) { - // using the exported function here to allow for easier mocking in tests - version = await exports.lookUpLatestPatchVersion(packageName, version, log); - } else if (/^\d+\.x$/.test(version)) { - throw new IncorrectWildcardVersionFormatError(packageName, version); - } - let fullPackageName = `${packageName}#${version}`; - const loadPath = path.join(cachePath, fullPackageName, 'package'); - let loadedPackage: string; - - // First, try to load the package from the local cache - log('info', `Checking ${cachePath} for ${fullPackageName}...`); - loadedPackage = loadFromPath(cachePath, fullPackageName, FHIRDefs); - if (loadedPackage) { - log('info', `Found ${fullPackageName} in ${cachePath}.`); - } else { - log('info', `Did not find ${fullPackageName} in ${cachePath}.`); - } - - // When a dev package is not present locally, fall back to using the current version - // as described here https://confluence.hl7.org/pages/viewpage.action?pageId=35718627#IGPublisherDocumentation-DependencyList - if (version === 'dev' && !loadedPackage) { - log( - 'info', - `Falling back to ${packageName}#current since ${fullPackageName} is not locally cached. To avoid this, add ${fullPackageName} to your local FHIR cache by building it locally with the HL7 FHIR IG Publisher.` - ); - version = 'current'; - fullPackageName = `${packageName}#${version}`; - loadedPackage = loadFromPath(cachePath, fullPackageName, FHIRDefs); - } - - let packageUrl; - if (packageName.startsWith('hl7.fhir.r5.') && version === 'current') { - packageUrl = `https://build.fhir.org/${packageName}.tgz`; - // TODO: Figure out how to determine if the cached package is current - // See: https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/Registry.20for.20FHIR.20Core.20packages.20.3E.204.2E0.2E1 - if (loadedPackage) { - log( - 'info', - `Downloading ${fullPackageName} since FHIR Package Loader cannot determine if the version in ${cachePath} is the most recent build.` - ); - } - } else if (/^current(\$.+)?$/.test(version)) { - // Authors can reference a specific CI branch by specifying version as current${branchname} (e.g., current$mybranch) - // See: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/Package.20cache.20-.20multiple.20dev.20versions/near/291131585 - let branch: string; - if (version.indexOf('$') !== -1) { - branch = version.slice(version.indexOf('$') + 1); - } - - // Even if a local current package is loaded, we must still check that the local package date matches - // the date on the most recent version on build.fhir.org. If the date does not match, we re-download to the cache - type QAEntry = { 'package-id': string; date: string; repo: string }; - const baseUrl = 'https://build.fhir.org/ig'; - const res = await axiosGet(`${baseUrl}/qas.json`); - const qaData: QAEntry[] = res?.data; - // Find matching packages and sort by date to get the most recent - let newestPackage: QAEntry; - if (qaData?.length > 0) { - let matchingPackages = qaData.filter(p => p['package-id'] === packageName); - if (branch == null) { - matchingPackages = matchingPackages.filter(p => p.repo.match(/\/(master|main)\/qa\.json$/)); - } else { - matchingPackages = matchingPackages.filter(p => p.repo.endsWith(`/${branch}/qa.json`)); - } - newestPackage = matchingPackages.sort((p1, p2) => { - return Date.parse(p2['date']) - Date.parse(p1['date']); - })[0]; - } - if (newestPackage?.repo) { - const packagePath = newestPackage.repo.slice(0, -8); // remove "/qa.json" from end - const igUrl = `${baseUrl}/${packagePath}`; - // get the package.manifest.json for the newest version of the package on build.fhir.org - const manifest = await axiosGet(`${igUrl}/package.manifest.json`); - let cachedPackageJSON; - if (fs.existsSync(path.join(loadPath, 'package.json'))) { - cachedPackageJSON = fs.readJSONSync(path.join(loadPath, 'package.json')); - } - // if the date on the package.manifest.json does not match the date on the cached package - // set the packageUrl to trigger a re-download of the package - if (manifest?.data?.date !== cachedPackageJSON?.date) { - packageUrl = `${igUrl}/package.tgz`; - if (cachedPackageJSON) { - log( - 'debug', - `Cached package date for ${fullPackageName} (${formatDate( - cachedPackageJSON.date - )}) does not match last build date on build.fhir.org (${formatDate( - manifest?.data?.date - )})` - ); - log( - 'info', - `Cached package ${fullPackageName} is out of date and will be replaced by the more recent version found on build.fhir.org.` - ); - } - } else { - log( - 'debug', - `Cached package date for ${fullPackageName} (${formatDate( - cachedPackageJSON.date - )}) matches last build date on build.fhir.org (${formatDate( - manifest?.data?.date - )}), so the cached package will be used` - ); - } - } else { - throw new CurrentPackageLoadError(fullPackageName); - } - } else if (!loadedPackage) { - const customRegistry = getCustomRegistry(log); - if (customRegistry) { - packageUrl = await getDistUrl(customRegistry, packageName, version); - } else { - packageUrl = `https://packages.fhir.org/${packageName}/${version}`; - } - } - - // If the packageUrl is set, we must download the package from that url, and extract it to our local cache - if (packageUrl) { - const doDownload = async (url: string) => { - log('info', `Downloading ${fullPackageName}... ${url}`); - const res = await axiosGet(url, { - responseType: 'arraybuffer' - }); - if (res?.data) { - log('info', `Downloaded ${fullPackageName}`); - // Create a temporary file and write the package to there - temp.track(); - const tempFile = temp.openSync(); - fs.writeFileSync(tempFile.path, res.data); - // Extract the package to a temporary directory - const tempDirectory = temp.mkdirSync(); - tar.x({ - cwd: tempDirectory, - file: tempFile.path, - sync: true, - strict: true - }); - cleanCachedPackage(tempDirectory); - // Add or replace the package in the FHIR cache - const targetDirectory = path.join(cachePath, fullPackageName); - if (fs.existsSync(targetDirectory)) { - fs.removeSync(targetDirectory); - } - fs.moveSync(tempDirectory, targetDirectory); - // Now try to load again from the path - loadedPackage = loadFromPath(cachePath, fullPackageName, FHIRDefs); - } else { - log('info', `Unable to download most current version of ${fullPackageName}`); - } - }; - try { - await doDownload(packageUrl); - } catch (e) { - if (packageUrl === `https://packages.fhir.org/${packageName}/${version}`) { - // It didn't exist in the normal registry. Fallback to packages2 registry. - // See: https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/Registry.20for.20FHIR.20Core.20packages.20.3E.204.2E0.2E1 - // See: https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/fhir.2Edicom/near/262334652 - packageUrl = `https://packages2.fhir.org/packages/${packageName}/${version}`; - try { - await doDownload(packageUrl); - } catch (e) { - throw new PackageLoadError(fullPackageName); - } - } else { - throw new PackageLoadError(fullPackageName, getCustomRegistry()); - } - } - } - - if (!loadedPackage) { - // If we fail again, then we couldn't get the package locally or from online - throw new PackageLoadError(fullPackageName, getCustomRegistry()); - } - log('info', `Loaded package ${fullPackageName}`); - return FHIRDefs; -} - -/** - * This function takes a package which contains contents at the same level as the "package" folder, and nests - * all that content within the "package" folder. - * - * A package should have the format described here https://confluence.hl7.org/pages/viewpage.action?pageId=35718629#NPMPackageSpecification-Format - * in which all contents are within the "package" folder. Some packages (ex US Core 3.1.0) have an incorrect format in which folders - * are not sub-folders of "package", but are instead at the same level. The IG Publisher fixes these packages as described - * https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/dev.20dependencies, so we should as well. - * - * @param {string} packageDirectory - The directory containing the package - */ -export function cleanCachedPackage(packageDirectory: string): void { - if (fs.existsSync(path.join(packageDirectory, 'package'))) { - fs.readdirSync(packageDirectory) - .filter(file => file !== 'package') - .forEach(file => { - fs.renameSync( - path.join(packageDirectory, file), - path.join(packageDirectory, 'package', file) - ); - }); - } -} - -/** - * Locates the targetPackage within the cachePath and loads the set of JSON files into FHIRDefs - * @param {string} cachePath - The path to the directory containing cached packages - * @param {string} targetPackage - The name of the package we are trying to load - * @param {FHIRDefinitions} FHIRDefs - The FHIRDefinitions object to load defs into - * @returns {string} the name of the loaded package if successful - */ -export function loadFromPath( - cachePath: string, - targetPackage: string, - FHIRDefs: FHIRDefinitions -): string { - const originalSize = FHIRDefs.size(); - const packages = fs.existsSync(cachePath) ? fs.readdirSync(cachePath) : []; - const cachedPackage = packages.find(packageName => packageName.toLowerCase() === targetPackage); - if (cachedPackage) { - const files = fs.readdirSync(path.join(cachePath, cachedPackage, 'package')); - for (const file of files) { - if (file.endsWith('.json')) { - const def = JSON.parse( - fs.readFileSync(path.join(cachePath, cachedPackage, 'package', file), 'utf-8').trim() - ); - FHIRDefs.add(def); - if (file === 'package.json') { - FHIRDefs.addPackageJson(targetPackage, def); - } - } - } - } - // If we did successfully load definitions, mark this package as loaded - if (FHIRDefs.size() > originalSize) { - FHIRDefs.package = targetPackage; - return targetPackage; - } - // If the package has already been loaded, just return the targetPackage string - if (FHIRDefs.package === targetPackage) { - return targetPackage; - } - // This last case is to ensure SUSHI (which uses a single FHIRDefinitions class for many packages) - // can tell if a package has already be loaded. We don't have access to the array of package names - // that SUSHI keeps track of, so we check for the package.json of the targetPackage. If it's there, - // the package has already been loaded, so just return the targetPackage string. - if (FHIRDefs.getPackageJson(targetPackage)) { - return targetPackage; - } -} - -export async function lookUpLatestVersion( - packageName: string, - log: LogFunction = () => {} -): Promise { - const customRegistry = getCustomRegistry(log); - let res: AxiosResponse; - try { - if (customRegistry) { - res = await axiosGet(`${customRegistry.replace(/\/$/, '')}/${packageName}`, { - responseType: 'json' - }); - } else { - try { - res = await axiosGet(`https://packages.fhir.org/${packageName}`, { - responseType: 'json' - }); - } catch (e) { - // Fallback to trying packages2.fhir.org - res = await axiosGet(`https://packages2.fhir.org/packages/${packageName}`, { - responseType: 'json' - }); - } - } - } catch { - throw new LatestVersionUnavailableError(packageName, customRegistry); - } - - if (res?.data?.['dist-tags']?.latest?.length) { - return res.data['dist-tags'].latest; - } else { - throw new LatestVersionUnavailableError(packageName, customRegistry); - } -} - -export async function lookUpLatestPatchVersion( - packageName: string, - version: string, - log: LogFunction = () => {} -): Promise { - if (!/^\d+\.\d+\.x$/.test(version)) { - throw new IncorrectWildcardVersionFormatError(packageName, version); - } - const customRegistry = getCustomRegistry(log); - let res: AxiosResponse; - try { - if (customRegistry) { - res = await axiosGet(`${customRegistry.replace(/\/$/, '')}/${packageName}`, { - responseType: 'json' - }); - } else { - try { - res = await axiosGet(`https://packages.fhir.org/${packageName}`, { - responseType: 'json' - }); - } catch (e) { - // Fallback to trying packages2.fhir.org - res = await axiosGet(`https://packages2.fhir.org/packages/${packageName}`, { - responseType: 'json' - }); - } - } - } catch { - throw new LatestVersionUnavailableError(packageName, customRegistry, true); - } - - if (res?.data?.versions) { - const versions = Object.keys(res.data.versions); - const latest = maxSatisfying(versions, version); - if (latest == null) { - throw new LatestVersionUnavailableError(packageName, customRegistry, true); - } - return latest; - } else { - throw new LatestVersionUnavailableError(packageName, customRegistry, true); - } -} - -/** - * Takes a date in format YYYYMMDDHHmmss and converts to YYYY-MM-DDTHH:mm:ss - * @param {string} date - The date to format - * @returns {string} the formatted date - */ -function formatDate(date: string): string { - return date - ? date.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6') - : ''; -} - -let hasLoggedCustomRegistry = false; - -export function getCustomRegistry(log: LogFunction = () => {}) { - if (process.env.FPL_REGISTRY) { - if (!hasLoggedCustomRegistry) { - hasLoggedCustomRegistry = true; - log( - 'info', - `Using custom registry specified by FPL_REGISTRY environment variable: ${process.env.FPL_REGISTRY}` - ); - } - return process.env.FPL_REGISTRY; - } -} - -export async function getDistUrl( - registry: string, - packageName: string, - version: string -): Promise { - const cleanedRegistry = registry.replace(/\/$/, ''); - // 1 get the manifest information about the package from the registry - const res = await axiosGet(`${cleanedRegistry}/${packageName}`); - // 2 find the NPM tarball location - const npmLocation = res.data?.versions?.[version]?.dist?.tarball; - - // 3 if found, use it, otherwise fallback to the FHIR spec location - if (npmLocation) { - return npmLocation; - } else { - return `${cleanedRegistry}/${packageName}/${version}`; - } -} diff --git a/src/loader/DefaultPackageLoader.ts b/src/loader/DefaultPackageLoader.ts index 30818d8..fe0dbbe 100644 --- a/src/loader/DefaultPackageLoader.ts +++ b/src/loader/DefaultPackageLoader.ts @@ -7,6 +7,8 @@ import { DefaultRegistryClient } from '../registry'; import { DiskBasedPackageCache } from '../cache/DiskBasedPackageCache'; import { BasePackageLoader, BasePackageLoaderOptions } from './BasePackageLoader'; +// TODO: New options w/ option for overriding FHIR cache + export async function defaultPackageLoader(options: BasePackageLoaderOptions) { return defaultPackageLoaderWithLocalResources([], options); } diff --git a/src/utils/DoubleMap.ts b/src/utils/DoubleMap.ts deleted file mode 100644 index 793b52d..0000000 --- a/src/utils/DoubleMap.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * The DoubleMap is a Map that contains both forward and reverse mappings between keys and values. - * This allows the DoubleMap to easily provide a list of unique values, - * because each value in the internal forwardMap will be a key in the reverseMap. - * The reported size of a DoubleMap is the number of unique values, - * which is the number of keys in the reverseMap. - * - * Note that because DoubleMap.values() returns the keys from reverseMap, - * it may contain fewer elements than the other functions: keys(), entries(), forEach(), and the for-of iterator. - */ -export class DoubleMap implements Map { - private forwardMap: Map; - private reverseMap: Map>; - - constructor() { - this.forwardMap = new Map(); - this.reverseMap = new Map(); - } - - set(key: K, value: V): this { - if (this.forwardMap.get(key) === value) { - return this; - } - this.delete(key); - this.forwardMap.set(key, value); - if (this.reverseMap.has(value)) { - this.reverseMap.get(value).add(key); - } else { - this.reverseMap.set(value, new Set([key])); - } - return this; - } - - delete(key: K): boolean { - if (this.forwardMap.has(key)) { - const currentValue = this.forwardMap.get(key); - const currentKeys = this.reverseMap.get(currentValue); - currentKeys.delete(key); - if (currentKeys.size === 0) { - this.reverseMap.delete(currentValue); - } - this.forwardMap.delete(key); - return true; - } else { - return false; - } - } - - get(key: K): V { - return this.forwardMap.get(key); - } - - get size(): number { - return this.reverseMap.size; - } - - clear(): void { - this.forwardMap.clear(); - this.reverseMap.clear(); - } - - forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { - this.forwardMap.forEach(callbackfn, thisArg); - } - - has(key: K): boolean { - return this.forwardMap.has(key); - } - - [Symbol.iterator](): IterableIterator<[K, V]> { - return this.entries(); - } - - entries(): IterableIterator<[K, V]> { - return this.forwardMap.entries(); - } - - keys(): IterableIterator { - return this.forwardMap.keys(); - } - - values(): IterableIterator { - return this.reverseMap.keys(); - } - - [Symbol.toStringTag]: string; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 5c6f1ab..569fc5c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,2 @@ export * from './logger'; -export * from './DoubleMap'; export * from './axiosUtils'; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 1ef238f..d89c0d4 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -29,25 +29,4 @@ export const logger = createLogger({ transports: [new transports.Console()] }); -export class ErrorsAndWarnings { - public errors: string[] = []; - public warnings: string[] = []; - - reset(): void { - this.errors = []; - this.warnings = []; - } -} - -export const wrapLogger = (log: LogFunction = () => {}, errorsAndWarnings: ErrorsAndWarnings) => { - return (level: string, message: string) => { - if (level === 'error') { - errorsAndWarnings.errors.push(message); - } else if (level === 'warn') { - errorsAndWarnings.warnings.push(message); - } - log(level, message); - }; -}; - export type LogFunction = (level: string, message: string) => void; diff --git a/test/FHIRDefinitions.test.ts b/test/FHIRDefinitions.test.ts deleted file mode 100644 index b1c2cc3..0000000 --- a/test/FHIRDefinitions.test.ts +++ /dev/null @@ -1,412 +0,0 @@ -import path from 'path'; -import { loadFromPath } from '../src/load'; -import { FHIRDefinitions, Type } from '../src/FHIRDefinitions'; -import { loggerSpy } from './testhelpers'; - -describe('FHIRDefinitions', () => { - let defs: FHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, 'testhelpers', 'testdefs'), 'r4-definitions', defs); - }); - - beforeEach(() => { - loggerSpy.reset(); - }); - - describe('#fishForFHIR()', () => { - it('should find base FHIR resources', () => { - const conditionByID = defs.fishForFHIR('Condition', Type.Resource); - expect(conditionByID.url).toBe('http://hl7.org/fhir/StructureDefinition/Condition'); - expect(conditionByID.fhirVersion).toBe('4.0.1'); - expect( - defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/Condition', Type.Resource) - ).toEqual(conditionByID); - }); - - it('should find base FHIR logical models', () => { - const eLTSSServiceModelByID = defs.fishForFHIR('eLTSSServiceModel', Type.Logical); - expect(eLTSSServiceModelByID.url).toBe( - 'http://hl7.org/fhir/us/eltss/StructureDefinition/eLTSSServiceModel' - ); - expect(eLTSSServiceModelByID.version).toBe('0.1.0'); - expect( - defs.fishForFHIR( - 'http://hl7.org/fhir/us/eltss/StructureDefinition/eLTSSServiceModel', - Type.Logical - ) - ).toEqual(eLTSSServiceModelByID); - }); - - it('should find base FHIR primitive types', () => { - const booleanByID = defs.fishForFHIR('boolean', Type.Type); - expect(booleanByID.url).toBe('http://hl7.org/fhir/StructureDefinition/boolean'); - expect(booleanByID.fhirVersion).toBe('4.0.1'); - expect( - defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/boolean', Type.Type) - ).toEqual(booleanByID); - }); - - it('should find base FHIR complex types', () => { - const addressByID = defs.fishForFHIR('Address', Type.Type); - expect(addressByID.url).toBe('http://hl7.org/fhir/StructureDefinition/Address'); - expect(addressByID.fhirVersion).toBe('4.0.1'); - expect( - defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/Address', Type.Type) - ).toEqual(addressByID); - }); - - it('should find base FHIR profiles', () => { - const vitalSignsByID = defs.fishForFHIR('vitalsigns', Type.Profile); - expect(vitalSignsByID.url).toBe('http://hl7.org/fhir/StructureDefinition/vitalsigns'); - expect(vitalSignsByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('observation-vitalsigns', Type.Profile)).toEqual(vitalSignsByID); - expect( - defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/vitalsigns', Type.Profile) - ).toEqual(vitalSignsByID); - }); - - it('should find base FHIR profiles of logical models', () => { - const serviceProfileByID = defs.fishForFHIR('service-profile', Type.Profile); - expect(serviceProfileByID.url).toBe( - 'http://hl7.org/fhir/some/example/StructureDefinition/ServiceProfile' - ); - expect(serviceProfileByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('ServiceProfile', Type.Profile)).toEqual(serviceProfileByID); - expect( - defs.fishForFHIR( - 'http://hl7.org/fhir/some/example/StructureDefinition/ServiceProfile', - Type.Profile - ) - ).toEqual(serviceProfileByID); - }); - - it('should find base FHIR extensions', () => { - const maidenNameExtensionByID = defs.fishForFHIR('patient-mothersMaidenName', Type.Extension); - expect(maidenNameExtensionByID.url).toBe( - 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName' - ); - expect(maidenNameExtensionByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('mothersMaidenName', Type.Extension)).toEqual( - maidenNameExtensionByID - ); - expect( - defs.fishForFHIR( - 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName', - Type.Extension - ) - ).toEqual(maidenNameExtensionByID); - }); - - it('should find base FHIR value sets', () => { - const allergyStatusValueSetByID = defs.fishForFHIR( - 'allergyintolerance-clinical', - Type.ValueSet - ); - expect(allergyStatusValueSetByID.url).toBe( - 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical' - ); - // For some reason, value sets don't specify a fhirVersion, but in this case the business - // version is the FHIR version, so we'll verify that instead - expect(allergyStatusValueSetByID.version).toBe('4.0.1'); - expect(defs.fishForFHIR('AllergyIntoleranceClinicalStatusCodes', Type.ValueSet)).toEqual( - allergyStatusValueSetByID - ); - expect( - defs.fishForFHIR('http://hl7.org/fhir/ValueSet/allergyintolerance-clinical', Type.ValueSet) - ).toEqual(allergyStatusValueSetByID); - }); - - it('should find base FHIR code systems', () => { - // Surprise! It turns out that the AllergyIntolerance status value set and code system - // have the same ID! - const allergyStatusCodeSystemByID = defs.fishForFHIR( - 'allergyintolerance-clinical', - Type.CodeSystem - ); - expect(allergyStatusCodeSystemByID.url).toBe( - 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical' - ); - // For some reason, code systems don't specify a fhirVersion, but in this case the business - // version is the FHIR version, so we'll verify that instead - expect(allergyStatusCodeSystemByID.version).toBe('4.0.1'); - expect(defs.fishForFHIR('AllergyIntoleranceClinicalStatusCodes', Type.CodeSystem)).toEqual( - allergyStatusCodeSystemByID - ); - expect( - defs.fishForFHIR( - 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical', - Type.CodeSystem - ) - ).toEqual(allergyStatusCodeSystemByID); - }); - - it('should find definitions by the enforced type order', () => { - // NOTE: There are two things with id allergyintolerance-clinical (the ValueSet and CodeSystem) - // When doing a non-type-specific search, we favor the ValueSet - const allergyStatusValueSetByID = defs.fishForFHIR( - 'allergyintolerance-clinical', - Type.ValueSet, - Type.CodeSystem - ); - expect(allergyStatusValueSetByID.resourceType).toBe('ValueSet'); - - const allergyStatusCodeSystemByID = defs.fishForFHIR( - 'allergyintolerance-clinical', - Type.CodeSystem, - Type.ValueSet - ); - expect(allergyStatusCodeSystemByID.resourceType).toBe('ValueSet'); - }); - - it('should not find the definition when the type is not requested', () => { - const conditionByID = defs.fishForFHIR( - 'Condition', - Type.Logical, - Type.Type, - Type.Profile, - Type.Extension, - Type.ValueSet, - Type.CodeSystem, - Type.Instance - ); - expect(conditionByID).toBeUndefined(); - - const booleanByID = defs.fishForFHIR( - 'boolean', - Type.Resource, - Type.Logical, - Type.Profile, - Type.Extension, - Type.ValueSet, - Type.CodeSystem, - Type.Instance - ); - expect(booleanByID).toBeUndefined(); - - const addressByID = defs.fishForFHIR( - 'Address', - Type.Resource, - Type.Logical, - Type.Profile, - Type.Extension, - Type.ValueSet, - Type.CodeSystem, - Type.Instance - ); - expect(addressByID).toBeUndefined(); - - const vitalSignsProfileByID = defs.fishForFHIR( - 'vitalsigns', - Type.Resource, - Type.Logical, - Type.Type, - Type.Extension, - Type.ValueSet, - Type.CodeSystem, - Type.Instance - ); - expect(vitalSignsProfileByID).toBeUndefined(); - - const maidenNameExtensionByID = defs.fishForFHIR( - 'patient-mothersMaidenName', - Type.Resource, - Type.Logical, - Type.Type, - Type.Profile, - Type.ValueSet, - Type.CodeSystem, - Type.Instance - ); - expect(maidenNameExtensionByID).toBeUndefined(); - - // NOTE: There are two things with id allergyintolerance-clinical (the ValueSet and CodeSystem) - const allergyStatusValueSetByID = defs.fishForFHIR( - 'allergyintolerance-clinical', - Type.Resource, - Type.Logical, - Type.Type, - Type.Profile, - Type.Extension, - Type.Instance - ); - expect(allergyStatusValueSetByID).toBeUndefined(); - - const w3cProvenanceCodeSystemByID = defs.fishForFHIR( - 'w3c-provenance-activity-type', - Type.Resource, - Type.Logical, - Type.Type, - Type.Profile, - Type.Extension, - Type.ValueSet, - Type.Instance - ); - expect(w3cProvenanceCodeSystemByID).toBeUndefined(); - - const eLTSSServiceModelByID = defs.fishForFHIR( - 'eLTSSServiceModel', - Type.Resource, - Type.Type, - Type.Profile, - Type.Extension, - Type.ValueSet, - Type.Instance - ); - expect(eLTSSServiceModelByID).toBeUndefined(); - }); - - it('should globally find any definition', () => { - const conditionByID = defs.fishForFHIR('Condition'); - expect(conditionByID.kind).toBe('resource'); - expect(conditionByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/Condition')).toEqual( - conditionByID - ); - - const booleanByID = defs.fishForFHIR('boolean'); - expect(booleanByID.kind).toBe('primitive-type'); - expect(booleanByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/boolean')).toEqual( - booleanByID - ); - - const addressByID = defs.fishForFHIR('Address'); - expect(addressByID.kind).toBe('complex-type'); - expect(addressByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/Address')).toEqual( - addressByID - ); - - const vitalSignsProfileByID = defs.fishForFHIR('vitalsigns'); - expect(vitalSignsProfileByID.type).toBe('Observation'); - expect(vitalSignsProfileByID.kind).toBe('resource'); - expect(vitalSignsProfileByID.derivation).toBe('constraint'); - expect(vitalSignsProfileByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('observation-vitalsigns')).toEqual(vitalSignsProfileByID); - expect(defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/vitalsigns')).toEqual( - vitalSignsProfileByID - ); - - const maidenNameExtensionByID = defs.fishForFHIR('patient-mothersMaidenName'); - expect(maidenNameExtensionByID.type).toBe('Extension'); - expect(maidenNameExtensionByID.fhirVersion).toBe('4.0.1'); - expect(defs.fishForFHIR('mothersMaidenName')).toEqual(maidenNameExtensionByID); - expect( - defs.fishForFHIR('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName') - ).toEqual(maidenNameExtensionByID); - - // NOTE: There are two things with id allergyintolerance-clinical (the ValueSet and CodeSystem) - // When doing a non-type-specific search, we favor the ValueSet - const allergyStatusValueSetByID = defs.fishForFHIR('allergyintolerance-clinical'); - expect(allergyStatusValueSetByID.resourceType).toBe('ValueSet'); - // For some reason, value sets don't specify a fhirVersion, but in this case the business - // version is the FHIR version, so we'll verify that instead - expect(allergyStatusValueSetByID.version).toBe('4.0.1'); - expect(defs.fishForFHIR('AllergyIntoleranceClinicalStatusCodes')).toEqual( - allergyStatusValueSetByID - ); - expect(defs.fishForFHIR('http://hl7.org/fhir/ValueSet/allergyintolerance-clinical')).toEqual( - allergyStatusValueSetByID - ); - - const w3cProvenanceCodeSystemByID = defs.fishForFHIR('w3c-provenance-activity-type'); - expect(w3cProvenanceCodeSystemByID.resourceType).toBe('CodeSystem'); - // For some reason, code systems don't specify a fhirVersion, but in this case the business - // version is the FHIR version, so we'll verify that instead - expect(w3cProvenanceCodeSystemByID.version).toBe('4.0.1'); - expect(defs.fishForFHIR('W3cProvenanceActivityType')).toEqual(w3cProvenanceCodeSystemByID); - expect(defs.fishForFHIR('http://hl7.org/fhir/w3c-provenance-activity-type')).toEqual( - w3cProvenanceCodeSystemByID - ); - - const eLTSSServiceModelByID = defs.fishForFHIR('eLTSSServiceModel'); - expect(eLTSSServiceModelByID.kind).toBe('logical'); - expect(eLTSSServiceModelByID.derivation).toBe('specialization'); - expect( - defs.fishForFHIR('http://hl7.org/fhir/us/eltss/StructureDefinition/eLTSSServiceModel') - ).toEqual(eLTSSServiceModelByID); - }); - - it('should find definition in parent defs before searching in children', () => { - const defsWithChildDefs = new FHIRDefinitions(); - - // package1 does not contain a Condition resource - const childDefs1 = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, 'testhelpers', 'testdefs'), 'package1', childDefs1); - childDefs1.package = 'package1'; - - // package2 contains a Condition resource with version 4.0.2 - const childDefs2 = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, 'testhelpers', 'testdefs'), 'package2', childDefs2); - childDefs2.package = 'package2'; - - // package3 contains a Condition resource with version 4.0.3 - const childDefs3 = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, 'testhelpers', 'testdefs'), 'package3', childDefs3); - childDefs3.package = 'package3'; - - // childDefs1 and childDefs2 are siblings, childDef3 is child of childDef1 - childDefs1.childFHIRDefs.push(childDefs3); - defsWithChildDefs.childFHIRDefs.push(childDefs1); - defsWithChildDefs.childFHIRDefs.push(childDefs2); - - // fishForFHIR should find the first level child (childDefs2) Condition before - // it finds the second level child (childDefs3) Condition - const conditionByID = defsWithChildDefs.fishForFHIR('Condition', Type.Resource); - expect(conditionByID.version).toEqual('4.0.2'); - }); - - it('should find definitions when fished by id with version', () => { - const vitalSignsById = defs.fishForFHIR('vitalsigns|4.0.1', Type.Profile); - expect(vitalSignsById).toBeDefined(); - expect(vitalSignsById.name).toBe('observation-vitalsigns'); - expect(vitalSignsById.url).toBe('http://hl7.org/fhir/StructureDefinition/vitalsigns'); - expect(vitalSignsById.version).toBe('4.0.1'); - }); - - it('should find definitions when fished by name with version', () => { - const vitalSignsByName = defs.fishForFHIR('observation-vitalsigns|4.0.1', Type.Profile); - expect(vitalSignsByName).toBeDefined(); - expect(vitalSignsByName.id).toBe('vitalsigns'); - expect(vitalSignsByName.url).toBe('http://hl7.org/fhir/StructureDefinition/vitalsigns'); - expect(vitalSignsByName.version).toBe('4.0.1'); - }); - - it('should find definitions when fished by url with version', () => { - const vitalSignsByUrl = defs.fishForFHIR( - 'http://hl7.org/fhir/StructureDefinition/vitalsigns|4.0.1', - Type.Profile - ); - expect(vitalSignsByUrl).toBeDefined(); - expect(vitalSignsByUrl.id).toBe('vitalsigns'); - expect(vitalSignsByUrl.name).toBe('observation-vitalsigns'); - expect(vitalSignsByUrl.version).toBe('4.0.1'); - }); - - it('should find definitions with a version with | in the version', () => { - const simpleProfileById = defs.fishForFHIR('SimpleProfile|1.0.0|a'); - expect(simpleProfileById).toBeDefined(); - expect(simpleProfileById.id).toBe('SimpleProfile'); - expect(simpleProfileById.name).toBe('SimpleProfile'); - expect(simpleProfileById.version).toBe('1.0.0|a'); - }); - - it('should return nothing if a definition with matching version is not found', () => { - const vitalSignsById = defs.fishForFHIR('vitalsigns|1.0.0', Type.Profile); - const vitalSignsByName = defs.fishForFHIR('observation-vitalsigns|1.0.0', Type.Profile); - const vitalSignsByUrl = defs.fishForFHIR( - 'http://hl7.org/fhir/StructureDefinition/vitalsigns|1.0.0', - Type.Profile - ); - expect(vitalSignsById).toBeUndefined(); - expect(vitalSignsByName).toBeUndefined(); - expect(vitalSignsByUrl).toBeUndefined(); - }); - - it('should return nothing if a definition without a version is found when fishing with a version', () => { - const simpleProfileById = defs.fishForFHIR('SimpleProfileNoVersion|1.0.0'); - expect(simpleProfileById).toBeUndefined(); - }); - }); -}); diff --git a/test/api.test.ts b/test/api.test.ts deleted file mode 100644 index 10c20fc..0000000 --- a/test/api.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import path from 'path'; -import { fpl } from '../src/api'; -import { FHIRDefinitions } from '../src/FHIRDefinitions'; -import { ErrorsAndWarnings } from '../src/utils'; -import * as loadModule from '../src/load'; -import * as logModule from '../src/utils/logger'; - -describe('fpl (API)', () => { - let loadSpy: jest.SpyInstance; - const cachePath = path.join(__dirname, 'testhelpers', 'fixtures'); - const log = jest.fn(); - let wrapLoggerSpy: jest.SpyInstance; - - beforeAll(() => { - loadSpy = jest.spyOn(loadModule, 'loadDependencies').mockResolvedValue(new FHIRDefinitions()); - wrapLoggerSpy = jest.spyOn(logModule, 'wrapLogger'); - }); - - beforeEach(() => { - loadSpy.mockClear(); - wrapLoggerSpy.mockClear(); - }); - - it('should load dependencies from an array in format package@version', async () => { - const fhirPackages = ['hl7.fake.test.package@1.0.0', 'hl7.fake.test.package@2.0.0']; - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0', 'hl7.fake.test.package#2.0.0'], - cachePath, - expect.any(Function) - ); - }); - - it('should load dependencies from an array in format package#version', async () => { - const fhirPackages = ['hl7.fake.test.package#1.0.0', 'hl7.fake.test.package#2.0.0']; - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0', 'hl7.fake.test.package#2.0.0'], - cachePath, - expect.any(Function) - ); - }); - - it('should load dependencies from a comma separated list in format package@version', async () => { - const fhirPackages = - 'hl7.fake.test.package@1.0.0, hl7.fake.test.package@2.0.0,hl7.fake.test.package@3.0.0'; - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0', 'hl7.fake.test.package#2.0.0', 'hl7.fake.test.package#3.0.0'], - cachePath, - expect.any(Function) - ); - }); - - it('should load dependencies from a comma separated list in format package#version', async () => { - const fhirPackages = - 'hl7.fake.test.package#1.0.0, hl7.fake.test.package#2.0.0,hl7.fake.test.package@3.0.0'; - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0', 'hl7.fake.test.package#2.0.0', 'hl7.fake.test.package#3.0.0'], - cachePath, - expect.any(Function) - ); - }); - - it('should load dependencies from a comma separated list in both formats (separated by # and @)', async () => { - const fhirPackages = - 'hl7.fake.test.package#1.0.0, hl7.fake.test.package@2.0.0,hl7.fake.test.package@3.0.0'; - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0', 'hl7.fake.test.package#2.0.0', 'hl7.fake.test.package#3.0.0'], - cachePath, - expect.any(Function) - ); - }); - - it('should return list of packages that failed to load', async () => { - const failedFhirDefs = new FHIRDefinitions(); - failedFhirDefs.package = 'hl7.fake.test.package#1.0.0'; - failedFhirDefs.unsuccessfulPackageLoad = true; - loadSpy.mockResolvedValueOnce(failedFhirDefs); - const fhirPackages = 'hl7.fake.test.package@1.0.0'; - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: failedFhirDefs, - errors: [], - warnings: [], - failedPackages: ['hl7.fake.test.package#1.0.0'] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0'], - cachePath, - expect.any(Function) - ); - }); - - it('should call wrapLogger to set up a logger that tracks errors and warnings', async () => { - const fhirPackages = 'hl7.fake.test.package#1.0.0'; - await fpl(fhirPackages, { cachePath, log }); - expect(wrapLoggerSpy).toHaveBeenCalledTimes(1); - expect(wrapLoggerSpy).toHaveBeenCalledWith(log, new ErrorsAndWarnings()); - }); - - it('should call wrapLogger even if no log function is provided', async () => { - const fhirPackages = 'hl7.fake.test.package#1.0.0'; - await fpl(fhirPackages); // log option not provided - expect(wrapLoggerSpy).toHaveBeenCalledTimes(1); - expect(wrapLoggerSpy).toHaveBeenCalledWith(undefined, new ErrorsAndWarnings()); - }); - - it('should return errors and warnings when present', async () => { - // Remove the loadSpy mock so we can reach the error that gets thrown and caught from mergeDependency - loadSpy.mockRestore(); - // Spy on mergeDependency and reject so we can test an error is logged - jest.spyOn(loadModule, 'mergeDependency').mockRejectedValue(new Error('bad')); - - const fhirPackages = 'hl7.fake.test.package@1.0.0'; - const failedFhirDefs = new FHIRDefinitions(); - failedFhirDefs.package = 'hl7.fake.test.package#1.0.0'; - failedFhirDefs.unsuccessfulPackageLoad = true; - - await expect(fpl(fhirPackages, { cachePath, log })).resolves.toEqual({ - defs: failedFhirDefs, - errors: ['Failed to load hl7.fake.test.package#1.0.0: bad'], - warnings: [], - failedPackages: ['hl7.fake.test.package#1.0.0'] - }); - - // Reset the loadSpy back so that any tests that come after this one still mock out loadDependencies - loadSpy = jest.spyOn(loadModule, 'loadDependencies').mockResolvedValue(new FHIRDefinitions()); - }); - - it('should pass along undefined if cachePath option is not provided', async () => { - const fhirPackages = 'hl7.fake.test.package#1.0.0'; - await expect(fpl(fhirPackages, { log })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0'], - undefined, - expect.any(Function) - ); - }); - - it('should pass along a wrappedLog function even if log option is not provided', async () => { - const fhirPackages = 'hl7.fake.test.package#1.0.0'; - await expect(fpl(fhirPackages, { cachePath })).resolves.toEqual({ - defs: new FHIRDefinitions(), - errors: [], - warnings: [], - failedPackages: [] - }); - expect(loadSpy).toHaveBeenCalledWith( - ['hl7.fake.test.package#1.0.0'], - cachePath, - expect.any(Function) // A function is passed even though options.log is undefined - ); - }); -}); diff --git a/test/current/BuildDotFhirDotOrgClient.test.ts b/test/current/BuildDotFhirDotOrgClient.test.ts index d47d191..ca6f3b3 100644 --- a/test/current/BuildDotFhirDotOrgClient.test.ts +++ b/test/current/BuildDotFhirDotOrgClient.test.ts @@ -7,15 +7,15 @@ import { Readable } from 'stream'; describe('BuildDotFhirDotOrgClient', () => { const client = new BuildDotFhirDotOrgClient({ log: loggerSpy.log }); let axiosSpy: jest.SpyInstance; - + describe('#downloadCurrentBuild', () => { - + describe('currentBuildNoBranch', () => { - + beforeEach(() => { loggerSpy.reset(); }); - + beforeAll(() => { axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { if (uri === 'https://build.fhir.org/ig/qas.json') { @@ -77,14 +77,14 @@ describe('BuildDotFhirDotOrgClient', () => { }, ] }; - } else if ( + } else if ( uri === 'https://build.fhir.org/ig/HL7/simple-US-Core-R4/branches/main/package.tgz' ) { return { status: 200, - data: Readable.from(['simple zipfile']) + data: Readable.from(['simple zipfile']) }; - } else if ( + } else if ( uri === 'https://build.fhir.org/ig/sushi/sushi-test/branches/master/package.tgz' ) { return { @@ -92,7 +92,7 @@ describe('BuildDotFhirDotOrgClient', () => { data: Readable.from(['sushi-test master zip file']) }; } else if ( - uri === 'https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.tgz' + uri === 'https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.tgz' ) { return { status: 200, @@ -101,59 +101,59 @@ describe('BuildDotFhirDotOrgClient', () => { } }); }); - + afterAll(() => { axiosSpy.mockRestore(); }); - + it ('should download the most current package from the main branch when no branch given', async () => { const latest = await client.downloadCurrentBuild('simple.hl7.fhir.us.core.r4', null); expect(loggerSpy.getLastMessage('info')).toBe('Attempting to download simple.hl7.fhir.us.core.r4#current from https://build.fhir.org/ig/HL7/simple-US-Core-R4/branches/main/package.tgz'); expect(latest).toBeInstanceOf(Readable); expect(latest.read()).toBe('simple zipfile'); }); - + it ('should try to download the current package from master branch if not specified', async () => { const latest = await client.downloadCurrentBuild('sushi-test', null); expect(loggerSpy.getLastMessage('info')).toBe('Attempting to download sushi-test#current from https://build.fhir.org/ig/sushi/sushi-test/branches/master/package.tgz'); expect(latest).toBeInstanceOf(Readable); expect(latest.read()).toBe('sushi-test master zip file'); }); - + it ('should download the most current package when a current package version has multiple versions', async () => { const latest = await client.downloadCurrentBuild('hl7.fhir.us.core.r4', null); expect(loggerSpy.getLastMessage('info')).toBe('Attempting to download hl7.fhir.us.core.r4#current from https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.tgz'); expect(latest).toBeInstanceOf(Readable); expect(latest.read()).toBe('current multiple version zip file'); }); - + it ('should throw error when invalid package name (unknown name) given', async () => { const latest = client.downloadCurrentBuild('invalid.pkg.name', null); - await expect(latest).rejects.toThrow(/Failed to download invalid.pkg.name#current/); + await expect(latest).rejects.toThrow(/The package invalid.pkg.name#current is not available/); }); - + it ('should throw error when invalid package name (empty string) given', async () => { const latest = client.downloadCurrentBuild('', null); - await expect(latest).rejects.toThrow(/Failed to download #current/); + await expect(latest).rejects.toThrow(/The package #current is not available/); }); - + it ('should not try to download the latest package from a branch that is not main/master if one is not available', async () => { const latest = client.downloadCurrentBuild('sushi-no-main', null); - await expect(latest).rejects.toThrow('Failed to download sushi-no-main#current'); + await expect(latest).rejects.toThrow(/The package sushi-no-main#current is not available/); }); - + it ('should throw error if able to find current build base url, but downloading does not find matching package', async () => { const latest = client.downloadCurrentBuild('sushi-test-no-download', null); await expect(latest).rejects.toThrow('Failed to download sushi-test-no-download#current from https://build.fhir.org/ig/sushi/sushi-test-no-download/branches/master/package.tgz'); }); }); - + describe('currentBuildGivenBranch', () => { - + beforeEach(() => { loggerSpy.reset(); }); - + beforeAll(() => { axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { if (uri === 'https://build.fhir.org/ig/qas.json') { @@ -183,7 +183,7 @@ describe('BuildDotFhirDotOrgClient', () => { status: 200, data: Readable.from(['zipfile']) }; - + } else if ( uri === 'https://build.fhir.org/ig/sushi/simple-test/branches/testbranchoneversion/package.tgz' ) { @@ -194,42 +194,42 @@ describe('BuildDotFhirDotOrgClient', () => { } }); }); - + afterAll(() => { axiosSpy.mockRestore(); }); - + it ('should download the package when branch name given', async () => { const latest = await client.downloadCurrentBuild('sushi-test', 'testbranch'); expect(loggerSpy.getLastMessage('info')).toBe('Attempting to download sushi-test#current$testbranch from https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.tgz'); expect(latest).toBeInstanceOf(Readable); expect(latest.read()).toBe('zipfile'); }); - + it ('should try to download the most recent branch-specific package when branch name given and multiple versions', async () => { const latest = await client.downloadCurrentBuild('simple-test', 'testbranchoneversion'); expect(loggerSpy.getLastMessage('info')).toBe('Attempting to download simple-test#current$testbranchoneversion from https://build.fhir.org/ig/sushi/simple-test/branches/testbranchoneversion/package.tgz'); expect(latest).toBeInstanceOf(Readable); expect(latest.read()).toBe('simple test one version branch'); }); - + it ('should throw error when invalid branch name (branch not available) given', async () => { const latest = client.downloadCurrentBuild('sushi-test', 'invalidbranchname'); - await expect(latest).rejects.toThrow('Failed to download sushi-test#current$invalidbranchname'); + await expect(latest).rejects.toThrow(/The package sushi-test#current\$invalidbranchname is not available/); }); - + it ('should throw error when invalid branch name (branch empty string) given', async () => { const latest = client.downloadCurrentBuild('sushi-test', ''); - await expect(latest).rejects.toThrow('Failed to download sushi-test#current'); + await expect(latest).rejects.toThrow(/The package sushi-test#current is not available/); }); }); - + describe('invalidBuild', () => { - + beforeEach(() => { loggerSpy.reset(); }); - + beforeAll(() => { axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { if (uri === 'https://build.fhir.org/ig/qas.json') { @@ -268,33 +268,33 @@ describe('BuildDotFhirDotOrgClient', () => { } }); }); - + afterAll(() => { axiosSpy.mockRestore(); }); - + it ('should throw error if download has 404 status', async () => { const latest = client.downloadCurrentBuild('sushi-test-bad-status', null); await expect(latest).rejects.toThrow('Failed to download sushi-test-bad-status#current from https://build.fhir.org/ig/sushi/sushi-test-bad-status/branches/master/package.tgz'); }); - + it ('should throw error if download has no data', async () => { const latest = client.downloadCurrentBuild('sushi-test-no-data', null); await expect(latest).rejects.toThrow('Failed to download sushi-test-no-data#current from https://build.fhir.org/ig/sushi/sushi-test-no-data/branches/master/package.tgz'); }); - + it ('should throw error if download is unsuccessful', async () => { const latest = client.downloadCurrentBuild('test-nodownload', null); - await expect(latest).rejects.toThrow('Failed to download test-nodownload#current'); + await expect(latest).rejects.toThrow(/The package test-nodownload#current is not available/); }); }); - + describe('#getCurrentBuildDate', () => { - + beforeEach(() => { loggerSpy.reset(); }); - + beforeAll(() => { axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { if (uri === 'https://build.fhir.org/ig/qas.json') { @@ -355,32 +355,32 @@ describe('BuildDotFhirDotOrgClient', () => { } }); }); - + afterAll(() => { axiosSpy.mockRestore(); }); - + it ('should get date from package main/master when no branch given', async () => { const latest = await client.getCurrentBuildDate('hl7.fhir.us.core.r4'); expect(latest).toBe('20200413230227'); }); - + it ('should get date from package from branch when branch is given', async () => { const latest = await client.getCurrentBuildDate('sushi-test', 'testbranch'); expect(latest).toBe('20240413230227'); }); - + it ('should return undefined when can not find current build base url ', async () => { const latest = await client.getCurrentBuildDate('wont.find.this.package.name'); expect(latest).toBeUndefined(); }); - + it ('should return undefined when can find build base url, but no manifest found', async () => { const latest = await client.getCurrentBuildDate('sushi-test-package-exists'); expect(latest).toBeUndefined(); }); - + it ('should return undefined when can find build base url, but manifest.data is null', async () => { const latest = await client.getCurrentBuildDate('sushi-test-manifest-empty'); expect(latest).toBeUndefined(); diff --git a/test/load.test.ts b/test/load.test.ts deleted file mode 100644 index f97ed4f..0000000 --- a/test/load.test.ts +++ /dev/null @@ -1,1362 +0,0 @@ -import axios from 'axios'; -import { cloneDeep } from 'lodash'; -import fs from 'fs-extra'; -import os from 'os'; -import process from 'process'; -import path from 'path'; -import tar from 'tar'; -import * as loadModule from '../src/load'; -import { - cleanCachedPackage, - loadFromPath, - mergeDependency, - loadDependencies, - loadDependency, - lookUpLatestVersion, - lookUpLatestPatchVersion -} from '../src/load'; -import { FHIRDefinitions, Type } from '../src/FHIRDefinitions'; -import { - IncorrectWildcardVersionFormatError, - LatestVersionUnavailableError, - PackageLoadError -} from '../src/errors'; -import { loggerSpy } from './testhelpers'; - -// Represents a typical response from packages.fhir.org -const TERM_PKG_RESPONSE = { - _id: 'hl7.terminology.r4', - name: 'hl7.terminology.r4', - 'dist-tags': { latest: '1.2.3-test' }, - versions: { - '1.2.3-test': { - name: 'hl7.terminology.r4', - version: '1.2.3-test', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe74983', - tarball: 'https://packages.simplifier.net/hl7.terminology.r4/1.2.3-test' - }, - fhirVersion: 'R4', - url: 'https://packages.simplifier.net/hl7.terminology.r4/1.2.3-test' - }, - '1.1.0': { - name: 'hl7.terminology.r4', - version: '1.1.0', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe749820', - tarball: 'https://packages.simplifier.net/hl7.terminology.r4/1.1.0' - }, - fhirVersion: 'R4', - url: 'https://packages.simplifier.net/hl7.terminology.r4/1.1.0' - }, - '1.1.2': { - name: 'hl7.terminology.r4', - version: '1.1.2', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe749822', - tarball: 'https://packages.simplifier.net/hl7.terminology.r4/1.1.2' - }, - fhirVersion: 'R4', - url: 'https://packages.simplifier.net/hl7.terminology.r4/1.1.2' - }, - '1.1.1': { - name: 'hl7.terminology.r4', - version: '1.1.1', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe749821', - tarball: 'https://packages.simplifier.net/hl7.terminology.r4/1.1.1' - }, - fhirVersion: 'R4', - url: 'https://packages.simplifier.net/hl7.terminology.r4/1.1.1' - } - } -}; - -// Represents a typical response from packages2.fhir.org (note: not on packages.fhir.org) -const EXT_PKG_RESPONSE = { - _id: 'hl7.fhir.uv.extensions', - name: 'hl7.fhir.uv.extensions', - 'dist-tags': { latest: '4.5.6-test' }, - versions: { - '4.5.6-test': { - name: 'hl7.fhir.uv.extensions', - date: '2023-03-26T08:46:31-00:00', - version: '1.0.0', - fhirVersion: '??', - kind: '??', - count: '18', - canonical: 'http://hl7.org/fhir/extensions', - description: 'None', - url: 'https://packages2.fhir.org/packages/hl7.fhir.uv.extensions/4.5.6-test' - }, - '2.0.0': { - name: 'hl7.fhir.uv.extensions', - date: '2023-03-26T08:46:31-00:00', - version: '2.0.0', - fhirVersion: '??', - kind: '??', - count: '18', - canonical: 'http://hl7.org/fhir/extensions', - description: 'None', - url: 'https://packages2.fhir.org/packages/hl7.fhir.uv.extensions/2.0.0' - }, - '2.0.2': { - name: 'hl7.fhir.uv.extensions', - date: '2023-03-26T08:46:31-00:00', - version: '2.0.2', - fhirVersion: '??', - kind: '??', - count: '18', - canonical: 'http://hl7.org/fhir/extensions', - description: 'None', - url: 'https://packages2.fhir.org/packages/hl7.fhir.uv.extensions/2.0.2' - }, - '2.0.1': { - name: 'hl7.fhir.uv.extensions', - date: '2023-03-26T08:46:31-00:00', - version: '2.0.1', - fhirVersion: '??', - kind: '??', - count: '18', - canonical: 'http://hl7.org/fhir/extensions', - description: 'None', - url: 'https://packages2.fhir.org/packages/hl7.fhir.uv.extensions/2.0.1' - } - } -}; - -describe('#loadFromPath()', () => { - let defsWithChildDefs: FHIRDefinitions; - let defs: FHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); - defsWithChildDefs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, 'testhelpers', 'testdefs'), 'r4-definitions', defs); - defs.fishForFHIR('Condition'); - defs.fishForFHIR('boolean'); - defs.fishForFHIR('Address'); - defs.fishForFHIR('vitalsigns'); - defs.fishForFHIR('patient-mothersMaidenName'); - defs.fishForFHIR('allergyintolerance-clinical', Type.ValueSet); - defs.fishForFHIR('allergyintolerance-clinical', Type.CodeSystem); - defs.fishForFHIR('MyIG'); - defs.fishForFHIR('MyLM'); - const childDef = cloneDeep(defs); - defsWithChildDefs.childFHIRDefs.push(childDef); - const otherChildDef = cloneDeep(defs); - otherChildDef.package = 'other-definitions'; - defsWithChildDefs.childFHIRDefs.push(otherChildDef); - }); - - it('should load base FHIR resources from FHIRDefs with no children', () => { - expect(defs.allResources().filter(r => r.id === 'Condition')).toHaveLength(1); - }); - - it('should load base FHIR resources from all child FHIRDefs', () => { - expect(defsWithChildDefs.allResources().filter(r => r.id === 'Condition')).toHaveLength(1); - }); - - it('should load base FHIR resources only from specified package', () => { - expect(defs.allResources('r4-definitions').filter(r => r.id === 'Condition')).toHaveLength(1); - expect( - defsWithChildDefs.allResources('r4-definitions').filter(r => r.id === 'Condition') - ).toHaveLength(1); // added in both childDefs, but identical resources are returned only once - }); - - it('should load base FHIR primitive types from FHIRDefs with no children', () => { - expect(defs.allTypes().filter(r => r.id === 'boolean')).toHaveLength(1); - }); - - it('should load base FHIR primitive types from all child FHIRDefs', () => { - expect(defsWithChildDefs.allTypes().filter(r => r.id === 'boolean')).toHaveLength(1); - }); - - it('should load base FHIR primitive types only from specified package', () => { - expect(defs.allTypes('r4-definitions').filter(r => r.id === 'boolean')).toHaveLength(1); - expect( - defsWithChildDefs.allTypes('r4-definitions').filter(r => r.id === 'boolean') - ).toHaveLength(1); - }); - - it('should load base FHIR complex types from FHIRDefs with no children', () => { - expect(defs.allTypes().filter(r => r.id === 'Address')).toHaveLength(1); - }); - - it('should load base FHIR complex types from all child FHIRDefs', () => { - expect(defsWithChildDefs.allTypes().filter(r => r.id === 'Address')).toHaveLength(1); - }); - - it('should load base FHIR complex types from specified package', () => { - expect(defs.allTypes('r4-definitions').filter(r => r.id === 'Address')).toHaveLength(1); - expect( - defsWithChildDefs.allTypes('r4-definitions').filter(r => r.id === 'Address') - ).toHaveLength(1); - }); - - it('should load base FHIR profiles from FHIRDefs with no children', () => { - expect(defs.allProfiles().filter(r => r.id === 'vitalsigns')).toHaveLength(1); - }); - - it('should load base FHIR profiles from all child FHIRDefs', () => { - expect(defsWithChildDefs.allProfiles().filter(r => r.id === 'vitalsigns')).toHaveLength(1); - }); - - it('should load base FHIR profiles from specified package', () => { - expect(defs.allProfiles('r4-definitions').filter(r => r.id === 'vitalsigns')).toHaveLength(1); - expect( - defsWithChildDefs.allProfiles('r4-definitions').filter(r => r.id === 'vitalsigns') - ).toHaveLength(1); - }); - - it('should load base FHIR extensions from FHIRDefs with no children', () => { - expect(defs.allExtensions().filter(r => r.id === 'patient-mothersMaidenName')).toHaveLength(1); - }); - - it('should load base FHIR extensions from all child FHIRDefs', () => { - expect( - defsWithChildDefs.allExtensions().filter(r => r.id === 'patient-mothersMaidenName') - ).toHaveLength(1); - }); - - it('should load base FHIR extensions from specified package', () => { - expect( - defs.allExtensions('r4-definitions').filter(r => r.id === 'patient-mothersMaidenName') - ).toHaveLength(1); - expect( - defsWithChildDefs - .allExtensions('r4-definitions') - .filter(r => r.id === 'patient-mothersMaidenName') - ).toHaveLength(1); - }); - - it('should load base FHIR value sets from FHIRDefs with no children', () => { - expect(defs.allValueSets().filter(r => r.id === 'allergyintolerance-clinical')).toHaveLength(1); - }); - - it('should load base FHIR value sets from all child FHIRDefs', () => { - expect( - defsWithChildDefs.allValueSets().filter(r => r.id === 'allergyintolerance-clinical') - ).toHaveLength(1); - }); - - it('should load base FHIR value sets from specified package', () => { - expect( - defs.allValueSets('r4-definitions').filter(r => r.id === 'allergyintolerance-clinical') - ).toHaveLength(1); - expect( - defsWithChildDefs - .allValueSets('r4-definitions') - .filter(r => r.id === 'allergyintolerance-clinical') - ).toHaveLength(1); - }); - - it('should load base FHIR code systems from FHIRDefs with no children', () => { - expect(defs.allCodeSystems().filter(r => r.id === 'allergyintolerance-clinical')).toHaveLength( - 1 - ); - }); - - it('should load base FHIR code systems from all child FHIRDefs', () => { - expect( - defsWithChildDefs.allCodeSystems().filter(r => r.id === 'allergyintolerance-clinical') - ).toHaveLength(1); - }); - - it('should load base FHIR code systems from specified package', () => { - expect( - defs.allCodeSystems('r4-definitions').filter(r => r.id === 'allergyintolerance-clinical') - ).toHaveLength(1); - expect( - defsWithChildDefs - .allCodeSystems('r4-definitions') - .filter(r => r.id === 'allergyintolerance-clinical') - ).toHaveLength(1); - }); - - it('should load base FHIR implementation guides from FHIRDefs with no children', () => { - expect(defs.allImplementationGuides().filter(r => r.id === 'MyIG')).toHaveLength(1); - }); - - it('should load base FHIR implementation guides from all child FHIRDefs', () => { - expect(defsWithChildDefs.allImplementationGuides().filter(r => r.id === 'MyIG')).toHaveLength( - 1 - ); - }); - - it('should load base FHIR implementation guides from specified package', () => { - expect( - defs.allImplementationGuides('r4-definitions').filter(r => r.id === 'MyIG') - ).toHaveLength(1); - expect( - defsWithChildDefs.allImplementationGuides('r4-definitions').filter(r => r.id === 'MyIG') - ).toHaveLength(1); - }); - - it('should load base FHIR logicals from FHIRDefs with no children', () => { - expect(defs.allLogicals().filter(r => r.id === 'MyLM')).toHaveLength(1); - }); - - it('should load base FHIR logicals from all child FHIRDefs', () => { - expect(defsWithChildDefs.allLogicals().filter(r => r.id === 'MyLM')).toHaveLength(1); - }); - - it('should load base FHIR logicals from specified package', () => { - expect(defs.allLogicals('r4-definitions').filter(r => r.id === 'MyLM')).toHaveLength(1); - expect( - defsWithChildDefs.allLogicals('r4-definitions').filter(r => r.id === 'MyLM') - ).toHaveLength(1); - }); - - it('should load the package.json file', () => { - expect(defs.getPackageJson('r4-definitions')).toBeDefined(); - }); - - it('should count size of each unique definition from child FHIRDefs in total', () => { - // the two child FHIRDefs are identical, - // so the size of the parent is equal to the size of each child. - expect(defsWithChildDefs.size()).toEqual(defs.size()); - }); - - it('should get all unsuccessfully loaded packages from FHIRDefs with no children', () => { - const failedPackage = new FHIRDefinitions(); - failedPackage.package = 'my-package'; - failedPackage.unsuccessfulPackageLoad = true; - expect(failedPackage.allUnsuccessfulPackageLoads()).toHaveLength(1); - }); - - it('should get all unsuccessfully loaded packages from all child FHIRDefs', () => { - const failedPackage = new FHIRDefinitions(); - failedPackage.package = 'my-package'; - failedPackage.unsuccessfulPackageLoad = true; - const failedChildPackage = new FHIRDefinitions(); - failedChildPackage.package = 'failed-child-package'; - failedChildPackage.unsuccessfulPackageLoad = true; - const successfulPackageLoad = new FHIRDefinitions(); - successfulPackageLoad.package = 'successful-child-package'; - // unsuccessfulPackageLoad defaults to false - failedPackage.childFHIRDefs.push(failedChildPackage, successfulPackageLoad); - expect(failedPackage.allUnsuccessfulPackageLoads()).toHaveLength(2); - }); - - it('should get all unsuccessfully loaded packages from specified package', () => { - const failedPackage = new FHIRDefinitions(); - failedPackage.package = 'my-package'; - failedPackage.unsuccessfulPackageLoad = true; - const failedChildPackage = new FHIRDefinitions(); - failedChildPackage.package = 'failed-child-package'; - failedChildPackage.unsuccessfulPackageLoad = true; - const successfulPackageLoad = new FHIRDefinitions(); - successfulPackageLoad.package = 'successful-child-package'; - // unsuccessfulPackageLoad defaults to false - failedPackage.childFHIRDefs.push(failedChildPackage, successfulPackageLoad); - expect(failedPackage.allUnsuccessfulPackageLoads('my-package')).toHaveLength(1); - expect(failedPackage.allUnsuccessfulPackageLoads('my-package')).toEqual(['my-package']); - }); - - it('should get all packages from FHIRDefs with no children', () => { - expect(defs.allPackages()).toHaveLength(1); - }); - - it('should get all packages from all child FHIRDefs', () => { - expect(defsWithChildDefs.allPackages()).toHaveLength(2); - }); - - it('should get all packages from specified package', () => { - expect(defs.allPackages('r4-definitions')).toHaveLength(1); - expect(defsWithChildDefs.allPackages('r4-definitions')).toHaveLength(1); - }); -}); - -describe('#loadDependencies()', () => { - const log = (level: string, message: string) => { - loggerSpy.log(level, message); - }; - let jestSpy: jest.SpyInstance; - beforeAll(() => { - jestSpy = jest - .spyOn(loadModule, 'mergeDependency') - .mockImplementation( - async (packageName: string, version: string, FHIRDefs: FHIRDefinitions) => { - // the mock loader can find hl7.fhir.(r2|r3|r4|r5|us).core - if (/^hl7.fhir.(r2|r3|r4|r4b|r5|us).core$/.test(packageName)) { - FHIRDefs.package = `${packageName}#${version}`; - return Promise.resolve(FHIRDefs); - } else if (/^self-signed.package$/.test(packageName)) { - throw new Error('self signed certificate in certificate chain'); - } else { - throw new PackageLoadError(`${packageName}#${version}`); - } - } - ); - }); - - beforeEach(() => { - loggerSpy.reset(); - }); - - afterAll(() => { - jestSpy.mockRestore(); - }); - - it('should return single FHIRDefinitions if only one package is loaded', () => { - const fhirPackages = ['hl7.fhir.us.core#4.0.1']; - return loadDependencies(fhirPackages, undefined, log).then(defs => { - expect(defs.package).toEqual('hl7.fhir.us.core#4.0.1'); - expect(loggerSpy.getAllLogs('warn')).toHaveLength(0); - expect(loggerSpy.getAllLogs('error')).toHaveLength(0); - }); - }); - - it('should return an array of FHIRDefinitions if multiple packages are loaded', () => { - const fhirPackages = ['hl7.fhir.us.core#4.0.1', 'hl7.fhir.us.core#3.0.1']; - return loadDependencies(fhirPackages, undefined, log).then(defs => { - expect(defs.package).toEqual(''); // No package specified on wrapper class - expect(defs.childFHIRDefs).toHaveLength(2); - expect(defs.childFHIRDefs[0].package).toEqual('hl7.fhir.us.core#4.0.1'); - expect(defs.childFHIRDefs[1].package).toEqual('hl7.fhir.us.core#3.0.1'); - expect(loggerSpy.getAllLogs('warn')).toHaveLength(0); - expect(loggerSpy.getAllLogs('error')).toHaveLength(0); - }); - }); - - it('should log an error when it fails to load any of dependencies', () => { - const fhirPackages = ['hl7.does.not.exist#current']; - return loadDependencies(fhirPackages, undefined, log).then(defs => { - expect(defs.package).toEqual('hl7.does.not.exist#current'); - expect(defs.allResources()).toHaveLength(0); // No resources added - expect(loggerSpy.getLastMessage('error')).toMatch( - /Failed to load hl7\.does\.not\.exist#current/s - ); - expect(loggerSpy.getLastMessage('error')).not.toMatch(/SSL/s); - }); - }); - - it('should log a more detailed error when it fails to download a dependency due to certificate issue', () => { - const selfSignedPackage = ['self-signed.package#1.0.0']; - return loadDependencies(selfSignedPackage, undefined, log).then(defs => { - expect(defs.package).toEqual('self-signed.package#1.0.0'); - expect(defs.allResources()).toHaveLength(0); // No resources added - expect(loggerSpy.getLastMessage('error')).toMatch( - /Failed to load self-signed\.package#1\.0\.0/s - ); - // AND it should log the detailed message about SSL - expect(loggerSpy.getLastMessage('error')).toMatch( - /Sometimes this error occurs in corporate or educational environments that use proxies and\/or SSL inspection/s - ); - }); - }); -}); - -describe('#loadDependency()', () => { - const log = (level: string, message: string) => { - loggerSpy.log(level, message); - }; - let jestSpy: jest.SpyInstance; - let fhirDefs: FHIRDefinitions; - beforeAll(async () => { - jestSpy = jest - .spyOn(loadModule, 'mergeDependency') - .mockImplementation( - async (packageName: string, version: string, FHIRDefs: FHIRDefinitions) => { - // the mock loader can find hl7.fhir.(r2|r3|r4|r5|us).core - if (/^hl7.fhir.(r2|r3|r4|r4b|r5|us).core$/.test(packageName)) { - return Promise.resolve(FHIRDefs); - } else if (/^self-signed.package$/.test(packageName)) { - throw new Error('self signed certificate in certificate chain'); - } else { - throw new PackageLoadError(`${packageName}#${version}`); - } - } - ); - }); - - beforeEach(async () => { - fhirDefs = await loadDependencies( - ['hl7.fhir.r4.core#4.0.1', 'hl7.fhir.r5.core#current'], - undefined, - log - ); - // Reset after loading dependencies so tests only check their own changes - loggerSpy.reset(); - jestSpy.mockClear(); - }); - - afterAll(() => { - jestSpy.mockRestore(); - }); - - it('should call mergeDependency with a blank FHIRDefinition', () => { - const packageName = 'hl7.fhir.us.core'; - const packageVersion = '4.0.1'; - return loadDependency(packageName, packageVersion, fhirDefs, undefined, log).then(() => { - expect(jestSpy).toHaveBeenCalledTimes(1); - expect(jestSpy).toHaveBeenLastCalledWith( - packageName, - packageVersion, - new FHIRDefinitions(), - path.join(os.homedir(), '.fhir', 'packages'), - log - ); - }); - }); - - it('should return a FHIRDefinition with a new childDefs if there were already childDefs before loading another package', () => { - const packageName = 'hl7.fhir.us.core'; - const packageVersion = '4.0.1'; - return loadDependency(packageName, packageVersion, fhirDefs, undefined, log).then(defs => { - expect(jestSpy).toHaveBeenCalledTimes(1); - expect(defs.childFHIRDefs).toHaveLength(3); - }); - }); - - it('should wrap provided definitions and merged definitions if there were no childDefs before loading another package', async () => { - // If only one package is specified, FHIRDefs has packages loaded directly and no childDefs - // but loading a second package with loadDependency should then wrap the original defs and - // the new package defs in one FHIRDefs with two childDefs - const oneFHIRDefs = await loadDependencies(['hl7.fhir.r4.core#4.0.1'], undefined, log); - const packageName = 'hl7.fhir.us.core'; - const packageVersion = '4.0.1'; - return loadDependency(packageName, packageVersion, oneFHIRDefs, undefined, log).then(defs => { - expect(jestSpy).toHaveBeenCalledTimes(2); // Once at the start of the test, once from loadDependency - expect(defs.childFHIRDefs).toHaveLength(2); - }); - }); -}); - -describe('#mergeDependency()', () => { - const log = (level: string, message: string) => { - loggerSpy.log(level, message); - }; - let defs: FHIRDefinitions; - let axiosSpy: jest.SpyInstance; - let axiosHeadSpy: jest.SpyInstance; - let tarSpy: jest.SpyInstance; - let removeSpy: jest.SpyInstance; - let moveSpy: jest.SpyInstance; - let writeSpy: jest.SpyInstance; - let cachePath: string; - - // Many tests check that the right package was downloaded to the right place. - // This function encapsulates that testing logic. It's coupled more tightly to - // the actual implementation than I'd prefer, but... at least it's in one place. - const expectDownloadSequence = ( - sources: string | string[] | { source: string; omitResponseType?: boolean }[], - destination: string | null, - isCurrent = false, - isCurrentFound = true - ): void => { - if (!Array.isArray(sources)) { - sources = [sources]; - } - if (isCurrent) { - const mockCalls: any[] = [['https://build.fhir.org/ig/qas.json']]; - if (isCurrentFound) { - if (typeof sources[0] === 'string') { - mockCalls.push( - [sources[0].replace(/package\.tgz$/, 'package.manifest.json')], - [sources[0], { responseType: 'arraybuffer' }] - ); - } else { - mockCalls.push([sources[0].source.replace(/package\.tgz$/, 'package.manifest.json')]); - if (sources[0].omitResponseType !== true) { - mockCalls.push([sources[0].source, { responseType: 'arraybuffer' }]); - } - } - } - expect(axiosSpy.mock.calls).toEqual(mockCalls); - } else { - expect(axiosSpy.mock.calls).toEqual( - sources.map(s => { - if (typeof s === 'string') { - return [s, { responseType: 'arraybuffer' }]; - } else { - if (s.omitResponseType === true) { - return [s.source]; - } else { - return [s.source, { responseType: 'arraybuffer' }]; - } - } - }) - ); - } - if (destination != null) { - const tempTarFile = writeSpy.mock.calls[0][0]; - const tempTarDirectory = tarSpy.mock.calls[0][0].cwd; - expect(tarSpy.mock.calls[0][0].file).toBe(tempTarFile); - expect(moveSpy.mock.calls[0][0]).toBe(tempTarDirectory); - expect(moveSpy.mock.calls[0][1]).toBe(destination); - } else { - expect(writeSpy).toHaveBeenCalledTimes(0); - expect(tarSpy).toHaveBeenCalledTimes(0); - expect(moveSpy).toHaveBeenCalledTimes(0); - } - }; - - beforeAll(() => { - axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { - if (uri === 'https://build.fhir.org/ig/qas.json') { - return { - data: [ - { - url: 'http://hl7.org/fhir/us/core/ImplementationGuide/hl7.fhir.us.core.r4-4.0.0', - name: 'USCoreR4', - 'package-id': 'hl7.fhir.us.core.r4', - 'ig-ver': '4.0.0', - date: 'Sat, 18 May, 2019 01:48:14 +0000', - errs: 538, - warnings: 34, - hints: 202, - version: '4.0.0', - tool: '4.1.0 (3)', - repo: 'HL7Imposter/US-Core-R4/branches/main/qa.json' - }, - { - url: 'http://hl7.org/fhir/us/core/ImplementationGuide/hl7.fhir.us.core.r4-4.0.0', - name: 'USCoreR4', - 'package-id': 'hl7.fhir.us.core.r4', - 'ig-ver': '4.0.0', - date: 'Mon, 20 Jan, 2020 19:36:43 +0000', - errs: 1496, - warnings: 36, - hints: 228, - version: '4.0.0', - tool: '4.1.0 (3)', - repo: 'HL7/US-Core-R4/branches/main/qa.json' - }, - { - url: 'http://hl7.org/fhir/sushi-test-no-download/ImplementationGuide/sushi-test-no-download-0.1.0', - name: 'sushi-test-no-download', - 'package-id': 'sushi-test-no-download', - 'ig-ver': '0.1.0', - repo: 'sushi/sushi-test-no-download/branches/master/qa.json' - }, - { - url: 'http://hl7.org/fhir/sushi-test-old/ImplementationGuide/sushi-test-old-0.1.0', - name: 'sushi-test-old', - 'package-id': 'sushi-test-old', - 'ig-ver': '0.1.0', - repo: 'sushi/sushi-test-old/branches/master/qa.json' - }, - { - url: 'http://hl7.org/fhir/sushi-test/ImplementationGuide/sushi-test-0.1.0', - name: 'sushi-test', - 'package-id': 'sushi-test', - 'ig-ver': '0.1.0', - repo: 'sushi/sushi-test/branches/master/qa.json' - }, - { - url: 'http://hl7.org/fhir/sushi-test/ImplementationGuide/sushi-test-0.1.0', - name: 'sushi-test', - 'package-id': 'sushi-test', - 'ig-ver': '0.2.0', - repo: 'sushi/sushi-test/branches/testbranch/qa.json' - }, - { - url: 'http://hl7.org/fhir/sushi-test/ImplementationGuide/sushi-test-0.1.0', - name: 'sushi-test', - 'package-id': 'sushi-test', - 'ig-ver': '0.2.0', - repo: 'sushi/sushi-test/branches/oldbranch/qa.json' - }, - { - url: 'http://hl7.org/fhir/sushi-no-main/ImplementationGuide/sushi-no-main-0.1.0', - name: 'sushi-no-main', - 'package-id': 'sushi-no-main', - 'ig-ver': '0.1.0', - repo: 'sushi/sushi-no-main/branches/feature/qa.json' - } - ] - }; - } else if ( - uri === 'https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.manifest.json' || - (uri.startsWith('https://build.fhir.org/ig/sushi/sushi-test') && uri.endsWith('json')) - ) { - return { - data: { - date: '20200413230227' - } - }; - } else if (uri === 'https://packages.fhir.org/hl7.fhir.r4.core') { - return { - data: { - _id: 'hl7.fhir.r4.core', - name: 'hl7.fhir.r4.core', - 'dist-tags': { latest: '4.0.1' }, - versions: { - '4.0.1': { - name: 'hl7.fhir.r4.core', - version: '4.0.1', - description: - 'Definitions (API, structures and terminologies) for the R4 version of the FHIR standard', - dist: { - shasum: '0e4b8d99f7918587557682c8b47df605424547a5', - tarball: 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' - }, - fhirVersion: 'R4', - url: 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' - } - } - } - }; - } else if ( - uri === 'https://packages.fhir.org/sushi-test/0.2.0' || - uri === 'https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.tgz' || - uri === 'https://build.fhir.org/ig/sushi/sushi-test/branches/oldbranch/package.tgz' || - uri === 'https://build.fhir.org/ig/sushi/sushi-test-old/branches/master/package.tgz' || - uri === 'https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.tgz' || - uri === 'https://build.fhir.org/hl7.fhir.r5.core.tgz' || - uri === 'https://packages2.fhir.org/packages/hl7.fhir.r4b.core/4.1.0' || - uri === 'https://packages.fhir.org/hl7.fhir.r4b.core/4.3.0' || - uri === 'https://packages2.fhir.org/packages/hl7.fhir.r5.core/4.5.0' || - uri === 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' || - uri === 'https://packages2.fhir.org/packages/fhir.dicom/2021.4.20210910' || - uri === 'https://custom-registry.example.org/good-thing/0.3.6' - ) { - return { - data: { - some: 'zipfile' - } - }; - } else if ( - uri === 'https://packages.fhir.org/hl7.fhir.r4b.core/4.1.0' || - uri === 'https://packages.fhir.org/hl7.fhir.r5.core/4.5.0' || - uri === 'https://packages.fhir.org/fhir.dicom/2021.4.20210910' - ) { - throw 'Not Found'; - } else { - return {}; - } - }); - axiosHeadSpy = jest.spyOn(axios, 'head').mockImplementation((): any => { - throw 'Method Not Allowed'; - }); - tarSpy = jest.spyOn(tar, 'x').mockImplementation(() => {}); - writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); - removeSpy = jest.spyOn(fs, 'removeSync').mockImplementation(() => {}); - moveSpy = jest.spyOn(fs, 'moveSync').mockImplementation(() => {}); - cachePath = path.join(__dirname, 'testhelpers', 'fixtures'); - delete process.env.FPL_REGISTRY; - }); - - beforeEach(() => { - loggerSpy.reset(); - defs = new FHIRDefinitions(); - axiosSpy.mockClear(); - axiosHeadSpy.mockClear(); - tarSpy.mockClear(); - writeSpy.mockClear(); - moveSpy.mockClear(); - removeSpy.mockClear(); - }); - - afterEach(() => { - delete process.env.FPL_REGISTRY; - }); - - // Packages with numerical versions - it('should not try to download a non-current package that is already in the cache', async () => { - const expectedDefs = new FHIRDefinitions(); - loadFromPath(cachePath, 'sushi-test#0.1.0', expectedDefs); - await expect(mergeDependency('sushi-test', '0.1.0', defs, cachePath, log)).resolves.toEqual( - expectedDefs - ); - expect(axiosSpy.mock.calls.length).toBe(0); - }); - - it('should recognize a package in the cache with uppercase letters', async () => { - const expectedDefs = new FHIRDefinitions(); - loadFromPath(cachePath, 'sushi-test-caps#0.1.0', expectedDefs); - await expect( - mergeDependency('sushi-test-caps', '0.1.0', defs, cachePath, log) - ).resolves.toEqual(expectedDefs); - expect(axiosSpy.mock.calls.length).toBe(0); - }); - - it('should try to load a package from packages.fhir.org when a non-current package is not cached', async () => { - await expect(mergeDependency('sushi-test', '0.2.0', defs, 'foo', log)).rejects.toThrow( - 'The package sushi-test#0.2.0 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://packages.fhir.org/sushi-test/0.2.0', - path.join('foo', 'sushi-test#0.2.0') - ); - }); - - it('should try to load FHIR R4 (4.0.1) from packages.fhir.org when it is not cached', async () => { - await expect(mergeDependency('hl7.fhir.r4.core', '4.0.1', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.r4.core#4.0.1 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1', - path.join('foo', 'hl7.fhir.r4.core#4.0.1') - ); - }); - - it('should try to load prerelease FHIR R4B (4.1.0) from packages2.fhir.org when it is not cached', async () => { - await expect(mergeDependency('hl7.fhir.r4b.core', '4.1.0', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.r4b.core#4.1.0 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - 'https://packages.fhir.org/hl7.fhir.r4b.core/4.1.0', - 'https://packages2.fhir.org/packages/hl7.fhir.r4b.core/4.1.0' - ], - path.join('foo', 'hl7.fhir.r4b.core#4.1.0') - ); - }); - - it('should try to load FHIR R4B (4.3.0) from packages.fhir.org when it is not cached', async () => { - await expect(mergeDependency('hl7.fhir.r4b.core', '4.3.0', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.r4b.core#4.3.0 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://packages.fhir.org/hl7.fhir.r4b.core/4.3.0', - path.join('foo', 'hl7.fhir.r4b.core#4.3.0') - ); - }); - - it('should try to load prerelease FHIR R5 (4.5.0) from packages2.fhir.org when it is not cached', async () => { - await expect(mergeDependency('hl7.fhir.r5.core', '4.5.0', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.r5.core#4.5.0 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - 'https://packages.fhir.org/hl7.fhir.r5.core/4.5.0', - 'https://packages2.fhir.org/packages/hl7.fhir.r5.core/4.5.0' - ], - path.join('foo', 'hl7.fhir.r5.core#4.5.0') - ); - }); - - it('should try to load a package from packages2.fhir.org when it is not on packages.fhir.org', async () => { - await expect( - mergeDependency('fhir.dicom', '2021.4.20210910', defs, 'foo', log) - ).rejects.toThrow( - 'The package fhir.dicom#2021.4.20210910 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - 'https://packages.fhir.org/fhir.dicom/2021.4.20210910', - 'https://packages2.fhir.org/packages/fhir.dicom/2021.4.20210910' - ], - path.join('foo', 'fhir.dicom#2021.4.20210910') - ); - }); - - it('should try to load a package from a custom registry that is like NPM', async () => { - // packages.fhir.org supports NPM clients - process.env.FPL_REGISTRY = 'https://packages.fhir.org'; - await expect(mergeDependency('hl7.fhir.r4.core', '4.0.1', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.r4.core#4.0.1 could not be loaded locally or from the custom FHIR package registry https://packages.fhir.org.' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - { source: 'https://packages.fhir.org/hl7.fhir.r4.core', omitResponseType: true }, - { source: 'https://packages.fhir.org/hl7.fhir.r4.core/4.0.1' } - ], - path.join('foo', 'hl7.fhir.r4.core#4.0.1') - ); - }); - - it('should try to load a package from a custom registry that is not like NPM', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - await expect(mergeDependency('good-thing', '0.3.6', defs, 'foo', log)).rejects.toThrow( - 'The package good-thing#0.3.6 could not be loaded locally or from the custom FHIR package registry https://custom-registry.example.org' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - { source: 'https://custom-registry.example.org/good-thing', omitResponseType: true }, - { source: 'https://custom-registry.example.org/good-thing/0.3.6' } - ], - path.join('foo', 'good-thing#0.3.6') - ); - }); - - it('should try to load a package from a custom registry specified with a trailing slash', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org/'; - await expect(mergeDependency('good-thing', '0.3.6', defs, 'foo', log)).rejects.toThrow( - 'The package good-thing#0.3.6 could not be loaded locally or from the custom FHIR package registry https://custom-registry.example.org/' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - { source: 'https://custom-registry.example.org/good-thing', omitResponseType: true }, - { source: 'https://custom-registry.example.org/good-thing/0.3.6' } - ], - path.join('foo', 'good-thing#0.3.6') - ); - }); - - it('should throw PackageLoadError when a package with a non-current version is not cached or available on packages.fhir.org', async () => { - await expect(mergeDependency('sushi-test', '0.3.0', defs, 'foo', log)).rejects.toThrow( - 'The package sushi-test#0.3.0 could not be loaded locally or from the FHIR package registry' - ); - expect(loggerSpy.getLastMessage('info')).toMatch( - /Unable to download most current version of sushi-test#0.3.0/ - ); - expectDownloadSequence('https://packages.fhir.org/sushi-test/0.3.0', null); - }); - - it('should throw PackageLoadError when a package cannot be loaded from packages2.fhir.org', async () => { - axiosSpy = jest - .spyOn(axios, 'get') - .mockImplementationOnce((uri: string): any => { - if (uri === 'https://packages.fhir.org/fhir.fake/2022.1.01') { - throw 'Not Found'; - } - }) - .mockImplementationOnce((uri: string): any => { - if (uri === 'https://packages2.fhir.org/packages/fhir.fake/2022.1.01') { - throw 'Not Found'; - } - }); - await expect(mergeDependency('fhir.fake', '2022.1.01', defs, 'foo', log)).rejects.toThrow( - 'The package fhir.fake#2022.1.01 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - [ - 'https://packages.fhir.org/fhir.fake/2022.1.01', - 'https://packages2.fhir.org/packages/fhir.fake/2022.1.01' - ], - null - ); - }); - - // Packages with current versions - it('should not try to download a current package that is already in the cache and up to date', async () => { - const expectedDefs = new FHIRDefinitions(); - loadFromPath(cachePath, 'sushi-test#current', expectedDefs); - await expect(mergeDependency('sushi-test', 'current', defs, cachePath, log)).resolves.toEqual( - expectedDefs - ); - expect(axiosSpy.mock.calls).toEqual([ - ['https://build.fhir.org/ig/qas.json'], - ['https://build.fhir.org/ig/sushi/sushi-test/branches/master/package.manifest.json'] - ]); - }); - - it('should not try to download a current$branch package that is already in the cache and up to date', async () => { - const expectedDefs = new FHIRDefinitions(); - loadFromPath(cachePath, 'sushi-test#current$testbranch', expectedDefs); - await expect( - mergeDependency('sushi-test', 'current$testbranch', defs, cachePath, log) - ).resolves.toEqual(expectedDefs); - expect(axiosSpy.mock.calls).toEqual([ - ['https://build.fhir.org/ig/qas.json'], - ['https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.manifest.json'] - ]); - }); - - it('should try to load the latest package from build.fhir.org when a current package version is not locally cached', async () => { - await expect( - mergeDependency('hl7.fhir.us.core.r4', 'current', defs, 'foo', log) - ).rejects.toThrow( - 'The package hl7.fhir.us.core.r4#current could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://build.fhir.org/ig/HL7/US-Core-R4/branches/main/package.tgz', - path.join('foo', 'hl7.fhir.us.core.r4#current'), - true - ); - }); - - it('should try to load the latest branch-specific package from build.fhir.org when a current package version is not locally cached', async () => { - await expect( - mergeDependency('sushi-test', 'current$testbranch', defs, 'foo', log) - ).rejects.toThrow( - 'The package sushi-test#current$testbranch could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://build.fhir.org/ig/sushi/sushi-test/branches/testbranch/package.tgz', - path.join('foo', 'sushi-test#current$testbranch'), - true - ); - }); - - it('should try to load the latest package from build.fhir.org when a current package version has an older version that is locally cached', async () => { - await expect( - mergeDependency('sushi-test-old', 'current', defs, cachePath, log) - ).resolves.toBeTruthy(); // Since tar is mocked, the actual cache is not updated - expectDownloadSequence( - 'https://build.fhir.org/ig/sushi/sushi-test-old/branches/master/package.tgz', - path.join(cachePath, 'sushi-test-old#current'), - true - ); - expect(removeSpy.mock.calls[0][0]).toBe(path.join(cachePath, 'sushi-test-old#current')); - }); - - it('should try to load the latest branch-specific package from build.fhir.org when a current package version has an older version that is locally cached', async () => { - await expect( - mergeDependency('sushi-test', 'current$oldbranch', defs, cachePath, log) - ).resolves.toBeTruthy(); // Since tar is mocked, the actual cache is not updated - expectDownloadSequence( - 'https://build.fhir.org/ig/sushi/sushi-test/branches/oldbranch/package.tgz', - path.join(cachePath, 'sushi-test#current$oldbranch'), - true - ); - expect(removeSpy.mock.calls[0][0]).toBe(path.join(cachePath, 'sushi-test#current$oldbranch')); - }); - - it('should not try to load the latest package from build.fhir.org from a branch that is not main/master', async () => { - await expect(mergeDependency('sushi-no-main', 'current', defs, cachePath, log)).rejects.toThrow( - 'The package sushi-no-main#current is not available on https://build.fhir.org/ig/qas.json, so no current version can be loaded' - ); - expectDownloadSequence('', null, true, false); - }); - - it('should not try to load the branch-specific package from build.fhir.org when that branch is not available in qas', async () => { - await expect( - mergeDependency('sushi-test', 'current$wrongbranch', defs, cachePath, log) - ).rejects.toThrow( - 'The package sushi-test#current$wrongbranch is not available on https://build.fhir.org/ig/qas.json, so no current version can be loaded' - ); - expectDownloadSequence('', null, true, false); - }); - - // This handles the edge case that comes from how SUSHI uses FHIRDefinitions - it('should successfully load a package even if the FHIRDefinitions.package property does not reflect the current package', async () => { - const newDefs = await mergeDependency('sushi-test', 'current', defs, cachePath, log); - axiosSpy.mockClear(); // Clear the spy between the two calls in the single test - - // This mimics the odd SUSHI case because we pass in a FHIRDefinitions that already had definitions loaded into it - // So instead of creating a new FHIRDefs for a new package, we re-use an old one. Only SUSHI does this. - // This is not expected and is not the intended pattern for normal use. - await expect( - mergeDependency('sushi-test-old', 'current', newDefs, cachePath, log) - ).resolves.toBeTruthy(); - expectDownloadSequence( - 'https://build.fhir.org/ig/sushi/sushi-test-old/branches/master/package.tgz', - path.join(cachePath, 'sushi-test-old#current'), - true - ); - expect(removeSpy.mock.calls[0][0]).toBe(path.join(cachePath, 'sushi-test-old#current')); - }); - - it('should try to load the latest FHIR R5 package from build.fhir.org when it is not locally cached', async () => { - await expect(mergeDependency('hl7.fhir.r5.core', 'current', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.r5.core#current could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://build.fhir.org/hl7.fhir.r5.core.tgz', - path.join('foo', 'hl7.fhir.r5.core#current'), - false // technically not treated like other current builds (for some reason) - ); - }); - - it('should revert to an old locally cached current version when a newer current version is not available for download', async () => { - const expectedDefs = new FHIRDefinitions(); - loadFromPath(cachePath, 'sushi-test-no-download#current', expectedDefs); - await expect( - mergeDependency('sushi-test-no-download', 'current', defs, cachePath, log) - ).resolves.toEqual(expectedDefs); - expectDownloadSequence( - 'https://build.fhir.org/ig/sushi/sushi-test-no-download/branches/master/package.tgz', - null, - true - ); - expect(removeSpy).toHaveBeenCalledTimes(0); - }); - - // Packages with dev versions - it('should not try to download a dev package that is already in the cache', async () => { - const expectedDefs = new FHIRDefinitions(); - loadFromPath(cachePath, 'sushi-test#dev', expectedDefs); - await expect(mergeDependency('sushi-test', 'dev', defs, cachePath, log)).resolves.toEqual( - expectedDefs - ); - expect(axiosSpy.mock.calls).toHaveLength(0); - }); - - it('should load the current package from build.fhir.org when a dev package is loaded and not locally cached', async () => { - await expect( - mergeDependency('sushi-test-old', 'dev', defs, cachePath, log) - ).resolves.toBeTruthy(); - expect( - loggerSpy - .getAllMessages('info') - .some(message => - message.match( - /Falling back to sushi-test-old#current since sushi-test-old#dev is not locally cached./ - ) - ) - ).toBeTruthy(); - expectDownloadSequence( - 'https://build.fhir.org/ig/sushi/sushi-test-old/branches/master/package.tgz', - path.join(cachePath, 'sushi-test-old#current'), - true - ); - expect(removeSpy.mock.calls[0][0]).toBe(path.join(cachePath, 'sushi-test-old#current')); - }); - - it('should load the latest version when the given version is "latest"', async () => { - jest.spyOn(loadModule, 'lookUpLatestVersion').mockResolvedValueOnce('0.2.0'); - await expect(mergeDependency('sushi-test', 'latest', defs, 'foo', log)).rejects.toThrow( - 'The package sushi-test#0.2.0 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://packages.fhir.org/sushi-test/0.2.0', - path.join('foo', 'sushi-test#0.2.0') - ); - }); - - it('should load the latest patch version when the given version uses a patch wildcard', async () => { - jest.spyOn(loadModule, 'lookUpLatestPatchVersion').mockResolvedValueOnce('0.2.0'); - await expect(mergeDependency('sushi-test', '0.2.x', defs, 'foo', log)).rejects.toThrow( - 'The package sushi-test#0.2.0 could not be loaded locally or from the FHIR package registry' - ); // the package is never actually added to the cache, since tar is mocked - expectDownloadSequence( - 'https://packages.fhir.org/sushi-test/0.2.0', - path.join('foo', 'sushi-test#0.2.0') - ); - }); - - it('should throw IncorrectWildcardVersionFormatError when the given version uses a non-patch wildcard', async () => { - await expect(mergeDependency('sushi-test', '0.x', defs, 'foo', log)).rejects.toThrow( - 'Incorrect version format for package sushi-test: 0.x. Wildcard should only be used to specify patch versions.' - ); - expect(axiosSpy.mock.calls.length).toBe(0); - }); - - it('should throw CurrentPackageLoadError when a current package is not listed', async () => { - await expect(mergeDependency('hl7.fhir.us.core', 'current', defs, 'foo', log)).rejects.toThrow( - 'The package hl7.fhir.us.core#current is not available on https://build.fhir.org/ig/qas.json, so no current version can be loaded' - ); - expect(axiosSpy.mock.calls.length).toBe(1); - expect(axiosSpy.mock.calls[0][0]).toBe('https://build.fhir.org/ig/qas.json'); - }); - - it('should throw CurrentPackageLoadError when https://build.fhir.org/ig/qas.json gives a bad response', async () => { - axiosSpy.mockImplementationOnce(() => {}); - await expect(mergeDependency('bad.response', 'current', defs, 'foo', log)).rejects.toThrow( - 'The package bad.response#current is not available on https://build.fhir.org/ig/qas.json, so no current version can be loaded' - ); - expect(axiosSpy.mock.calls.length).toBe(1); - expect(axiosSpy.mock.calls[0][0]).toBe('https://build.fhir.org/ig/qas.json'); - }); -}); - -describe('#cleanCachedPackage', () => { - let renameSpy: jest.SpyInstance; - let cachePath: string; - - beforeAll(() => { - renameSpy = jest.spyOn(fs, 'renameSync').mockImplementation(() => {}); - cachePath = path.join(__dirname, 'testhelpers', 'fixtures'); - }); - - beforeEach(() => { - renameSpy.mockClear(); - }); - - it('should move all contents of a package into the "package" folder', () => { - const packagePath = path.join(cachePath, 'sushi-test-wrong-format#current'); - cleanCachedPackage(packagePath); - expect(renameSpy.mock.calls.length).toBe(2); - expect(renameSpy.mock.calls).toContainEqual([ - path.join(packagePath, 'other'), - path.join(packagePath, 'package', 'other') - ]); - expect(renameSpy.mock.calls).toContainEqual([ - path.join(packagePath, 'StructureDefinition-MyPatient.json'), - path.join(packagePath, 'package', 'StructureDefinition-MyPatient.json') - ]); - }); - - it('should do nothing if the package does not have a "package" folder', () => { - const packagePath = path.join(cachePath, 'sushi-test-no-package#current'); - cleanCachedPackage(packagePath); - expect(renameSpy.mock.calls.length).toBe(0); - }); - - it('should do nothing if the package is correctly structured', () => { - const packagePath = path.join(cachePath, 'sushi-test#current'); - cleanCachedPackage(packagePath); - expect(renameSpy.mock.calls.length).toBe(0); - }); -}); - -describe('#lookUpLatestVersion', () => { - let axiosSpy: jest.SpyInstance; - - beforeAll(() => { - axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { - if ( - uri === 'https://custom-registry.example.org/hl7.terminology.r4' || - uri === 'https://packages.fhir.org/hl7.terminology.r4' - ) { - return { data: TERM_PKG_RESPONSE }; - } else if (uri === 'https://packages2.fhir.org/packages/hl7.fhir.uv.extensions') { - return { data: EXT_PKG_RESPONSE }; - } else if (uri === 'https://packages.fhir.org/hl7.no.latest') { - return { - data: { - name: 'hl7.no.latest', - 'dist-tags': { - v1: '1.5.1', - v2: '2.1.1' - } - } - }; - } else { - throw new Error('Not found'); - } - }); - }); - - afterEach(() => { - delete process.env.FPL_REGISTRY; - }); - - afterAll(() => { - axiosSpy.mockRestore(); - }); - - it('should get the latest version for a package on the packages server', async () => { - const latest = await lookUpLatestVersion('hl7.terminology.r4'); - expect(latest).toBe('1.2.3-test'); - }); - - it('should get the latest version for a package on the packages2 server', async () => { - const latest = await lookUpLatestVersion('hl7.fhir.uv.extensions'); - expect(latest).toBe('4.5.6-test'); - }); - - it('should get the latest version for a package on a custom server', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org/'; - const latest = await lookUpLatestVersion('hl7.terminology.r4'); - expect(latest).toBe('1.2.3-test'); - }); - - it('should throw LatestVersionUnavailableError when the request to get package information fails', async () => { - await expect(lookUpLatestVersion('hl7.bogus.package')).rejects.toThrow( - LatestVersionUnavailableError - ); - }); - - it('should throw LatestVersionUnavailableError when the package exists, but has no latest tag', async () => { - await expect(lookUpLatestVersion('hl7.no.latest')).rejects.toThrow( - LatestVersionUnavailableError - ); - }); -}); - -describe('#lookUpLatestPatchVersion', () => { - let axiosSpy: jest.SpyInstance; - - beforeAll(() => { - axiosSpy = jest.spyOn(axios, 'get').mockImplementation((uri: string): any => { - if ( - uri === 'https://custom-registry.example.org/hl7.terminology.r4' || - uri === 'https://packages.fhir.org/hl7.terminology.r4' - ) { - return { data: TERM_PKG_RESPONSE }; - } else if (uri === 'https://packages2.fhir.org/packages/hl7.fhir.uv.extensions') { - return { data: EXT_PKG_RESPONSE }; - } else if (uri === 'https://packages.fhir.org/hl7.no.versions') { - return { - data: { - name: 'hl7.no.versions', - 'dist-tags': { - v1: '1.5.1', - v2: '2.1.1' - } - } - }; - } else if (uri === 'https://packages.fhir.org/hl7.no.good.patches') { - return { - data: { - name: 'hl7.no.good.patches', - versions: { - '2.0.0': { - name: 'hl7.no.good.patches', - version: '2.0.0' - }, - '2.0.1': { - name: 'hl7.no.good.patches', - version: '2.0.1' - } - } - } - }; - } else if (uri === 'https://packages.fhir.org/hl7.patches.with.snapshots') { - return { - data: { - name: 'hl7.patches.with.snapshots', - versions: { - '2.0.0': { - name: 'hl7.patches.with.snapshots', - version: '2.0.0' - }, - '2.0.1': { - name: 'hl7.patches.with.snapshots', - version: '2.0.1' - }, - '2.0.2-snapshot1': { - name: 'hl7.patches.with.snapshots', - version: '2.0.2-snapshot1' - } - } - } - }; - } else { - throw new Error('Not found'); - } - }); - }); - - afterEach(() => { - delete process.env.FPL_REGISTRY; - }); - - afterAll(() => { - axiosSpy.mockRestore(); - }); - - it('should get the latest patch version for a package on the packages server', async () => { - const latestPatch = await lookUpLatestPatchVersion('hl7.terminology.r4', '1.1.x'); - expect(latestPatch).toBe('1.1.2'); - }); - - it('should get the latest patch version for a package on the packages2 server', async () => { - const latestPatch = await lookUpLatestPatchVersion('hl7.fhir.uv.extensions', '2.0.x'); - expect(latestPatch).toBe('2.0.2'); - }); - - it('should get the latest patch version for a package on a custom server', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org/'; - const latestPatch = await lookUpLatestPatchVersion('hl7.terminology.r4', '1.1.x'); - expect(latestPatch).toBe('1.1.2'); - }); - - it('should get the latest patch version ignoring any versions with qualifiers after the version (-snapshot1)', async () => { - const latestPatch = await lookUpLatestPatchVersion('hl7.patches.with.snapshots', '2.0.x'); - expect(latestPatch).toBe('2.0.1'); - }); - - it('should throw LatestVersionUnavailableError when the request to get package information fails', async () => { - await expect(lookUpLatestPatchVersion('hl7.bogus.package', '1.0.x')).rejects.toThrow( - LatestVersionUnavailableError - ); - }); - - it('should throw LatestVersionUnavailableError when the package exists, but has no versions listed', async () => { - await expect(lookUpLatestPatchVersion('hl7.no.versions', '1.0.x')).rejects.toThrow( - LatestVersionUnavailableError - ); - }); - - it('should throw LatestVersionUnavailableError when the package exists, but has no matching versions for the patch version supplied', async () => { - await expect(lookUpLatestPatchVersion('hl7.no.good.patches', '1.0.x')).rejects.toThrow( - LatestVersionUnavailableError - ); - }); - - it('should throw IncorrectWildcardVersionFormatError when a wildcard is used for minor version', async () => { - await expect(lookUpLatestPatchVersion('hl7.terminology.r4', '1.x')).rejects.toThrow( - IncorrectWildcardVersionFormatError - ); - }); -}); diff --git a/test/utils/logger.test.ts b/test/utils/logger.test.ts deleted file mode 100644 index 47b00ea..0000000 --- a/test/utils/logger.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ErrorsAndWarnings, wrapLogger, LogFunction } from '../../src/utils/logger'; - -describe('wrapLogger', () => { - const basicLog = jest.fn(); - let logWithTrack: LogFunction; - let errorsAndWarnings: ErrorsAndWarnings; - beforeAll(() => { - errorsAndWarnings = new ErrorsAndWarnings(); - logWithTrack = wrapLogger(basicLog, errorsAndWarnings); - }); - - beforeEach(() => { - errorsAndWarnings.reset(); - basicLog.mockClear(); - }); - - it('should track errors and warnings', () => { - logWithTrack('warn', 'warn1'); - logWithTrack('warn', 'warn2'); - logWithTrack('error', 'error1'); - logWithTrack('error', 'error2'); - expect(errorsAndWarnings.warnings).toHaveLength(2); - expect(errorsAndWarnings.warnings).toContainEqual('warn1'); - expect(errorsAndWarnings.warnings).toContainEqual('warn2'); - expect(errorsAndWarnings.errors).toHaveLength(2); - expect(errorsAndWarnings.errors).toContainEqual('error1'); - expect(errorsAndWarnings.errors).toContainEqual('error2'); - }); - - it('should reset errors and warnings', () => { - logWithTrack('warn', 'warn1'); - logWithTrack('error', 'error1'); - expect(errorsAndWarnings.warnings).toHaveLength(1); - expect(errorsAndWarnings.warnings).toContainEqual('warn1'); - expect(errorsAndWarnings.errors).toHaveLength(1); - expect(errorsAndWarnings.errors).toContainEqual('error1'); - errorsAndWarnings.reset(); - expect(errorsAndWarnings.errors).toHaveLength(0); - expect(errorsAndWarnings.warnings).toHaveLength(0); - }); - - it('should call the log callback', () => { - logWithTrack('error', 'error1'); - expect(basicLog).toHaveBeenCalledTimes(1); - expect(basicLog).toHaveBeenCalledWith('error', 'error1'); - logWithTrack('info', 'info1'); - expect(basicLog).toHaveBeenCalledTimes(2); - expect(basicLog).toHaveBeenCalledWith('info', 'info1'); - }); -});