From 6541681d663874762456e14450d6b335610ce8be Mon Sep 17 00:00:00 2001 From: Chris Moesel Date: Fri, 20 Dec 2024 13:52:43 -0500 Subject: [PATCH 1/4] Integrate FHIR Package Loader v2 (#1543) Integrate the brand new FHIR Package Loader (v2). The general approach was: * Update `FHIRDefinitions` to extend `BasePackageLoader` * Update fishing functions in `FHIRDefinitions` to use the `findResource` and `findResourceInfo` functions in `BasePackageLoader` -- ensuring that `FHIRDefinitions` still conforms to the expected `Fisher` interface. * Remove most other functions from `FHIRDefinitions` in favor of the functions inherited from `BasePackageLoader` * Update all affected code as necessary (most significant changes in package loading and IG Exporter) * Update tests to load most fixtures using FPL virtual packages This also updates the regression framework to support pre-caching of dependencies so the first run of SUSHI does not have the extra work of downloading all dependencies (which could skew results). It also reports out more information regarding performance. --- .tool-versions | 2 +- npm-shrinkwrap.json | 775 ++++++++++------ package.json | 23 +- regression/cli.ts | 10 + regression/run.ts | 291 +++++- src/app.ts | 31 +- src/export/StructureDefinitionExporter.ts | 2 +- src/fhirdefs/FHIRDefinitions.ts | 320 ++++--- src/fhirdefs/impliedExtensions.ts | 44 +- src/fhirdefs/index.ts | 1 - src/fhirdefs/load.ts | 135 --- src/fhirtypes/ElementDefinition.ts | 14 +- src/fhirtypes/InstanceDefinition.ts | 3 +- src/fhirtypes/StructureDefinition.ts | 2 + src/fhirtypes/common.ts | 40 +- src/ig/IGExporter.ts | 369 ++++---- src/ig/index.ts | 1 + src/ig/predefinedResources.ts | 90 ++ src/run/FshToFhir.ts | 4 +- src/utils/FSHLogger.ts | 24 +- src/utils/Fishable.ts | 3 +- src/utils/Processing.ts | 109 +-- test/export/CodeSystemExporter.test.ts | 9 +- test/export/FHIRExporter.test.ts | 17 +- test/export/InstanceExporter.test.ts | 27 +- test/export/MappingExporter.test.ts | 25 +- ...uctureDefinition.ExtensionExporter.test.ts | 16 +- ...tructureDefinition.LogicalExporter.test.ts | 43 +- ...tructureDefinition.ProfileExporter.test.ts | 9 +- ...ructureDefinition.ResourceExporter.test.ts | 9 +- .../StructureDefinitionExporter.test.ts | 111 +-- test/export/ValueSetExporter.test.ts | 9 +- test/fhirdefs/FHIRDefinitions.test.ts | 323 +++---- test/fhirdefs/impliedExtension.test.ts | 33 +- test/fhirdefs/load.test.ts | 211 ----- ...mentDefinition.applyAddElementRule.test.ts | 9 +- .../ElementDefinition.applyFlags.test.ts | 9 +- .../ElementDefinition.applyMapping.test.ts | 9 +- .../ElementDefinition.assignBoolean.test.ts | 9 +- .../ElementDefinition.assignFshCode.test.ts | 18 +- ...lementDefinition.assignFshQuantity.test.ts | 9 +- .../ElementDefinition.assignFshRatio.test.ts | 9 +- ...ementDefinition.assignFshReference.test.ts | 22 +- ...efinition.assignInstanceDefinition.test.ts | 9 +- .../ElementDefinition.assignNumber.test.ts | 8 +- .../ElementDefinition.assignString.test.ts | 8 +- .../ElementDefinition.assignValue.test.ts | 22 +- .../ElementDefinition.bindToVS.test.ts | 19 +- ...finition.checkAssignInlineInstance.test.ts | 9 +- ...entDefinition.constrainCardinality.test.ts | 9 +- .../ElementDefinition.constrainType.test.ts | 48 +- ...finition.setInstancePropertyByPath.test.ts | 9 +- .../ElementDefinition.slicing.test.ts | 10 +- test/fhirtypes/ElementDefinition.test.ts | 14 +- test/fhirtypes/StructureDefinition.test.ts | 10 +- test/ig/IGExporter.IG.test.ts | 184 ++-- ...GExporter.addConfiguredPageContent.test.ts | 7 +- test/ig/IGExporter.link-references.test.ts | 23 +- test/ig/IGExporter.test.ts | 14 +- .../StructureDefinition-MyPatient.json | 25 + .../input/stuff/Patient-BarPatient.json | 8 + test/ig/predefinedResources.test.ts | 218 +++++ test/run/FshToFhir.test.ts | 52 +- test/testhelpers/TestFHIRDefinitions.ts | 180 ++++ test/testhelpers/TestFisher.ts | 79 +- test/testhelpers/asserts.ts | 9 +- test/testhelpers/index.ts | 1 + test/utils/FishingUtils.test.ts | 7 +- test/utils/MasterFisher.test.ts | 37 +- test/utils/Processing.test.ts | 865 +++++------------- tsconfig.json | 8 +- 71 files changed, 2594 insertions(+), 2527 deletions(-) delete mode 100644 src/fhirdefs/load.ts create mode 100644 src/ig/predefinedResources.ts delete mode 100644 test/fhirdefs/load.test.ts create mode 100644 test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/resources/StructureDefinition-MyPatient.json create mode 100644 test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/stuff/Patient-BarPatient.json create mode 100644 test/ig/predefinedResources.test.ts create mode 100644 test/testhelpers/TestFHIRDefinitions.ts diff --git a/.tool-versions b/.tool-versions index 8f2e342a2..6df261cac 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 18.18.0 +nodejs 18.20.1 diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e30d0b1b1..eea4eb61d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -15,7 +15,7 @@ "chalk": "^4.1.2", "commander": "^12.1.0", "fhir": "^4.12.0", - "fhir-package-loader": "^1.0.0", + "fhir-package-loader": "^2.0.0", "fs-extra": "^11.2.0", "html-minifier-terser": "5.1.1", "https-proxy-agent": "^7.0.5", @@ -36,41 +36,42 @@ "sushi": "dist/app.js" }, "devDependencies": { - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.13.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.15.0", "@types/diff": "^5.2.3", "@types/fs-extra": "^11.0.4", "@types/html-minifier-terser": "5.1.1", "@types/ini": "^4.1.1", "@types/jest": "^29.5.14", "@types/json-diff": "^1.0.3", - "@types/lodash": "^4.17.12", - "@types/node": "^20.17.1", + "@types/lodash": "^4.17.13", + "@types/node": "^20.17.8", "@types/opener": "^1.4.3", "@types/readline-sync": "^1.4.8", "@types/sax": "^1.2.7", "@types/temp": "^0.9.4", "@types/text-table": "^0.2.5", "@types/valid-url": "^1.0.7", - "@typescript-eslint/eslint-plugin": "^8.12.0", - "@typescript-eslint/parser": "^8.12.0", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "acorn": "^8.14.0", "copyfiles": "^2.4.1", "del-cli": "^6.0.0", "diff": "^7.0.0", "diff2html-cli": "^5.2.15", - "eslint": "^9.13.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "extract-zip": "^2.0.1", "jest": "^29.7.0", "jest-extended": "^4.0.2", + "jest-mock-extended": "^4.0.0-beta1", "json-diff": "^1.0.6", - "nock": "^13.5.5", + "nock": "^13.5.6", "opener": "^1.5.2", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.6.3" + "typescript": "^5.7.2" } }, "node_modules/@ampproject/remapping": { @@ -707,18 +708,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.4", @@ -752,19 +753,18 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -829,11 +829,10 @@ } }, "node_modules/@eslint/js": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", - "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -852,7 +851,6 @@ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "levn": "^0.4.1" }, @@ -920,6 +918,106 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1437,6 +1535,15 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1642,18 +1749,16 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", - "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==", - "dev": true, - "license": "MIT" + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true }, "node_modules/@types/node": { - "version": "20.17.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz", - "integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==", + "version": "20.17.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", + "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } @@ -1740,17 +1845,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.0.tgz", - "integrity": "sha512-uRqchEKT0/OwDePTwCjSFO2aH4zccdeQ7DgAzM/8fuXc+PAXvpdMRbuo+oCmK1lSfXssk2UUBNiWihobKxQp/g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.12.0", - "@typescript-eslint/type-utils": "8.12.0", - "@typescript-eslint/utils": "8.12.0", - "@typescript-eslint/visitor-keys": "8.12.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1774,16 +1878,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.0.tgz", - "integrity": "sha512-7U20duDQWAOhCk2VtyY41Vor/CJjiEW063Zel9aoRXq89FQ/jr+0e0m3kxh9Sk5SFW9B1AblVIBtXd+1xQ1NWQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.12.0", - "@typescript-eslint/types": "8.12.0", - "@typescript-eslint/typescript-estree": "8.12.0", - "@typescript-eslint/visitor-keys": "8.12.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4" }, "engines": { @@ -1803,14 +1906,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.0.tgz", - "integrity": "sha512-jbuCXK18iEshRFUtlCIMAmOKA6OAsKjo41UcXPqx7ZWh2b4cmg6pV/pNcZSB7oW9mtgF95yizr7Jnwt3IUD2pA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.0", - "@typescript-eslint/visitor-keys": "8.12.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1821,14 +1923,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.0.tgz", - "integrity": "sha512-cHioAZO/nLgyzTmwv7gWIjEKMHSbioKEZqLCaItTn7RvJP1QipuGVwEjPJa6Kv9u9UiUMVAESY9JH186TjKITw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.12.0", - "@typescript-eslint/utils": "8.12.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1839,6 +1940,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -1846,11 +1950,10 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.0.tgz", - "integrity": "sha512-Cc+iNtqBJ492f8KLEmKXe1l6683P0MlFO8Bk1NMphnzVIGH4/Wn9kvandFH+gYR1DDUjH/hgeWRGdO5Tj8gjYg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1860,14 +1963,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.0.tgz", - "integrity": "sha512-a4koVV7HHVOQWcGb6ZcAlunJnAdwo/CITRbleQBSjq5+2WLoAJQCAAiecvrAdSM+n/man6Ghig5YgdGVIC6xqw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.12.0", - "@typescript-eslint/visitor-keys": "8.12.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1889,16 +1991,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-5i1tqLwlf0fpX1j05paNKyIzla/a4Y3Xhh6AFzi0do/LDJLvohtZYaisaTB9kq0D4uBocAxWDTGzNMOCCwIgXA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.12.0", - "@typescript-eslint/types": "8.12.0", - "@typescript-eslint/typescript-estree": "8.12.0" + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1909,17 +2010,21 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.0.tgz", - "integrity": "sha512-2rXkr+AtZZLuNY18aUjv5wtB9oUiwY1WnNi7VTsdCdy1m958ULeUKoAegldQTjqpbpNJ5cQ4egR8/bh5tbrKKQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1929,6 +2034,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2026,7 +2143,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2089,10 +2205,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2224,7 +2339,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2395,11 +2509,11 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/ci-info": { @@ -2651,7 +2765,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2940,6 +3053,11 @@ "node": ">=0.4.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2976,8 +3094,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enabled": { "version": "2.0.0", @@ -3024,32 +3141,31 @@ } }, "node_modules/eslint": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", - "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.13.0", - "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.5", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3063,8 +3179,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -3097,11 +3212,10 @@ } }, "node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3125,6 +3239,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3152,11 +3279,10 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3183,15 +3309,14 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3201,11 +3326,10 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3243,7 +3367,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3479,33 +3602,28 @@ } }, "node_modules/fhir-package-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-1.0.0.tgz", - "integrity": "sha512-x3VY3RY1wkJv8Fd7dA7fY3aw+6Vg7qeCU0pci7wUaEhnJ84k7Lnca6dfH00l36uzH1N5EwVX51iKuuwsS6RdlA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-2.0.0.tgz", + "integrity": "sha512-26TQYVU6frmlluiDY0Nm/hjgGertOdN092BRHja+yCbAx1kvk4sBzAW9SmmaD33SeHzsDxEPWvTRBEDq/5nSFw==", "dependencies": { - "axios": "^1.6.7", + "axios": "^1.7.8", "chalk": "^4.1.2", - "commander": "^11.1.0", + "commander": "^12.1.0", + "fhir": "^4.12.0", "fs-extra": "^11.2.0", - "https-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", "lodash": "^4.17.21", - "semver": "^7.5.4", - "tar": "^6.2.0", + "mnemonist": "^0.39.8", + "semver": "^7.6.3", + "sql.js": "^1.12.0", + "tar": "^7.4.3", "temp": "^0.9.1", - "winston": "^3.11.0" + "winston": "^3.17.0" }, "bin": { "fpl": "dist/app.js" } }, - "node_modules/fhir-package-loader/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "engines": { - "node": ">=16" - } - }, "node_modules/fhir/node_modules/inherits": { "version": "2.0.3", "inBundle": true, @@ -3670,6 +3788,21 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3708,33 +3841,6 @@ "node": ">=14.14" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4143,7 +4249,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -4272,8 +4377,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -4341,6 +4445,20 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -4882,6 +5000,20 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-mock-extended": { + "version": "4.0.0-beta1", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-4.0.0-beta1.tgz", + "integrity": "sha512-MYcI0wQu3ceNhqKoqAJOdEfsVMamAFqDTjoLN5Y45PAG3iIm4WGnhOu0wpMjlWCexVPO71PMoNir9QrGXrnIlw==", + "dev": true, + "dependencies": { + "ts-essentials": "^10.0.2" + }, + "peerDependencies": { + "@jest/globals": "^28.0.0 || ^29.0.0", + "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0", + "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -5349,9 +5481,9 @@ "dev": true }, "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -5493,7 +5625,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -5514,45 +5645,30 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { - "node": ">=8" + "node": ">= 18" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -5560,6 +5676,14 @@ "node": ">=10" } }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5581,9 +5705,9 @@ } }, "node_modules/nock": { - "version": "13.5.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", - "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", "dev": true, "dependencies": { "debug": "^4.1.0", @@ -5704,6 +5828,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5831,6 +5960,11 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -5900,7 +6034,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5911,6 +6044,26 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/path-type": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", @@ -6031,9 +6184,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", + "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -6291,6 +6444,39 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -6334,9 +6520,9 @@ "dev": true }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "engines": { "node": ">=10" } @@ -6369,7 +6555,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6381,7 +6566,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -6390,7 +6574,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -6453,6 +6636,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sql.js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.12.0.tgz", + "integrity": "sha512-Bi+43yMx/tUFZVYD4AUscmdL6NHn3gYQ+CM+YheFWLftOmrEC/Mz6Yh7E96Y2WDHYz3COSqT+LP6Z79zgrwJlA==" + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -6505,7 +6693,20 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6519,7 +6720,18 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6596,25 +6808,42 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } }, "node_modules/temp": { "version": "0.9.4", @@ -6830,6 +7059,20 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-essentials": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.0.3.tgz", + "integrity": "sha512-/FrVAZ76JLTWxJOERk04fm8hYENDo0PWSP3YLQKxevLwWtxemGcl5JJEzN4iqfDlRve0ckyfFaOBu4xbNH/wZw==", + "dev": true, + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ts-jest": { "version": "29.2.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", @@ -6966,11 +7209,10 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7111,7 +7353,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -7123,34 +7364,33 @@ } }, "node_modules/winston": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", - "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", - "license": "MIT", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.6.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", - "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", + "logform": "^2.7.0", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": { @@ -7280,6 +7520,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index b9856516c..937f135b5 100644 --- a/package.json +++ b/package.json @@ -49,41 +49,42 @@ ], "license": "Apache-2.0", "devDependencies": { - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.13.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.15.0", "@types/diff": "^5.2.3", "@types/fs-extra": "^11.0.4", "@types/html-minifier-terser": "5.1.1", "@types/ini": "^4.1.1", "@types/jest": "^29.5.14", "@types/json-diff": "^1.0.3", - "@types/lodash": "^4.17.12", - "@types/node": "^20.17.1", + "@types/lodash": "^4.17.13", + "@types/node": "^20.17.8", "@types/opener": "^1.4.3", "@types/readline-sync": "^1.4.8", "@types/sax": "^1.2.7", "@types/temp": "^0.9.4", "@types/text-table": "^0.2.5", "@types/valid-url": "^1.0.7", - "@typescript-eslint/eslint-plugin": "^8.12.0", - "@typescript-eslint/parser": "^8.12.0", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "acorn": "^8.14.0", "copyfiles": "^2.4.1", "del-cli": "^6.0.0", "diff": "^7.0.0", "diff2html-cli": "^5.2.15", - "eslint": "^9.13.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "extract-zip": "^2.0.1", "jest": "^29.7.0", "jest-extended": "^4.0.2", + "jest-mock-extended": "^4.0.0-beta1", "json-diff": "^1.0.6", - "nock": "^13.5.5", + "nock": "^13.5.6", "opener": "^1.5.2", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.6.3" + "typescript": "^5.7.2" }, "dependencies": { "ajv": "^8.17.1", @@ -92,7 +93,7 @@ "chalk": "^4.1.2", "commander": "^12.1.0", "fhir": "^4.12.0", - "fhir-package-loader": "^1.0.0", + "fhir-package-loader": "^2.0.0", "fs-extra": "^11.2.0", "html-minifier-terser": "5.1.1", "https-proxy-agent": "^7.0.5", diff --git a/regression/cli.ts b/regression/cli.ts index f679ba160..67acdd4c6 100644 --- a/regression/cli.ts +++ b/regression/cli.ts @@ -46,6 +46,11 @@ async function main() { 'The folder to write regression data to', path.relative(process.cwd(), path.join(__dirname, 'output')) ) + .option( + '-d, --disableDependencyPrecaching', + 'Disable IG dependency precaching. Note that disabling precaching may make the baseline version of SUSHI appear slower since it may need to download dependencies not yet in the cache.', + false + ) .action(runAction); program @@ -137,6 +142,11 @@ async function runAction(options: any) { } config.continued = false; } + + if (options.disableDependencyPrecaching) { + config.disablePrecaching = options.disableDependencyPrecaching; + } + const data = new RegressionData(config); if (doContinue) { dataJSON!.repos.forEach(r => { diff --git a/regression/run.ts b/regression/run.ts index 42e167987..cc9a61067 100644 --- a/regression/run.ts +++ b/regression/run.ts @@ -8,11 +8,19 @@ import axios from 'axios'; import readlineSync from 'readline-sync'; import extract from 'extract-zip'; import opener from 'opener'; -import { isEqual, union } from 'lodash'; +import { isEqual, mean, union } from 'lodash'; import { createTwoFilesPatch } from 'diff'; import { diffString } from 'json-diff'; import chalk from 'chalk'; +import { FHIRDefinitions, createFHIRDefinitions } from '../src/fhirdefs/FHIRDefinitions'; import { findReposUsingFSHFinder } from './find'; +import { + loadExternalDependencies, + readConfig, + restoreMainLogger, + switchToSecretLogger +} from '../src/utils'; +import { DiskBasedPackageCache } from 'fhir-package-loader'; // Track temporary files so they are deleted when the process exits temp.track(); @@ -26,6 +34,7 @@ export class Config { repos?: string[]; file?: string; }; + disablePrecaching: boolean; output: string; dataFile: string; continued: boolean; @@ -122,6 +131,63 @@ class RunStats { public elapsed?: number; constructor(public errors: number) {} + + get elapsedSeconds() { + if (this.elapsed != null) { + return Math.round(this.elapsed / 1000); + } + } +} + +class SummaryStats { + constructor( + private repos: Repo[], + private elapsedTime: number + ) {} + + get totalRepos() { + return this.repos.length; + } + + get reposWithDiffErrors() { + return this.repos.filter(r => r.sushiStats1.errors != r.sushiStats2.errors).length; + } + + get reposWithDiffWarnings() { + return this.repos.filter(r => r.sushiStats1.warnings != r.sushiStats2.warnings).length; + } + + get reposWithDiffOutput() { + return this.repos.filter(r => r.changed).length; + } + + get reposWithSlowerTime() { + return this.repos.filter(r => r.getTimeDeltaPercentage() > 0).length; + } + + get reposWithFasterTime() { + return this.repos.filter(r => r.getTimeDeltaPercentage() < 0).length; + } + + get avgTimeDeltaPercentage() { + return roundAwayFromZero(mean(this.repos.map(r => r.getTimeDeltaPercentage()))); + } + + get avgTimeSlowerPercentage() { + return roundAwayFromZero( + mean(this.repos.map(r => r.getTimeDeltaPercentage()).filter(t => t > 0)) + ); + } + + get avgTimeFasterPercentage() { + return roundAwayFromZero( + mean(this.repos.map(r => r.getTimeDeltaPercentage()).filter(t => t < 0)) + ); + } + + get totalTime() { + return this.elapsedTime; + } } export class Repo { @@ -135,6 +201,28 @@ export class Repo { public branch: string ) {} + getTimeDeltaSeconds() { + const [time1, time2] = [ + this.sushiStats1.elapsedSeconds ?? 0, + this.sushiStats2.elapsedSeconds ?? 0 + ]; + if (time1 === 0 || time2 === 0) { + // probably invalid, so just return 0 + return 0; + } + return time2 - time1; + } + + getTimeDeltaPercentage() { + const [time1, time2] = [this.sushiStats1.elapsed ?? 0, this.sushiStats2.elapsed ?? 0]; + if (time1 === 0 || time2 === 0) { + // probably invalid, so just return 0 + return 0; + } + // Round away from zero to whole percentage + return roundAwayFromZero((time2 / time1 - 1) * 100); + } + getDownloadURL() { return `https://github.com/${this.name}/archive/${this.branch}.zip`; } @@ -160,6 +248,10 @@ export async function run(config: Config, data: RegressionData) { const htmlTemplate = await fs.readFile(path.join(__dirname, 'template.html'), 'utf8'); const jsonTemplate = await fs.readFile(path.join(__dirname, 'jsontemplate.html'), 'utf8'); await Promise.all([setupSUSHI(1, config), setupSUSHI(2, config)]); + let packageCacher: FHIRDefinitions | undefined; + if (!config.disablePrecaching) { + packageCacher = await getPackageCacher(); + } const repos = await getRepoList(config); // Iterate repos synchronously since running more than one SUSHI in parallel might cause // issues w/ .fhir cache management. We *could* do the downloads and extractions and @@ -195,36 +287,46 @@ export async function run(config: Config, data: RegressionData) { repo.error = true; continue; } - // We can only run SUSHI one at a time due to its asynch management of the .fhir cache + if (packageCacher) { + await precacheDependencies(repo, config, packageCacher); + } + // We should only run SUSHI one at a time due to its asynch management of the .fhir cache repo.sushiStats1 = await runSUSHI(1, repo, config); repo.sushiStats2 = await runSUSHI(2, repo, config); await generateDiff(repo, config, htmlTemplate, jsonTemplate); data.repos.push(repo); await fs.writeJSON(config.dataFile, data, { spaces: 2 }); } - await createReport(repos, config); + const elapsed = Math.ceil((new Date().getTime() - start.getTime()) / 1000); + await createReport(repos, elapsed, config); data.done = true; await fs.writeJSON(config.dataFile, data, { spaces: 2 }); - const elapsed = Math.ceil((new Date().getTime() - start.getTime()) / 1000); console.log(`Total time: ${elapsed} seconds`); } -export function logOptions(config: Config) { - console.log(` --a ${config.version1}`); - console.log(` --b ${config.version2}`); +export function getOptionStrings(config: Config) { + const options = [`--a ${config.version1}`, `--b ${config.version2}`]; if (config.repoOptions.lookback != null) { - console.log(` --lookback ${config.repoOptions.lookback}`); + options.push(`--lookback ${config.repoOptions.lookback}`); } if (config.repoOptions.count != null) { - console.log(` --count ${config.repoOptions.count}`); + options.push(`--count ${config.repoOptions.count}`); } if (config.repoOptions.repos != null) { - console.log(` --repo ${config.repoOptions.repos.join(' ')}`); + options.push(`--repo ${config.repoOptions.repos.join(' ')}`); } if (config.repoOptions.file != null) { - console.log(` --file ${config.repoOptions.file}`); + options.push(`--file ${config.repoOptions.file}`); + } + options.push(`--out ${config.output}`); + if (config.disablePrecaching) { + options.push('--disableDependencyPrecaching'); } - console.log(` --out ${config.output}`); + return options; +} + +export function logOptions(config: Config) { + getOptionStrings(config).forEach(o => console.log(` ${o}`)); } async function prepareOutputFolder(config: Config): Promise { @@ -357,6 +459,40 @@ async function downloadZip(zipURL: string, zipPath: string) { }); } +// A special FHIRDefinitions that downloads and caches packages but doesn't register resources +async function getPackageCacher(isSupplemental = false): Promise { + const defs = await createFHIRDefinitions(isSupplemental, async () => getPackageCacher(true), { + packageCache: new (class PackageCacheForRegression extends DiskBasedPackageCache { + constructor() { + super(path.join(os.homedir(), '.fhir', 'packages')); + } + + // Override getPotentialResourcePaths to avoid registering all the resources + getPotentialResourcePaths(): string[] { + return []; + } + })() + }); + return defs; +} + +async function precacheDependencies(repo: Repo, config: Config, packageCacher: FHIRDefinitions) { + process.stdout.write(' - Precaching IG dependencies'); + // suppress logging so we don't see all the package logging + const loggerData = switchToSecretLogger(); + try { + const sushiConfig = readConfig(config.getRepoSUSHIDir(repo, 1)); + if (sushiConfig) { + await loadExternalDependencies(packageCacher, sushiConfig); + } + } catch { + process.stdout.write(' (precache failed)'); + } finally { + restoreMainLogger(loggerData); + process.stdout.write('\n'); + } +} + async function runSUSHI(num: 1 | 2, repo: Repo, config: Config): Promise { const version = config.getVersion(num); const repoSUSHIDir = config.getRepoSUSHIDir(repo, num); @@ -377,7 +513,7 @@ async function runSUSHI(num: 1 | 2, repo: Repo, config: Config): Promise Number.parseInt(m)); @@ -483,10 +619,19 @@ async function generateDiff( ], { cwd: path.dirname(__dirname), shell: true } ); - process.stdout.write(': CHANGED\n'); + process.stdout.write(': CHANGED'); } else { - process.stdout.write(': SAME\n'); + process.stdout.write(': SAME'); } + const deltaSeconds = repo.getTimeDeltaSeconds(); + if (deltaSeconds !== 0) { + const deltaPercentage = repo.getTimeDeltaPercentage(); + const label = deltaSeconds > 0 ? 'slower' : 'faster'; + process.stdout.write( + ` (${Math.abs(deltaPercentage)}% / ${Math.abs(deltaSeconds)}s ${label})` + ); + } + process.stdout.write('\n'); } catch (e) { repo.changed = true; const message = `Error comparing results: ${e}`; @@ -546,16 +691,17 @@ function sortRepos(repos: Repo[]) { if (aWrnsChanged != bWrnsChanged) { return aWrnsChanged ? -1 : 1; } - // Repos that slowed down by 20% or more and took more than 30s go next - const [aSlowed, bSlowed] = [a, b].map( - repo => - repo.sushiStats1.elapsed && - repo.sushiStats2.elapsed && - repo.sushiStats2.elapsed >= 30 && - repo.sushiStats2.elapsed >= repo.sushiStats1.elapsed * 1.2 + // Then by delta time to run (in seconds) + const [aDeltaSeconds, bDeltaSeconds] = [a, b].map(repo => Math.abs(repo.getTimeDeltaSeconds())); + if (aDeltaSeconds != bDeltaSeconds) { + return bDeltaSeconds - aDeltaSeconds; + } + // Then by delta time to run (in percent) + const [aDeltaPercent, bDeltaPercent] = [a, b].map(repo => + Math.abs(repo.getTimeDeltaPercentage()) ); - if (aSlowed != bSlowed) { - return aSlowed ? -1 : 1; + if (aDeltaPercent != bDeltaPercent) { + return bDeltaPercent - aDeltaPercent; } // Then alphabetical return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; @@ -573,20 +719,49 @@ function reportWarnings(repo: Repo) { } function reportElapsed(repo: Repo) { - const [time1, time2] = [repo.sushiStats1.elapsed ?? 0, repo.sushiStats2.elapsed ?? 0]; + const [time1, time2] = [ + repo.sushiStats1.elapsedSeconds ?? 0, + repo.sushiStats2.elapsedSeconds ?? 0 + ]; if (time1 === time2) { return time1; } else { - let report = `${time1} → ${time2}`; - if (time2 >= 30 && time2 >= time1 * 1.2) { - report = `${report}`; - } - return report; + return `${time1} → ${time2}`; + } +} + +function reportPerformance(repo: Repo) { + const deltaSeconds = repo.getTimeDeltaSeconds(); + if (deltaSeconds === 0) { + return ''; } + const deltaPercent = repo.getTimeDeltaPercentage(); + const color = deltaSeconds > 0 ? 'red' : 'green'; + const label = deltaSeconds > 0 ? 'slower' : 'faster'; + return `${Math.abs(deltaPercent)}% ${label} (${Math.abs(deltaSeconds)} seconds)`; } -async function createReport(repos: Repo[], config: Config) { +function reportAveragePerformance(deltaPercent: number, numRepos?: number) { + const repos = numRepos == null ? '' : ` (${numRepos} repos)`; + if (deltaPercent === 0) { + return `0%${repos}`; + } else if (Number.isNaN(deltaPercent)) { + return `n/a${repos}`; + } + const color = deltaPercent > 0 ? 'red' : 'green'; + const label = deltaPercent > 0 ? 'slower' : 'faster'; + return `${Math.abs(deltaPercent)}% ${label}${repos}`; +} + +function roundAwayFromZero(num: number) { + // Math.round always rounds up (e.g., round(-0.5) is 0) + // but we want to round away from zero (e.g. round(-0.5) is -1) + return Math.round(Math.abs(num)) * (num < 0 ? -1 : 1); +} + +async function createReport(repos: Repo[], elapsed: number, config: Config) { sortRepos(repos); + const stats = new SummaryStats(repos, elapsed); const reportFile = config.getOverallDiffReport(); await fs.appendFile( reportFile, @@ -634,9 +809,59 @@ async function createReport(repos: Repo[], config: Config) { padding: 10px; border-bottom: 1px solid #ccc; } + + table#summary { + margin: 0; + width: auto; + white-space: nowrap; + } +

SUSHI Regression Results

+

Options: ${getOptionStrings(config).join(' ')}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Summary
Total Repos${stats.totalRepos}
Repos w/ Different Output${stats.reposWithDiffOutput}
Repos w/ Different Error Count${stats.reposWithDiffErrors}
Repos w/ Different Warning Count${stats.reposWithDiffWarnings}
Mean Performance${reportAveragePerformance(stats.avgTimeDeltaPercentage)}
Mean Performance for Slower Repos${reportAveragePerformance(stats.avgTimeSlowerPercentage, stats.reposWithSlowerTime)}
Mean Performance for Faster Repos${reportAveragePerformance(stats.avgTimeFasterPercentage, stats.reposWithFasterTime)}
Total Time${stats.totalTime} seconds
+

Detailed Results

@@ -644,6 +869,7 @@ async function createReport(repos: Repo[], config: Config) { + @@ -666,6 +892,7 @@ async function createReport(repos: Repo[], config: Config) { + - + `, { encoding: 'utf8' } diff --git a/src/app.ts b/src/app.ts index f08ffe48c..913d1d93f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,11 +6,11 @@ import { Command, OptionValues, Option } from 'commander'; import chalk from 'chalk'; import process from 'process'; import { pad, padStart, padEnd } from 'lodash'; +import { DefaultRegistryClient } from 'fhir-package-loader'; import { FSHTank, RawFSH } from './import'; import { exportFHIR, Package } from './export'; -import { IGExporter } from './ig'; -import { loadCustomResources } from './fhirdefs'; -import { FHIRDefinitions } from './fhirdefs'; +import { IGExporter, loadPredefinedResources } from './ig'; +import { createFHIRDefinitions } from './fhirdefs'; import { Configuration } from './fshtypes'; import { logger, @@ -33,7 +33,8 @@ import { getLocalSushiVersion, checkSushiVersion, writeFSHIndex, - updateConfig + updateConfig, + logMessage } from './utils'; const FSH_VERSION = '3.0.0'; @@ -153,7 +154,8 @@ async function app() { async function runUpdateDependencies(projectPath: string) { const input = ensureInputDir(projectPath); const config: Configuration = readConfig(input); - await updateExternalDependencies(config); + const registryClient = new DefaultRegistryClient({ log: logMessage }); + await updateExternalDependencies(config, registryClient); } async function runBuild(input: string, program: OptionValues, helpText: string) { @@ -279,11 +281,14 @@ async function runBuild(input: string, program: OptionValues, helpText: string) } // Load dependencies - const defs = new FHIRDefinitions(); + const defs = await createFHIRDefinitions(false); await loadExternalDependencies(defs, config); - // Load custom resources. In current tank configuration (input/fsh), resources will be in input/ - loadCustomResources(path.join(input, '..'), originalInput, config.parameters, defs); + // Load custom resources from typical input/* paths and custom configured paths + await loadPredefinedResources(defs, path.join(input, '..'), originalInput, config.parameters); + + // Optimize the database after loading to ensure the most efficient queries + defs.optimize(); // Check for StructureDefinition const structDef = defs.fishForFHIR('StructureDefinition', Type.Resource); @@ -296,6 +301,16 @@ async function runBuild(input: string, program: OptionValues, helpText: string) process.exit(1); } + // UNCOMMENT the following lines to get a SQLite export of the FPL database for debugging + // + // const fplExport = await defs.exportDB(); + // if (fplExport.mimeType === 'application/x-sqlite3') { + // const exportPath = path.join(outDir, 'fsh-generated', 'FPL.sqlite'); + // fs.ensureDirSync(path.join(outDir, 'fsh-generated')); + // fs.writeFileSync(exportPath, fplExport.data); + // logger.info(`Exported FPL database to ${exportPath}`); + // } + logger.info('Converting FSH to FHIR resources...'); const outPackage = exportFHIR(tank, defs); const { skippedResources } = writeFHIRResources(outDir, outPackage, defs, program.snapshot); diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts index f1c2f762a..7f34dbe0c 100644 --- a/src/export/StructureDefinitionExporter.ts +++ b/src/export/StructureDefinitionExporter.ts @@ -1064,7 +1064,7 @@ export class StructureDefinitionExporter implements Fishable { } } } catch (e) { - logger.error(e.message, rule.sourceInfo); + logger.error(e.message ?? e, rule.sourceInfo); if (e.stack) { logger.debug(e.stack); } diff --git a/src/fhirdefs/FHIRDefinitions.ts b/src/fhirdefs/FHIRDefinitions.ts index 3ac06c77a..9d42013f5 100644 --- a/src/fhirdefs/FHIRDefinitions.ts +++ b/src/fhirdefs/FHIRDefinitions.ts @@ -1,33 +1,95 @@ -import { cloneDeep, flatten } from 'lodash'; -import { FHIRDefinitions as BaseFHIRDefinitions } from 'fhir-package-loader'; -import { Type, Metadata, Fishable } from '../utils'; -import { IMPLIED_EXTENSION_REGEX, materializeImpliedExtension } from './impliedExtensions'; -import { R5_DEFINITIONS_NEEDED_IN_R4 } from './R5DefsForR4'; +import path from 'path'; +import os from 'os'; +import { flatten } from 'lodash'; import { - LOGICAL_TARGET_EXTENSION, - TYPE_CHARACTERISTICS_EXTENSION, - findImposeProfiles -} from '../fhirtypes/common'; + BasePackageLoader, + BasePackageLoaderOptions, + BuildDotFhirDotOrgClient, + CurrentBuildClient, + DefaultRegistryClient, + DiskBasedPackageCache, + PackageCache, + PackageDB, + RegistryClient, + ResourceInfo, + SQLJSPackageDB, + SafeMode, + byLoadOrder, + byType +} from 'fhir-package-loader'; +import { PREDEFINED_PACKAGE_NAME } from '../ig'; +import { Type, Metadata, Fishable, logger } from '../utils'; +import { + IMPLIED_EXTENSION_REGEX, + materializeImpliedExtension, + materializeImpliedExtensionMetadata +} from './impliedExtensions'; + +const FISHING_ORDER = [ + Type.Resource, + Type.Logical, + Type.Type, + Type.Profile, + Type.Extension, + Type.ValueSet, + Type.CodeSystem +]; + +const DEFAULT_SORT = [byType(...FISHING_ORDER), byLoadOrder(false)]; -export class FHIRDefinitions extends BaseFHIRDefinitions implements Fishable { - private predefinedResources: Map; +export class FHIRDefinitions extends BasePackageLoader implements Fishable { + private fplLogInterceptor: (level: string, message: string) => boolean; + private fplPackageDB: PackageDB; private supplementalFHIRDefinitions: Map; - constructor(public readonly isSupplementalFHIRDefinitions = false) { - super(); - this.predefinedResources = new Map(); - this.supplementalFHIRDefinitions = new Map(); - // There are several R5 resources that are allowed for use in R4 and R4B. - // Add them first so they're always available. If a later version is loaded - // that has these definitions, it will overwrite them, so this should be safe. - if (!isSupplementalFHIRDefinitions) { - R5_DEFINITIONS_NEEDED_IN_R4.forEach(def => this.add(def)); + constructor( + public readonly isSupplementalFHIRDefinitions = false, + private supplementalFHIRDefinitionsFactory?: () => Promise, + // override is mainly intended to be used in unit tests + override?: { + packageDB?: PackageDB; + packageCache?: PackageCache; + registryClient?: RegistryClient; + currentBuildClient?: CurrentBuildClient; + options?: BasePackageLoaderOptions; } - } + ) { + let options: BasePackageLoaderOptions = { + // Analysis of 500 projects shows most only need a cache of 100. Double it for the others. + resourceCacheSize: 200, + // Cloning every resource is slow, but we need some safety from unintentional modification. + safeMode: SafeMode.FREEZE, + // Use the same logger as SUSHI uses + log: (level: string, message: string) => { + // if there is an interceptor, invoke it and suppress the log if appropriate + if (this.fplLogInterceptor) { + const continueToLog = this.fplLogInterceptor(level, message); + if (!continueToLog) { + return; + } + } + logger.log(level, message); + } + }; + if (override?.options) { + options = Object.assign(options, override.options); + } + const packageDB = override?.packageDB ?? new SQLJSPackageDB(); + const fhirCache = path.join(os.homedir(), '.fhir', 'packages'); + const packageCache = override?.packageCache ?? new DiskBasedPackageCache(fhirCache, options); + const registryClient = override?.registryClient ?? new DefaultRegistryClient(options); + const buildClient = override?.currentBuildClient ?? new BuildDotFhirDotOrgClient(options); + super(packageDB, packageCache, registryClient, buildClient, options); + this.fplPackageDB = packageDB; - // Expose the package.json files to support extracting the version when "latest" is used - allPackageJsons(): any[] { - return Array.from(this.packageJsons?.values() ?? []); + this.supplementalFHIRDefinitions = new Map(); + if (!supplementalFHIRDefinitionsFactory) { + this.supplementalFHIRDefinitionsFactory = async () => { + const fhirDefs = new FHIRDefinitions(true); + await fhirDefs.initialize(); + return fhirDefs; + }; + } } // This getter is only used in tests to verify what supplemental packages are loaded @@ -35,42 +97,20 @@ export class FHIRDefinitions extends BaseFHIRDefinitions implements Fishable { return flatten(Array.from(this.supplementalFHIRDefinitions.keys())); } - allPredefinedResources(makeClone = true): any[] { - if (makeClone) { - return Array.from(this.predefinedResources.values()).map(v => cloneDeep(v)); - } else { - return Array.from(this.predefinedResources.values()); + async initialize() { + if (this.fplPackageDB instanceof SQLJSPackageDB) { + await this.fplPackageDB.initialize(); } } - add(definition: any): void { - // For supplemental FHIR versions, we only care about resources and types, - // but for normal packages, we care about everything. - if (this.isSupplementalFHIRDefinitions) { - if ( - definition.resourceType === 'StructureDefinition' && - (definition.kind === 'primitive-type' || - definition.kind === 'complex-type' || - definition.kind === 'datatype' || - (definition.kind === 'resource' && definition.derivation !== 'constraint')) - ) { - super.add(definition); - } - } else { - super.add(definition); - } - } - - addPredefinedResource(file: string, definition: any): void { - this.predefinedResources.set(file, definition); - } - - getPredefinedResource(file: string): any { - return this.predefinedResources.get(file); - } - - resetPredefinedResources() { - this.predefinedResources = new Map(); + /** + * An interceptor that can suppress FPL log messages based on level or message. This is + * primarily used to suppress error logs when loading automatic dependencies. + * @param interceptor an interceptor method that receives log information and returns true to + * continue logging or false to suppress that log statement + */ + setFHIRPackageLoaderLogInterceptor(interceptor?: (level: string, message: string) => boolean) { + this.fplLogInterceptor = interceptor; } addSupplementalFHIRDefinitions(fhirPackage: string, definitions: FHIRDefinitions): void { @@ -81,40 +121,63 @@ export class FHIRDefinitions extends BaseFHIRDefinitions implements Fishable { return this.supplementalFHIRDefinitions.get(fhirPackage); } + /** + * Loads a "supplemental" FHIR package other than the primary FHIR version being used. This is + * needed to support extensions for converting between versions (e.g., "implied" extensions). + * The definitions from the supplemental FHIR package are not loaded into the main set of + * definitions, but rather, are loaded into their own private FHIRDefinitions. + * @param fhirPackage - the FHIR package to load in the format {packageId}#{version} + * @returns Promise promise that always resolves successfully (even if there is an error) + */ + async loadSupplementalFHIRPackage(fhirPackage: string): Promise { + const supplementalDefs = await this.supplementalFHIRDefinitionsFactory(); + const [fhirPackageId, fhirPackageVersion] = fhirPackage.split('#'); + await supplementalDefs + .loadPackage(fhirPackageId, fhirPackageVersion) + .then(status => { + if (status == 'LOADED') { + this.addSupplementalFHIRDefinitions(fhirPackage, supplementalDefs); + } + }) + .catch(e => { + logger.error(`Failed to load supplemental FHIR package ${fhirPackage}: ${e.message}`); + if (e.stack) { + logger.debug(e.stack); + } + }); + } + + allPredefinedResources(): any[] { + // Return in FIFO order to match previous SUSHI behavior + const options = { + scope: PREDEFINED_PACKAGE_NAME, + sort: [byLoadOrder(true)] + }; + return this.findResourceJSONs('*', options) ?? []; + } + fishForPredefinedResource(item: string, ...types: Type[]): any | undefined { - const resource = this.fishForFHIR(item, ...types); - if ( - resource && - this.allPredefinedResources(false).find( - predefResource => - predefResource.id === resource.id && - predefResource.resourceType === resource.resourceType && - predefResource.url === resource.url - ) - ) { - return resource; - } + return this.findResourceJSON(item, { + type: types, + scope: PREDEFINED_PACKAGE_NAME, + sort: DEFAULT_SORT + }); } fishForPredefinedResourceMetadata(item: string, ...types: Type[]): Metadata | undefined { - const resource = this.fishForPredefinedResource(item, ...types); - if (resource) { - return { - id: resource.id as string, - name: resource.name as string, - sdType: resource.type as string, - url: resource.url as string, - parent: resource.baseDefinition as string, - imposeProfiles: findImposeProfiles(resource), - abstract: resource.abstract as boolean, - version: resource.version as string, - resourceType: resource.resourceType as string - }; - } + const info = this.findResourceInfo(item, { + type: types, + scope: PREDEFINED_PACKAGE_NAME, + sort: DEFAULT_SORT + }); + return convertInfoToMetadata(info); } fishForFHIR(item: string, ...types: Type[]): any | undefined { - const def = super.fishForFHIR(item, ...types); + const def = this.findResourceJSON(item, { + type: types, + sort: DEFAULT_SORT + }); if (def) { return def; } @@ -125,37 +188,64 @@ export class FHIRDefinitions extends BaseFHIRDefinitions implements Fishable { } fishForMetadata(item: string, ...types: Type[]): Metadata | undefined { - const result = this.fishForFHIR(item, ...types); - if (result) { - let canBeTarget: boolean; - let canBind: boolean; - if (result.resourceType === 'StructureDefinition' && result.kind === 'logical') { - canBeTarget = - result.extension?.some((ext: any) => { - return ( - (ext?.url === TYPE_CHARACTERISTICS_EXTENSION && ext?.valueCode === 'can-be-target') || - (ext?.url === LOGICAL_TARGET_EXTENSION && ext?.valueBoolean === true) - ); - }) ?? false; - canBind = - result.extension?.some( - (ext: any) => - ext?.url === TYPE_CHARACTERISTICS_EXTENSION && ext?.valueCode === 'can-bind' - ) ?? false; - } - return { - id: result.id as string, - name: result.name as string, - sdType: result.type as string, - url: result.url as string, - parent: result.baseDefinition as string, - imposeProfiles: findImposeProfiles(result), - abstract: result.abstract as boolean, - version: result.version as string, - resourceType: result.resourceType as string, - canBeTarget, - canBind - }; + const info = this.findResourceInfo(item, { + type: types, + sort: DEFAULT_SORT + }); + if (info) { + return convertInfoToMetadata(info); + } + // If it's an "implied extension", try to materialize it. See:http://hl7.org/fhir/versions.html#extensions + if (IMPLIED_EXTENSION_REGEX.test(item) && types.some(t => t === Type.Extension)) { + return materializeImpliedExtensionMetadata(item, this); } } } + +export async function createFHIRDefinitions( + isSupplementalFHIRDefinitions = false, + supplementalFHIRDefinitionsFactory?: () => Promise, + // override is mainly intended to be used in unit tests + override?: { + packageDB?: PackageDB; + packageCache?: PackageCache; + registryClient?: RegistryClient; + currentBuildClient?: CurrentBuildClient; + options?: BasePackageLoaderOptions; + } +) { + const fhirDefinitions = new FHIRDefinitions( + isSupplementalFHIRDefinitions, + supplementalFHIRDefinitionsFactory, + override + ); + await fhirDefinitions.initialize(); + return fhirDefinitions; +} + +function convertInfoToMetadata(info: ResourceInfo): Metadata { + if (info) { + // Note: explicitly return undefined instead of null to keep tests happy + return { + id: info.id || undefined, + name: info.name || undefined, + sdType: info.sdType || undefined, + url: info.url || undefined, + parent: info.sdBaseDefinition || undefined, + imposeProfiles: info.sdImposeProfiles || undefined, + abstract: info.sdAbstract != null ? info.sdAbstract : undefined, + version: info.version || undefined, + resourceType: info.resourceType || undefined, + canBeTarget: logicalCharacteristic(info, 'can-be-target'), + canBind: logicalCharacteristic(info, 'can-bind'), + resourcePath: info.resourcePath || undefined + }; + } +} + +function logicalCharacteristic(info: ResourceInfo, characteristic: string) { + // return true or false for logicals, otherwise leave it undefined + if (info.sdKind === 'logical') { + return info.sdCharacteristics?.some(c => c === characteristic) ?? false; + } +} diff --git a/src/fhirdefs/impliedExtensions.ts b/src/fhirdefs/impliedExtensions.ts index 5ef4c30f1..7f8c08f92 100644 --- a/src/fhirdefs/impliedExtensions.ts +++ b/src/fhirdefs/impliedExtensions.ts @@ -26,8 +26,13 @@ // - Once the extension has been "fished", it's used like any other extension. import { union } from 'lodash'; -import { logger, Type } from '../utils'; +import { logger, Metadata, Type } from '../utils'; import { ElementDefinition, ElementDefinitionType, StructureDefinition } from '../fhirtypes'; +import { + TYPE_CHARACTERISTICS_EXTENSION, + LOGICAL_TARGET_EXTENSION, + findImposeProfiles +} from '../fhirtypes/common'; import { FHIRDefinitions } from '../fhirdefs'; export const IMPLIED_EXTENSION_REGEX = @@ -165,6 +170,43 @@ export function materializeImpliedExtension(url: string, defs: FHIRDefinitions): return ext.toJSON(true); } +export function materializeImpliedExtensionMetadata( + url: string, + defs: FHIRDefinitions +): Metadata | undefined { + const result = materializeImpliedExtension(url, defs); + if (result) { + let canBeTarget: boolean; + let canBind: boolean; + if (result.resourceType === 'StructureDefinition' && result.kind === 'logical') { + canBeTarget = + result.extension?.some((ext: any) => { + return ( + (ext?.url === TYPE_CHARACTERISTICS_EXTENSION && ext?.valueCode === 'can-be-target') || + (ext?.url === LOGICAL_TARGET_EXTENSION && ext?.valueBoolean === true) + ); + }) ?? false; + canBind = + result.extension?.some( + (ext: any) => ext?.url === TYPE_CHARACTERISTICS_EXTENSION && ext?.valueCode === 'can-bind' + ) ?? false; + } + return { + id: result.id as string, + name: result.name as string, + sdType: result.type as string, + url: result.url as string, + parent: result.baseDefinition as string, + imposeProfiles: findImposeProfiles(result), + abstract: result.abstract as boolean, + version: result.version as string, + resourceType: result.resourceType as string, + canBeTarget, + canBind + }; + } +} + /** * Determines if the ElementDefinition can be represented using an implied extension. According to * the FHIR documentation, elements with type "Resource" cannot be represented as implied diff --git a/src/fhirdefs/index.ts b/src/fhirdefs/index.ts index ec20eaa87..5ca4bb649 100644 --- a/src/fhirdefs/index.ts +++ b/src/fhirdefs/index.ts @@ -1,3 +1,2 @@ export * from './FHIRDefinitions'; -export * from './load'; export * from './R5DefsForR4'; diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts deleted file mode 100644 index 7c6282c41..000000000 --- a/src/fhirdefs/load.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { FHIRDefinitions } from './FHIRDefinitions'; -import { mergeDependency } from 'fhir-package-loader'; -import fs from 'fs-extra'; -import path from 'path'; -import junk from 'junk'; -import { logger, logMessage, getFilesRecursive } from '../utils'; -import { Fhir as FHIRConverter } from 'fhir/fhir'; -import { ImplementationGuideDefinitionParameter } from '../fhirtypes'; - -/** - * Loads custom resources defined in resourceDir into FHIRDefs - * @param {string} resourceDir - The path to the directory containing the resource subdirs - * @param {string} projectDir - User's specified project directory - * @param {ImplementationGuideDefinitionParameter[]} configParameters - optional, an array of config parameters in which to - * determine if there are additional resource paths for predefined resource - * @param {FHIRDefinitions} defs - The FHIRDefinitions object to load definitions into - */ -export function loadCustomResources( - resourceDir: string, - projectDir: string = null, - configParameters: ImplementationGuideDefinitionParameter[] = null, - defs: FHIRDefinitions -): void { - // Similar code for loading custom resources exists in IGExporter.ts addPredefinedResources() - const pathEnds = [ - 'capabilities', - 'extensions', - 'models', - 'operations', - 'profiles', - 'resources', - 'vocabulary', - 'examples' - ]; - const predefinedResourcePaths = pathEnds.map(pathEnd => path.join(resourceDir, pathEnd)); - if (configParameters && projectDir) { - const pathResources = configParameters - ?.filter(parameter => parameter.value && parameter.code === 'path-resource') - .map(parameter => parameter.value); - const pathResourceDirectories = pathResources - .map(directoryPath => path.join(projectDir, directoryPath)) - .filter(directoryPath => fs.existsSync(directoryPath)); - if (pathResourceDirectories) predefinedResourcePaths.push(...pathResourceDirectories); - } - const converter = new FHIRConverter(); - let invalidFileCount = 0; - for (const dirPath of predefinedResourcePaths) { - let foundSpreadsheets = false; - if (fs.existsSync(dirPath)) { - const files = getFilesRecursive(dirPath); - for (const file of files) { - let resourceJSON: any; - try { - if (junk.is(file)) { - // Ignore "junk" files created by the OS, like .DS_Store on macOS and Thumbs.db on Windows - continue; - } else if (file.endsWith('.json')) { - resourceJSON = fs.readJSONSync(file); - } else if (file.endsWith('-spreadsheet.xml')) { - foundSpreadsheets = true; - continue; - } else if (file.endsWith('xml')) { - const xml = fs.readFileSync(file).toString(); - if (/<\?mso-application progid="Excel\.Sheet"\?>/m.test(xml)) { - foundSpreadsheets = true; - continue; - } - resourceJSON = converter.xmlToObj(xml); - } else { - invalidFileCount++; - logger.debug(`File not processed by SUSHI: ${file}`); - continue; - } - } catch (e) { - if (e.message.startsWith('Unknown resource type:')) { - // Skip unknown FHIR resource types. When we have instances of Logical Models, - // the resourceType will not be recognized as a known FHIR resourceType, but that's okay. - continue; - } - logger.error(`Loading ${file} failed with the following error:\n${e.message}`); - if (e.stack) { - logger.debug(e.stack); - } - continue; - } - // All resources are added to the predefined map, so that this map can later be used to - // access predefined resources in the IG Exporter - defs.addPredefinedResource(file, resourceJSON); - if (path.basename(dirPath) !== 'examples') { - // add() will only add resources of resourceType: - // StructureDefinition, ValueSet, CodeSystem, or ImplementationGuide - defs.add(resourceJSON); - } - } - } - if (foundSpreadsheets) { - logger.info( - `Found spreadsheets in directory ${dirPath}. SUSHI does not support spreadsheets, so any resources in the spreadsheets will be ignored.` - ); - } - } - if (invalidFileCount > 0) { - logger.info( - invalidFileCount > 1 - ? `Found ${invalidFileCount} files in input/* resource folders that were neither XML nor JSON. These files were not processed as resources by SUSHI. To see the unprocessed files in the logs, run SUSHI with the "--log-level debug" flag.` - : `Found ${invalidFileCount} file in an input/* resource folder that was neither XML nor JSON. This file was not processed as a resource by SUSHI. To see the unprocessed file in the logs, run SUSHI with the "--log-level debug" flag.` - ); - } -} - -/** - * Loads a "supplemental" FHIR package other than the primary FHIR version being used. This is - * needed to support extensions for converting between versions (e.g., "implied" extensions). - * The definitions from the supplemental FHIR package are not loaded into the main set of - * definitions, but rather, are loaded into their own private FHIRDefinitions instance accessible - * within the primary FHIRDefinitions instance passed into this function. - * @param fhirPackage - the FHIR package to load in the format {packageId}#{version} - * @param defs - the FHIRDefinitions object to load the supplemental FHIR defs into - * @returns Promise promise that always resolves successfully (even if there is an error) - */ -export async function loadSupplementalFHIRPackage( - fhirPackage: string, - defs: FHIRDefinitions -): Promise { - const supplementalDefs = new FHIRDefinitions(true); - const [fhirPackageId, fhirPackageVersion] = fhirPackage.split('#'); - return mergeDependency(fhirPackageId, fhirPackageVersion, supplementalDefs, undefined, logMessage) - .then((def: FHIRDefinitions) => defs.addSupplementalFHIRDefinitions(fhirPackage, def)) - .catch((e: Error) => { - logger.error(`Failed to load supplemental FHIR package ${fhirPackage}: ${e.message}`); - if (e.stack) { - logger.debug(e.stack); - } - }); -} diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts index 3764f6deb..de4bde77a 100644 --- a/src/fhirtypes/ElementDefinition.ts +++ b/src/fhirtypes/ElementDefinition.ts @@ -146,6 +146,7 @@ export class ElementDefinitionType { } static fromJSON(json: any): ElementDefinitionType { + json = cloneDeep(json); const elDefType = new ElementDefinitionType(json.code); // TODO: other fromJSON methods check properties for undefined. @@ -616,10 +617,12 @@ export class ElementDefinition { const elementSD = fisher.fishForFHIR('Element', Type.Type); // The root element's constraint does not define the source property // because it is the source. So, we need to add the missing source property. - elementSD.snapshot.element[0].constraint.forEach((c: ElementDefinitionConstraint) => { - c.source = elementSD.url; - }); - this.constraint = elementSD.snapshot.element[0].constraint; + this.constraint = cloneDeep(elementSD.snapshot.element[0].constraint).map( + (c: ElementDefinitionConstraint) => { + c.source = elementSD.url; + return c; + } + ); // Capture the current state as the original element definition. // All changes after this will be a part of the differential. @@ -3047,6 +3050,7 @@ export class ElementDefinition { * @returns {ElementDefinition} the ElementDefinition representing the data passed in */ static fromJSON(json: LooseElementDefJSON, captureOriginal = true): ElementDefinition { + json = cloneDeep(json); const ed = new ElementDefinition(); for (let prop of PROPS_AND_UNDERPROPS) { if (prop.endsWith('[x]')) { @@ -3059,7 +3063,7 @@ export class ElementDefinition { ed.type = json[prop].map(type => ElementDefinitionType.fromJSON(type)); } else { // @ts-ignore - ed[prop] = cloneDeep(json[prop]); + ed[prop] = json[prop]; } } } diff --git a/src/fhirtypes/InstanceDefinition.ts b/src/fhirtypes/InstanceDefinition.ts index 97eb1e85c..37adb2717 100644 --- a/src/fhirtypes/InstanceDefinition.ts +++ b/src/fhirtypes/InstanceDefinition.ts @@ -1,5 +1,5 @@ import sanitize from 'sanitize-filename'; -import { difference } from 'lodash'; +import { cloneDeep, difference } from 'lodash'; import { orderedCloneDeep } from './common'; import { Meta } from './specialTypes'; import { HasId } from './mixins'; @@ -41,6 +41,7 @@ export class InstanceDefinition { } static fromJSON(json: { [key: string]: any }): InstanceDefinition { + json = cloneDeep(json); const instanceDefinition = new InstanceDefinition(); Object.keys(json).forEach(key => { instanceDefinition[key] = json[key]; diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts index 85deebe37..6ac7f450c 100644 --- a/src/fhirtypes/StructureDefinition.ts +++ b/src/fhirtypes/StructureDefinition.ts @@ -552,6 +552,7 @@ export class StructureDefinition { * @returns {StructureDefinition} a new StructureDefinition instance representing the passed in JSON */ static fromJSON(json: LooseStructDefJSON, captureOriginalElements = true): StructureDefinition { + // NOTE: Clone property assignment instead of cloning full JSON (to avoid cloning snapshot/differential twice) const sd = new StructureDefinition(); // First handle properties that are just straight translations from JSON for (const prop of PROPS_AND_UNDERPROPS) { @@ -565,6 +566,7 @@ export class StructureDefinition { sd.elements.length = 0; if (json.snapshot && json.snapshot.element) { for (const el of json.snapshot.element) { + // No need to clone since ElementDefinition.fromJSON will clone el const ed = ElementDefinition.fromJSON(el, captureOriginalElements); ed.structDef = sd; sd.elements.push(ed); diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts index d91f740b1..42f359b05 100644 --- a/src/fhirtypes/common.ts +++ b/src/fhirtypes/common.ts @@ -1010,26 +1010,28 @@ export function replaceReferences( } else if (value instanceof FshCode) { // the version on a CodeSystem resource is not the same as the system's actual version out in the world. // so, they don't need to match. - const baseSystem = value.system?.split('|')[0]; - const codeSystemMeta = fisher.fishForMetadata(baseSystem, Type.CodeSystem); - if (codeSystemMeta) { - clone = cloneDeep(rule); - const assignedCode = clone.value as FshCode; - assignedCode.system = value.system.replace(/^[^|]+/, codeSystemMeta.url); + if (value.system != null) { + const baseSystem = value.system.split('|')[0]; + const codeSystemMeta = fisher.fishForMetadata(baseSystem, Type.CodeSystem); + if (codeSystemMeta) { + clone = cloneDeep(rule); + const assignedCode = clone.value as FshCode; + assignedCode.system = value.system.replace(/^[^|]+/, codeSystemMeta.url); - // Find the code system using the returned metadata to avoid duplicate warnings if version mismatches - const matchedCanonical = codeSystemMeta.url - ? `${codeSystemMeta.url}${codeSystemMeta.version ? `|${codeSystemMeta.version}` : ''}` - : value.system; - const codeSystem = fishInTankBestVersion( - tank, - matchedCanonical, - rule.sourceInfo, - Type.CodeSystem - ); - if (codeSystem && (codeSystem instanceof FshCodeSystem || codeSystem instanceof Instance)) { - // if a local system was used, check to make sure the code is actually in that system - listUndefinedLocalCodes(codeSystem, [assignedCode.code], tank, rule); + // Find the code system using the returned metadata to avoid duplicate warnings if version mismatches + const matchedCanonical = codeSystemMeta.url + ? `${codeSystemMeta.url}${codeSystemMeta.version ? `|${codeSystemMeta.version}` : ''}` + : value.system; + const codeSystem = fishInTankBestVersion( + tank, + matchedCanonical, + rule.sourceInfo, + Type.CodeSystem + ); + if (codeSystem && (codeSystem instanceof FshCodeSystem || codeSystem instanceof Instance)) { + // if a local system was used, check to make sure the code is actually in that system + listUndefinedLocalCodes(codeSystem, [assignedCode.code], tank, rule); + } } } } diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts index aed50d878..6c9120970 100644 --- a/src/ig/IGExporter.ts +++ b/src/ig/IGExporter.ts @@ -13,6 +13,7 @@ import { readFileSync } from 'fs-extra'; import junk from 'junk'; +import { getPredefinedResourcePaths, PREDEFINED_PACKAGE_NAME } from './predefinedResources'; import { Package } from '../export'; import { ImplementationGuide, @@ -28,10 +29,11 @@ import { } from '../fhirtypes'; import { CONFORMANCE_AND_TERMINOLOGY_RESOURCES } from '../fhirtypes/common'; import { ConfigurationMenuItem, ConfigurationResource } from '../fshtypes'; -import { logger, Type, getFilesRecursive, stringOrElse, getFHIRVersionInfo } from '../utils'; +import { logger, Type, stringOrElse, getFHIRVersionInfo } from '../utils'; import { FHIRDefinitions } from '../fhirdefs'; import { Configuration } from '../fshtypes'; import { parseCodeLexeme } from '../import'; +import { byLoadOrder } from 'fhir-package-loader'; // Deprecated but still supported in IG Publisher, so we'll support it too. const DEPRECATED_RESOURCE_FORMAT_EXTENSION = @@ -222,7 +224,7 @@ export class IGExporter { d => !/^hl7\.fhir\.extensions\.r[2345]$/.test(d.packageId) ); if (dependencies?.length) { - const igs = this.fhirDefs.allImplementationGuides(); + const igs = this.fhirDefs.findResourceJSONs('*', { type: ['ImplementationGuide'] }); for (const dependency of dependencies) { const dependsEntry = this.fixDependsOn(dependency, igs); if (dependsEntry) { @@ -271,15 +273,16 @@ export class IGExporter { } if (dependsOn.version === 'latest') { + // TODO: This assumes only a single version of a package is in scope const dependencyIG = igs.find(ig => ig.packageId === dependsOn.packageId); if (dependencyIG?.version != null) { dependsOn.version = dependencyIG.version; } else { - const packageJSON = this.fhirDefs - .allPackageJsons() - .find(p => p.name === dependsOn.packageId); - if (packageJSON?.version != null) { - dependsOn.version = packageJSON.version; + const packageInfos = this.fhirDefs + .findPackageInfos(dependsOn.packageId) + .filter(info => info.version != null); + if (packageInfos.length) { + dependsOn.version = packageInfos[0].version; } } } @@ -296,8 +299,9 @@ export class IGExporter { dependsOn.uri = dependencyIG?.url; if (dependsOn.uri == null) { // there may be a package.json that can help us here - const dependencyPackageJson = this.fhirDefs.getPackageJson( - `${dependsOn.packageId}#${dependsOn.version}` + const dependencyPackageJson = this.fhirDefs.findPackageJSON( + dependsOn.packageId, + dependsOn.version ); dependsOn.uri = dependencyPackageJson?.canonical; } @@ -808,9 +812,6 @@ export class IGExporter { /** * Add each of the resources from the package to the ImplementationGuide JSON file. * Configuration may specify resources to omit. - * - * This function has similar operation to addPredefinedResources, and both should be - * analyzed when making changes to either. */ private addResources(): void { // NOTE: Custom resources are not included in the implementation guide @@ -946,214 +947,172 @@ export class IGExporter { this.ig.definition.resource.push(newResource); } - /** - * Adds any user provided resource files to the ImplementationGuide JSON file. - * This includes definitions in: - * capabilities, extensions, models, operations, profiles, resources, vocabulary, examples - * Based on: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root.input - * - * NOTE: This only includes files nested in subfolders when specified in the path-resource - * parameter, which is based on how the IG Publisher works. - * - * This function has similar operation to addResources, and both should be - * analyzed when making changes to either. - */ private addPredefinedResources(): void { - // Similar code for loading custom resources exists in load.ts loadCustomResources() - const pathEnds = [ - 'capabilities', - 'extensions', - 'models', - 'operations', - 'profiles', - 'resources', - 'vocabulary', - 'examples' - ]; - const predefinedResourcePaths = pathEnds.map(pathEnd => - path.join(this.inputPath, 'input', pathEnd) - ); - const pathResourceDirectories: string[] = []; - const pathResources = this.config.parameters - ?.filter(parameter => parameter.value && parameter.code === 'path-resource') - .map(parameter => parameter.value); - if (pathResources) { - pathResources.forEach(directoryPath => { - const fullPath = path.join(this.inputPath, ...directoryPath.split('/')); - if (existsSync(fullPath)) { - pathResourceDirectories.push(fullPath); - } else if (directoryPath.endsWith('/*') && existsSync(fullPath.slice(0, -2))) { - pathResourceDirectories.push( - ...readdirSync(fullPath.slice(0, -2), { withFileTypes: true, recursive: true }) - .filter(file => file.isDirectory()) - .map(dir => path.join(dir.path, dir.name)) - ); - } - }); - if (pathResourceDirectories) predefinedResourcePaths.push(...pathResourceDirectories); - } - const deeplyNestedFiles: string[] = []; const configuredBinaryResources = (this.config.resources ?? []).filter( resource => resource.reference?.reference?.startsWith('Binary/') && resource.extension?.some(e => IG_RESOURCE_FORMAT_EXTENSIONS.includes(e.url)) ); - for (const dirPath of predefinedResourcePaths) { - if (existsSync(dirPath)) { - const files = getFilesRecursive(dirPath); - for (const file of files) { + + const localResourcePaths = getPredefinedResourcePaths( + path.join(this.inputPath, 'input'), + this.inputPath, + this.config.parameters + ); + const predefinedResourceInfos = this.fhirDefs.findResourceInfos('*', { + scope: PREDEFINED_PACKAGE_NAME, + sort: [byLoadOrder(true)] // FIFO order to match previous SUSHI behavior + }); + const predefinedResources = this.fhirDefs.allPredefinedResources().map(r => cloneDeep(r)); + const deeplyNestedFiles: string[] = []; + for (let i = 0; i < predefinedResources.length; i++) { + // Since virtual resources start with 'virtual:{package}#{version}:', remove that part + const file = predefinedResourceInfos[i].resourcePath.replace(/^virtual:[^#]+#[^:]+:/, ''); + // If it's deeply nested, do not include it in the resource list + if (!localResourcePaths.includes(path.dirname(file))) { + deeplyNestedFiles.push(file); + continue; + } + const resourceJSON: InstanceDefinition = predefinedResources[i]; + if (resourceJSON) { + // For predefined examples of Logical Models, the user must provide an entry in config + // that specifies the reference as Binary/[id], the extension that specifies the resource format, + // and the exampleCanonical that references the LogicalModel the resource is an example of. + // Note: the exampleCanonical should reference the resourceType because the resourceType should + // be the absolute URL, but we previously supported using the logical model's id as the example's + // resourceType, so support having an exampleCanonical in either form for now. + // In that case, we do not want to add our own entry for the predefined resource - we just + // want to use the resource entry from the sushi-config.yaml + // For predefined examples of Logical Models that do not have a resourceType or id, + // a Binary resource reference based on the file name can be used, based on Zulip: + // https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/How.20do.20I.20get.20SUSHI.20to.20ignore.20a.20binary.20JSON.20logical.20instance.3F/near/407861211 + const configuredBinaryReference = configuredBinaryResources.find( + resource => + (resource.reference?.reference === `Binary/${resourceJSON.id}` && + (resource.exampleCanonical === + `${this.config.canonical}/StructureDefinition/${resourceJSON.resourceType}` || + resource.exampleCanonical === resourceJSON.resourceType)) || + resource.reference?.reference === `Binary/${path.parse(file).name}` + ); + + if (configuredBinaryReference) { if ( - path.dirname(file) !== dirPath && - !pathResourceDirectories?.includes(path.dirname(file)) + configuredBinaryReference.extension?.some( + ext => ext.url === DEPRECATED_RESOURCE_FORMAT_EXTENSION + ) ) { - if (!deeplyNestedFiles.includes(file)) { - deeplyNestedFiles.push(file); - } - continue; - } - const resourceJSON: InstanceDefinition = this.fhirDefs.getPredefinedResource(file); - if (resourceJSON) { - // For predefined examples of Logical Models, the user must provide an entry in config - // that specifies the reference as Binary/[id], the extension that specifies the resource format, - // and the exampleCanonical that references the LogicalModel the resource is an example of. - // Note: the exampleCanonical should reference the resourceType because the resourceType should - // be the absolute URL, but we previously supported using the logical model's id as the example's - // resourceType, so support having an exampleCanonical in either form for now. - // In that case, we do not want to add our own entry for the predefined resource - we just - // want to use the resource entry from the sushi-config.yaml - // For predefined examples of Logical Models that do not have a resourceType or id, - // a Binary resource reference based on the file name can be used, based on Zulip: - // https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/How.20do.20I.20get.20SUSHI.20to.20ignore.20a.20binary.20JSON.20logical.20instance.3F/near/407861211 - const configuredBinaryReference = configuredBinaryResources.find( - resource => - (resource.reference?.reference === `Binary/${resourceJSON.id}` && - (resource.exampleCanonical === - `${this.config.canonical}/StructureDefinition/${resourceJSON.resourceType}` || - resource.exampleCanonical === resourceJSON.resourceType)) || - resource.reference?.reference === `Binary/${path.parse(file).name}` + logger.warn( + `The extension ${DEPRECATED_RESOURCE_FORMAT_EXTENSION} has been deprecated. Update the configuration for ${configuredBinaryReference.reference?.reference ?? configuredBinaryReference.name} to use the current extension, ${CURRENT_RESOURCE_FORMAT_EXTENSION}.` ); + } + continue; + } - if (configuredBinaryReference) { - if ( - configuredBinaryReference.extension?.some( - ext => ext.url === DEPRECATED_RESOURCE_FORMAT_EXTENSION - ) - ) { - logger.warn( - `The extension ${DEPRECATED_RESOURCE_FORMAT_EXTENSION} has been deprecated. Update the configuration for ${configuredBinaryReference.reference?.reference ?? configuredBinaryReference.name} to use the current extension, ${CURRENT_RESOURCE_FORMAT_EXTENSION}.` - ); - } - continue; - } - - if (resourceJSON.resourceType == null || resourceJSON.id == null) { - logger.warn( - `Resource at ${file} is missing ${ - resourceJSON.resourceType == null ? 'resourceType' : '' - }${resourceJSON.resourceType == null && resourceJSON.id == null ? ' and ' : ''}${ - resourceJSON.id == null ? 'id' : '' - }.` - ); - continue; - } - - const referenceKey = `${resourceJSON.resourceType}/${resourceJSON.id}`; - const newResource: ImplementationGuideDefinitionResource = { - reference: { - reference: referenceKey - } - }; - const configResource = (this.config.resources ?? []).find( - resource => resource.reference?.reference == referenceKey - ); + if (resourceJSON.resourceType == null || resourceJSON.id == null) { + logger.warn( + `Resource at ${file} is missing ${ + resourceJSON.resourceType == null ? 'resourceType' : '' + }${resourceJSON.resourceType == null && resourceJSON.id == null ? ' and ' : ''}${ + resourceJSON.id == null ? 'id' : '' + }.` + ); + continue; + } - if (configResource?.omit !== true) { - const existingIndex = this.ig.definition.resource.findIndex( - r => r.reference.reference === referenceKey - ); - // If the user has provided a resource, it should override the generated resource. - // This can be helpful for working around cases where the generated resource has some incorrect values. - const existingResource = - existingIndex >= 0 ? this.ig.definition.resource[existingIndex] : null; - const existingIsExample = - existingResource?.exampleBoolean || existingResource?.exampleCanonical; - const existingName = existingIsExample ? existingResource.name : null; - const existingDescription = existingIsExample ? existingResource.description : null; - - const metaExtensionDescription = this.getMetaExtensionDescription(resourceJSON); - const metaExtensionName = this.getMetaExtensionName(resourceJSON); - // On some resources (Patient for example) title, name, and description can be objects, avoid using them when this is true - newResource.description = - configResource?.description ?? - metaExtensionDescription ?? - existingDescription ?? - stringOrElse(resourceJSON.description); - if (configResource?.fhirVersion) { - newResource.fhirVersion = configResource.fhirVersion; - } - if (configResource?.groupingId) { - newResource.groupingId = configResource.groupingId; - this.addGroup(newResource.groupingId); - } - if (path.basename(dirPath) === 'examples') { - newResource.name = - configResource?.name ?? - metaExtensionName ?? - existingName ?? - stringOrElse(resourceJSON.title) ?? - stringOrElse(resourceJSON.name) ?? - resourceJSON.id; - newResource._linkRef = resourceJSON.id; - // set exampleCanonical or exampleBoolean, preferring configured values - if (configResource?.exampleCanonical) { - newResource.exampleCanonical = configResource.exampleCanonical; - } else if (typeof configResource?.exampleBoolean === 'boolean') { - newResource.exampleBoolean = configResource.exampleBoolean; - } else { - const exampleUrl = resourceJSON.meta?.profile?.find(url => { - const [baseUrl, version] = url.split('|', 2); - const availableProfile = - this.pkg.fish(baseUrl, Type.Profile) ?? - this.fhirDefs.fishForFHIR(baseUrl, Type.Profile); - return ( - availableProfile != null && - (version == null || - version === (availableProfile.version ?? this.config.version)) - ); - }); - if (exampleUrl) { - newResource.exampleCanonical = exampleUrl.split('|', 1)[0]; - } else { - newResource.exampleBoolean = true; - } - } - } else { - if (configResource?.exampleCanonical) { - newResource.exampleCanonical = configResource.exampleCanonical; - } else if (typeof configResource?.exampleBoolean === 'boolean') { - newResource.exampleBoolean = configResource.exampleBoolean; - } else { - newResource.exampleBoolean = false; - } - newResource.name = - configResource?.name ?? - metaExtensionName ?? - existingResource?.name ?? - stringOrElse(resourceJSON.title) ?? - stringOrElse(resourceJSON.name) ?? - resourceJSON.id; - newResource._linkRef = stringOrElse(resourceJSON.name) ?? resourceJSON.id; - } - if (configResource?.extension?.length) { - newResource.extension = configResource.extension; - } + const referenceKey = `${resourceJSON.resourceType}/${resourceJSON.id}`; + const newResource: ImplementationGuideDefinitionResource = { + reference: { + reference: referenceKey + } + }; + const configResource = (this.config.resources ?? []).find( + resource => resource.reference?.reference == referenceKey + ); - if (existingIndex >= 0) { - this.ig.definition.resource[existingIndex] = newResource; + if (configResource?.omit !== true) { + const existingIndex = this.ig.definition.resource.findIndex( + r => r.reference.reference === referenceKey + ); + // If the user has provided a resource, it should override the generated resource. + // This can be helpful for working around cases where the generated resource has some incorrect values. + const existingResource = + existingIndex >= 0 ? this.ig.definition.resource[existingIndex] : null; + const existingIsExample = + existingResource?.exampleBoolean || existingResource?.exampleCanonical; + const existingName = existingIsExample ? existingResource.name : null; + const existingDescription = existingIsExample ? existingResource.description : null; + + const metaExtensionDescription = this.getMetaExtensionDescription(resourceJSON); + const metaExtensionName = this.getMetaExtensionName(resourceJSON); + // On some resources (Patient for example) title, name, and description can be objects, avoid using them when this is true + newResource.description = + configResource?.description ?? + metaExtensionDescription ?? + existingDescription ?? + stringOrElse(resourceJSON.description); + if (configResource?.fhirVersion) { + newResource.fhirVersion = configResource.fhirVersion; + } + if (configResource?.groupingId) { + newResource.groupingId = configResource.groupingId; + this.addGroup(newResource.groupingId); + } + if (path.basename(path.dirname(file)) === 'examples') { + newResource.name = + configResource?.name ?? + metaExtensionName ?? + existingName ?? + stringOrElse(resourceJSON.title) ?? + stringOrElse(resourceJSON.name) ?? + resourceJSON.id; + newResource._linkRef = resourceJSON.id; + // set exampleCanonical or exampleBoolean, preferring configured values + if (configResource?.exampleCanonical) { + newResource.exampleCanonical = configResource.exampleCanonical; + } else if (typeof configResource?.exampleBoolean === 'boolean') { + newResource.exampleBoolean = configResource.exampleBoolean; + } else { + const exampleUrl = resourceJSON.meta?.profile?.find(url => { + const [baseUrl, version] = url.split('|', 2); + const availableProfile = + this.pkg.fish(baseUrl, Type.Profile) ?? + this.fhirDefs.fishForFHIR(baseUrl, Type.Profile); + return ( + availableProfile != null && + (version == null || version === (availableProfile.version ?? this.config.version)) + ); + }); + if (exampleUrl) { + newResource.exampleCanonical = exampleUrl.split('|', 1)[0]; } else { - this.ig.definition.resource.push(newResource); + newResource.exampleBoolean = true; } } + } else { + if (configResource?.exampleCanonical) { + newResource.exampleCanonical = configResource.exampleCanonical; + } else if (typeof configResource?.exampleBoolean === 'boolean') { + newResource.exampleBoolean = configResource.exampleBoolean; + } else { + newResource.exampleBoolean = false; + } + newResource.name = + configResource?.name ?? + metaExtensionName ?? + existingResource?.name ?? + stringOrElse(resourceJSON.title) ?? + stringOrElse(resourceJSON.name) ?? + resourceJSON.id; + newResource._linkRef = stringOrElse(resourceJSON.name) ?? resourceJSON.id; + } + if (configResource?.extension?.length) { + newResource.extension = configResource.extension; + } + + if (existingIndex >= 0) { + this.ig.definition.resource[existingIndex] = newResource; + } else { + this.ig.definition.resource.push(newResource); } } } diff --git a/src/ig/index.ts b/src/ig/index.ts index d158c00fc..8bcd9efda 100644 --- a/src/ig/index.ts +++ b/src/ig/index.ts @@ -1 +1,2 @@ export * from './IGExporter'; +export * from './predefinedResources'; diff --git a/src/ig/predefinedResources.ts b/src/ig/predefinedResources.ts new file mode 100644 index 000000000..f40f9fc5b --- /dev/null +++ b/src/ig/predefinedResources.ts @@ -0,0 +1,90 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { DiskBasedVirtualPackage, LoadStatus } from 'fhir-package-loader'; +import { ImplementationGuideDefinitionParameter } from '../fhirtypes'; +import { FHIRDefinitions } from '../fhirdefs'; +import { logMessage } from '../utils'; + +export const PREDEFINED_PACKAGE_NAME = 'sushi-local'; +export const PREDEFINED_PACKAGE_VERSION = 'LOCAL'; + +/** + * Gets the local resource directory paths corresponding to the typical locations in an IG + * as well as those configured in the IG parameter path-resource. Only those directories + * that exist will be returned. + * @param {string} resourceDir - The path to the directory containing the resource subdirs + * @param {string} projectDir - User's specified project directory + * @param {ImplementationGuideDefinitionParameter[]} configParameters - optional, an array of + * config parameters in which to determine if there are additional resource paths for + * predefined resource + * @returns string[] list of paths to search for predefined resources + */ +export function getPredefinedResourcePaths( + resourceDir: string, + projectDir: string = null, + configParameters: ImplementationGuideDefinitionParameter[] = null +): string[] { + const pathEnds = [ + 'capabilities', + 'extensions', + 'models', + 'operations', + 'profiles', + 'resources', + 'vocabulary', + 'examples' + ]; + const predefinedResourcePaths = new Set( + pathEnds.map(pathEnd => path.join(resourceDir, pathEnd)).filter(p => fs.existsSync(p)) + ); + if (configParameters && projectDir) { + const pathResources = configParameters + .filter(parameter => parameter.value && parameter.code === 'path-resource') + .map(parameter => parameter.value); + pathResources.forEach(pathResource => { + const fullPath = path.join(projectDir, ...pathResource.replace(/\/\*$/, '').split('/')); + if (fs.existsSync(fullPath)) { + predefinedResourcePaths.add(fullPath); + // path-resource paths ending with /* should recursively include subfolders + if (pathResource.endsWith('/*')) { + fs.readdirSync(fullPath, { withFileTypes: true, recursive: true }) + .filter(file => file.isDirectory()) + .map(dir => path.join(dir.parentPath, dir.name)) + .forEach(p => predefinedResourcePaths.add(p)); + } + } + }); + } + return Array.from(predefinedResourcePaths); +} + +/** + * Loads predefined resources from the typical locations in an IG as well as those configured + * in the IG parameter path-resource. + * @param {string} resourceDir - The path to the directory containing the resource subdirs + * @param {string} projectDir - User's specified project directory + * @param {ImplementationGuideDefinitionParameter[]} configParameters - optional, an array of + * config parameters in which to determine if there are additional resource paths for + * predefined resource + * @returns Promise the load status ('LOADED' or 'FAILED') + */ +export async function loadPredefinedResources( + defs: FHIRDefinitions, + resourceDir: string, + projectDir: string = null, + configParameters: ImplementationGuideDefinitionParameter[] = null +): Promise { + const localResourcePaths = getPredefinedResourcePaths(resourceDir, projectDir, configParameters); + const status = await defs.loadVirtualPackage( + new DiskBasedVirtualPackage( + { name: PREDEFINED_PACKAGE_NAME, version: PREDEFINED_PACKAGE_VERSION }, + localResourcePaths, + { + log: logMessage, + allowNonResources: true, // support for logical instances + recursive: true + } + ) + ); + return status; +} diff --git a/src/run/FshToFhir.ts b/src/run/FshToFhir.ts index 15d9ee560..73e6c13d8 100644 --- a/src/run/FshToFhir.ts +++ b/src/run/FshToFhir.ts @@ -1,6 +1,6 @@ import { RawFSH } from '../import'; import { exportFHIR } from '../export'; -import { FHIRDefinitions } from '../fhirdefs'; +import { createFHIRDefinitions } from '../fhirdefs'; import { ImplementationGuideDependsOn } from '../fhirtypes'; import { fillTank, @@ -60,7 +60,7 @@ export async function fshToFhir( }; // load dependencies - const defs = new FHIRDefinitions(); + const defs = await createFHIRDefinitions(); await loadExternalDependencies(defs, config); // load FSH text into memory diff --git a/src/utils/FSHLogger.ts b/src/utils/FSHLogger.ts index 8c5531ee4..7953d8278 100644 --- a/src/utils/FSHLogger.ts +++ b/src/utils/FSHLogger.ts @@ -1,11 +1,19 @@ import { createLogger, format, transports } from 'winston'; +import { TransformableInfo } from 'logform'; import chalk from 'chalk'; import cloneDeep from 'lodash/cloneDeep'; import { TextLocation } from '../fshtypes/FshEntity'; const { combine, printf } = format; -const withLocation = format(info => { +interface LoggerInfo extends TransformableInfo { + file?: string; + location?: TextLocation; + appliedFile?: string; + appliedLocation?: TextLocation; +} + +const withLocation = format((info: LoggerInfo) => { if (info.file) { info.message += `\n File: ${info.file}`; delete info.file; @@ -31,18 +39,18 @@ const withLocation = format(info => { return info; }); -const ignoreWarnings = format(info => { +const ignoreWarnings = format((info: LoggerInfo) => { // Only warnings can be ignored if (info.level !== 'warn') { return info; } const shouldIgnore = ignoredWarnings?.some(m => { - return typeof m === 'string' ? m === info.message : m.test(info.message); + return typeof m === 'string' ? m === info.message : m.test(info.message as string); }); return shouldIgnore ? false : info; }); -const incrementCounts = format(info => { +const incrementCounts = format((info: LoggerInfo) => { switch (info.level) { case 'info': stats.numInfo++; @@ -63,19 +71,19 @@ const incrementCounts = format(info => { return info; }); -const trackErrorsAndWarnings = format(info => { +const trackErrorsAndWarnings = format((info: LoggerInfo) => { if (!errorsAndWarnings.shouldTrack) { return info; } if (info.level === 'error') { errorsAndWarnings.errors.push({ - message: info.message, + message: info.message as string, location: info.location, input: info.file }); } else if (info.level === 'warn') { errorsAndWarnings.warnings.push({ - message: info.message, + message: info.message as string, location: info.location, input: info.file }); @@ -83,7 +91,7 @@ const trackErrorsAndWarnings = format(info => { return info; }); -const printer = printf(info => { +const printer = printf((info: LoggerInfo) => { let level; switch (info.level) { case 'info': diff --git a/src/utils/Fishable.ts b/src/utils/Fishable.ts index ccbf19370..b2c1fa33d 100644 --- a/src/utils/Fishable.ts +++ b/src/utils/Fishable.ts @@ -16,7 +16,7 @@ export enum Type { export interface Metadata { id: string; - name: string; + name?: string; sdType?: string; resourceType?: string; url?: string; @@ -27,6 +27,7 @@ export interface Metadata { instanceUsage?: Instance['usage']; canBeTarget?: boolean; canBind?: boolean; + resourcePath?: string; } export interface Fishable { diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts index 4caa6d473..443641d2b 100644 --- a/src/utils/Processing.ts +++ b/src/utils/Processing.ts @@ -6,11 +6,11 @@ import YAML from 'yaml'; import { execSync } from 'child_process'; import { YAMLMap, Collection } from 'yaml/types'; import { isPlainObject, padEnd, startCase, sortBy, upperFirst } from 'lodash'; -import { mergeDependency, FHIRDefinitions as BaseFHIRDefinitions } from 'fhir-package-loader'; import { EOL } from 'os'; -import { AxiosResponse } from 'axios'; +import table from 'text-table'; +import { OptionValues } from 'commander'; import { logger, logMessage } from './FSHLogger'; -import { loadSupplementalFHIRPackage, FHIRDefinitions } from '../fhirdefs'; +import { FHIRDefinitions, R5_DEFINITIONS_NEEDED_IN_R4 } from '../fhirdefs'; import { FSHTank, RawFSH, @@ -24,8 +24,7 @@ import { Configuration } from '../fshtypes'; import { axiosGet } from './axiosUtils'; import { ImplementationGuideDependsOn } from '../fhirtypes'; import { FHIRVersionName, getFHIRVersionInfo } from '../utils/FHIRVersionUtils'; -import table from 'text-table'; -import { OptionValues } from 'commander'; +import { InMemoryVirtualPackage, RegistryClient } from 'fhir-package-loader'; const EXT_PKG_TO_FHIR_PKG_MAP: { [key: string]: string } = { 'hl7.fhir.extensions.r2': 'hl7.fhir.r2.core#1.0.2', @@ -34,13 +33,6 @@ const EXT_PKG_TO_FHIR_PKG_MAP: { [key: string]: string } = { 'hl7.fhir.extensions.r5': 'hl7.fhir.r5.core#5.0.0' }; -const CERTIFICATE_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'; - type AutomaticDependency = { packageId: string; version: string; @@ -244,7 +236,10 @@ export function updateConfig(config: Configuration, program: OptionValues): void } } -export async function updateExternalDependencies(config: Configuration): Promise { +export async function updateExternalDependencies( + config: Configuration, + registryClient: RegistryClient +): Promise { // only try to update if we got the config from sushi-config.yaml, and not from an IG const changedVersions: Map = new Map(); if (config.filePath == null) { @@ -258,39 +253,13 @@ export async function updateExternalDependencies(config: Configuration): Promise const promises = config.dependencies.map(async dep => { // current and dev have special meanings, so don't try to update those dependencies if (dep.version != 'current' && dep.version != 'dev') { - let res: AxiosResponse; - let latestVersion: string; - if (process.env.FPL_REGISTRY) { - try { - res = await axiosGet(`${process.env.FPL_REGISTRY}/${dep.packageId}`); - latestVersion = res?.data?.['dist-tags']?.latest; - } catch { - logger.warn( - `Could not get version info for package ${dep.packageId} from custom FHIR package registry ${process.env.FPL_REGISTRY}.` - ); - return; - } - } else { - try { - res = await axiosGet(`https://packages.fhir.org/${dep.packageId}`); - latestVersion = res?.data?.['dist-tags']?.latest; - } catch { - try { - res = await axiosGet(`https://packages2.fhir.org/packages/${dep.packageId}`); - latestVersion = res?.data?.['dist-tags']?.latest; - } catch { - logger.warn(`Could not get version info for package ${dep.packageId}`); - return; - } - } - } - - if (latestVersion) { + try { + const latestVersion = await registryClient.resolveVersion(dep.packageId, 'latest'); if (dep.version !== latestVersion) { dep.version = latestVersion; changedVersions.set(dep.packageId, dep.version); } - } else { + } catch { logger.warn(`Could not determine latest version for package ${dep.packageId}`); } } @@ -368,9 +337,27 @@ export async function loadExternalDependencies( export async function loadAutomaticDependencies( fhirVersion: string, configuredDependencies: ImplementationGuideDependsOn[], - defs: BaseFHIRDefinitions + defs: FHIRDefinitions ): Promise { const fhirVersionName = getFHIRVersionInfo(fhirVersion).name; + + if (fhirVersionName === 'R4' || fhirVersionName === 'R4B') { + // There are several R5 resources that are allowed for use in R4 and R4B. + // Add them first so they're always available. + const R5forR4Map = new Map(); + R5_DEFINITIONS_NEEDED_IN_R4.forEach(def => R5forR4Map.set(def.id, def)); + const virtualR5forR4Package = new InMemoryVirtualPackage( + { name: 'sushi-r5forR4', version: '1.0.0' }, + R5forR4Map, + { + log: (level: string, message: string) => { + logMessage(level, message); + } + } + ); + await defs.loadVirtualPackage(virtualR5forR4Package); + } + // Load dependencies serially so dependency loading order is predictable and repeatable for (const dep of AUTOMATIC_DEPENDENCIES) { // Skip dependencies not intended for this version of FHIR @@ -387,21 +374,29 @@ export async function loadAutomaticDependencies( return configRootId === packageRootId; }); if (!alreadyConfigured) { + let status: string; try { - await mergeDependency(dep.packageId, dep.version, defs, undefined, logMessage); + // Suppress error logs when loading automatic dependencies because many IGs can succeed without them + defs.setFHIRPackageLoaderLogInterceptor((level: string) => { + return level !== 'error'; + }); + status = await defs.loadPackage(dep.packageId, dep.version); } catch (e) { + // This shouldn't happen, but just in case + status = 'FAILED'; + if (e.stack) { + logger.debug(e.stack); + } + } finally { + // Unset the log interceptor so it behaves normally after this + defs.setFHIRPackageLoaderLogInterceptor(); + } + if (status !== 'LOADED') { let message = `Failed to load automatically-provided ${dep.packageId}#${dep.version}`; if (process.env.FPL_REGISTRY) { message += ` from custom FHIR package registry ${process.env.FPL_REGISTRY}.`; } - message += `: ${e.message}`; - if (/certificate/.test(e.message)) { - message += CERTIFICATE_MESSAGE; - } logger.warn(message); - if (e.stack) { - logger.debug(e.stack); - } } } } @@ -442,14 +437,10 @@ async function loadConfiguredDependencies( logger.info( `Loading supplemental version of FHIR to support extensions from ${dep.packageId}` ); - await loadSupplementalFHIRPackage(EXT_PKG_TO_FHIR_PKG_MAP[dep.packageId], defs); + await defs.loadSupplementalFHIRPackage(EXT_PKG_TO_FHIR_PKG_MAP[dep.packageId]); } else { - await mergeDependency(dep.packageId, dep.version, defs, undefined, logMessage).catch(e => { - let message = `Failed to load ${dep.packageId}#${dep.version}: ${e.message}`; - if (/certificate/.test(e.message)) { - message += CERTIFICATE_MESSAGE; - } - logger.error(message); + await defs.loadPackage(dep.packageId, dep.version).catch(e => { + logger.error(`Failed to load ${dep.packageId}#${dep.version}: ${e.message}`); if (e.stack) { logger.debug(e.stack); } @@ -543,7 +534,7 @@ export function writeFHIRResources( logger.info('Exporting FHIR resources as JSON...'); let count = 0; const skippedResources: string[] = []; - const predefinedResources = defs.allPredefinedResources(false); + const predefinedResources = defs.allPredefinedResources(); const writeResources = ( resources: { getFileName: () => string; diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts index dee93fa86..99fd9f56b 100644 --- a/test/export/CodeSystemExporter.test.ts +++ b/test/export/CodeSystemExporter.test.ts @@ -1,12 +1,10 @@ -import { loadFromPath } from 'fhir-package-loader'; -import path from 'path'; import { cloneDeep } from 'lodash'; import { CodeSystemExporter, Package } from '../../src/export'; import { FSHDocument, FSHTank } from '../../src/import'; import { FshCodeSystem, FshCode, RuleSet, Instance } from '../../src/fshtypes'; import { CaretValueRule, InsertRule, AssignmentRule, ConceptRule } from '../../src/fshtypes/rules'; import { FHIRDefinitions } from '../../src/fhirdefs'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { loggerSpy } from '../testhelpers'; import { minimalConfig } from '../utils/minimalConfig'; @@ -16,9 +14,8 @@ describe('CodeSystemExporter', () => { let pkg: Package; let exporter: CodeSystemExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { diff --git a/test/export/FHIRExporter.test.ts b/test/export/FHIRExporter.test.ts index 787434741..8e3249bc2 100644 --- a/test/export/FHIRExporter.test.ts +++ b/test/export/FHIRExporter.test.ts @@ -1,5 +1,3 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { exportFHIR, Package, FHIRExporter } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; @@ -11,12 +9,12 @@ import { CaretValueRule, ValueSetConceptComponentRule } from '../../src/fshtypes/rules'; -import { TestFisher, loggerSpy } from '../testhelpers'; +import { TestFisher, getTestFHIRDefinitions, loggerSpy, testDefsPath } from '../testhelpers'; describe('FHIRExporter', () => { - it('should output empty results with empty input', () => { + it('should output empty results with empty input', async () => { const input = new FSHTank([], minimalConfig); - const result = exportFHIR(input, new FHIRDefinitions()); + const result = exportFHIR(input, await getTestFHIRDefinitions()); expect(result).toEqual( new Package({ filePath: 'sushi-config.yaml', @@ -35,9 +33,8 @@ describe('FHIRExporter', () => { let doc: FSHDocument; let exporter: FHIRExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -65,7 +62,9 @@ describe('FHIRExporter', () => { expect(result.profiles[0].contained.length).toBe(1); const containedResource = result.profiles[0].contained[0]; expect(containedResource).toEqual( - defs.allValueSets().find(vs => vs.id === 'allergyintolerance-clinical') + defs + .findResourceJSONs('*', { type: ['ValueSet'] }) + .find(vs => vs.id === 'allergyintolerance-clinical') ); }); diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts index 0f8bb77a2..75de12134 100644 --- a/test/export/InstanceExporter.test.ts +++ b/test/export/InstanceExporter.test.ts @@ -1,4 +1,3 @@ -import { loadFromPath } from 'fhir-package-loader'; import { CodeSystemExporter, InstanceExporter, @@ -7,7 +6,6 @@ import { ValueSetExporter } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; -import { FHIRDefinitions } from '../../src/fhirdefs'; import { Instance, Profile, @@ -33,13 +31,18 @@ import { OnlyRule, PathRule } from '../../src/fshtypes/rules'; -import { loggerSpy, TestFisher } from '../testhelpers'; +import { + getTestFHIRDefinitions, + loggerSpy, + testDefsPath, + TestFHIRDefinitions, + TestFisher +} from '../testhelpers'; import { InstanceDefinition } from '../../src/fhirtypes'; -import path from 'path'; import { minimalConfig } from '../utils/minimalConfig'; describe('InstanceExporter', () => { - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let doc: FSHDocument; let tank: FSHTank; let pkg: Package; @@ -49,9 +52,8 @@ describe('InstanceExporter', () => { let exporter: InstanceExporter; let exportInstance: (instance: Instance) => InstanceDefinition; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -11674,22 +11676,21 @@ describe('InstanceExporter', () => { }); describe('InstanceExporter R5', () => { - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let doc: FSHDocument; let sdExporter: StructureDefinitionExporter; let exporter: InstanceExporter; let exportInstance: (instance: Instance) => InstanceDefinition; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); }); beforeEach(() => { doc = new FSHDocument('fileName'); const input = new FSHTank([doc], minimalConfig); const pkg = new Package(input.config); - const fisher = new TestFisher(input, defs, pkg, 'hl7.fhir.r5.core#5.0.0', 'r5-definitions'); + const fisher = new TestFisher(input, defs, pkg); sdExporter = new StructureDefinitionExporter(input, pkg, fisher); exporter = new InstanceExporter(input, pkg, fisher); exportInstance = (instance: Instance) => { diff --git a/test/export/MappingExporter.test.ts b/test/export/MappingExporter.test.ts index 73f2fd882..dba81aff8 100644 --- a/test/export/MappingExporter.test.ts +++ b/test/export/MappingExporter.test.ts @@ -1,16 +1,13 @@ -import path from 'path'; import { cloneDeep } from 'lodash'; -import { loadFromPath } from 'fhir-package-loader'; import { MappingExporter, StructureDefinitionExporter, Package } from '../../src/export'; import { FSHDocument, FSHTank } from '../../src/import'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { loggerSpy } from '../testhelpers'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { StructureDefinition } from '../../src/fhirtypes'; import { Mapping, Profile, RuleSet } from '../../src/fshtypes'; import { MappingRule, InsertRule, AssignmentRule } from '../../src/fshtypes/rules'; import { minimalConfig } from '../utils/minimalConfig'; -import { readFileSync } from 'fs'; describe('MappingExporter', () => { let defs: FHIRDefinitions; @@ -23,22 +20,12 @@ describe('MappingExporter', () => { let logical: StructureDefinition; let resource: StructureDefinition; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); - const extraProfile = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'StructureDefinition-NoMappingsProfile.json' - ), - 'utf-8' - ).trim() + beforeAll(async () => { + defs = await getTestFHIRDefinitions( + true, + testDefsPath('r4-definitions'), + testDefsPath('StructureDefinition-NoMappingsProfile.json') ); - defs.add(extraProfile); }); beforeEach(() => { diff --git a/test/export/StructureDefinition.ExtensionExporter.test.ts b/test/export/StructureDefinition.ExtensionExporter.test.ts index 624c0b131..442b2d53f 100644 --- a/test/export/StructureDefinition.ExtensionExporter.test.ts +++ b/test/export/StructureDefinition.ExtensionExporter.test.ts @@ -1,12 +1,9 @@ -import fs from 'fs-extra'; -import { loadFromPath } from 'fhir-package-loader'; import { StructureDefinitionExporter, Package } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { Extension, Instance, FshCode, Profile } from '../../src/fshtypes'; import { loggerSpy } from '../testhelpers/loggerSpy'; -import { TestFisher } from '../testhelpers'; -import path from 'path'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { minimalConfig } from '../utils/minimalConfig'; import { ContainsRule, AssignmentRule, CaretValueRule } from '../../src/fshtypes/rules'; @@ -16,13 +13,12 @@ describe('ExtensionExporter', () => { let pkg: Package; let exporter: StructureDefinitionExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); - const myComplexExtension = JSON.parse( - fs.readFileSync(path.join(__dirname, '../testhelpers/testdefs/mvc-extension.json'), 'utf-8') + beforeAll(async () => { + defs = await getTestFHIRDefinitions( + true, + testDefsPath('r4-definitions'), + testDefsPath('mvc-extension.json') ); - defs.add(myComplexExtension); }); beforeEach(() => { diff --git a/test/export/StructureDefinition.LogicalExporter.test.ts b/test/export/StructureDefinition.LogicalExporter.test.ts index c399f2d88..aa64cb986 100644 --- a/test/export/StructureDefinition.LogicalExporter.test.ts +++ b/test/export/StructureDefinition.LogicalExporter.test.ts @@ -1,11 +1,14 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { StructureDefinitionExporter, Package } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { Logical, Profile, FshValueSet, Invariant, FshCode } from '../../src/fshtypes'; import { loggerSpy } from '../testhelpers/loggerSpy'; -import { TestFisher } from '../testhelpers'; +import { + getTestFHIRDefinitions, + testDefsPath, + TestFHIRDefinitions, + TestFisher +} from '../testhelpers'; import { minimalConfig } from '../utils/minimalConfig'; import { AddElementRule, @@ -18,7 +21,6 @@ import { ObeysRule, OnlyRule } from '../../src/fshtypes/rules'; -import { readFileSync } from 'fs-extra'; describe('LogicalExporter', () => { let defs: FHIRDefinitions; @@ -26,9 +28,8 @@ describe('LogicalExporter', () => { let pkg: Package; let exporter: StructureDefinitionExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -929,27 +930,13 @@ describe('LogicalExporter', () => { }); describe('#with-type-characteristics-codes', () => { - let extraDefs: FHIRDefinitions; - - beforeAll(() => { - extraDefs = new FHIRDefinitions(); - const characteristicCS = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'CodeSystem-type-characteristics-code.json' - ), - 'utf-8' - ).trim() - ); - extraDefs.add(characteristicCS); - loadFromPath( - path.join(__dirname, '..', 'testhelpers', 'testdefs'), - 'r4-definitions', - extraDefs + let extraDefs: TestFHIRDefinitions; + + beforeAll(async () => { + extraDefs = await getTestFHIRDefinitions( + true, + testDefsPath('CodeSystem-type-characteristics-code.json'), + testDefsPath('r4-definitions') ); }); diff --git a/test/export/StructureDefinition.ProfileExporter.test.ts b/test/export/StructureDefinition.ProfileExporter.test.ts index e806b7f09..69f7ab5e7 100644 --- a/test/export/StructureDefinition.ProfileExporter.test.ts +++ b/test/export/StructureDefinition.ProfileExporter.test.ts @@ -1,11 +1,9 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { StructureDefinitionExporter, Package } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { Profile, Instance, FshCode } from '../../src/fshtypes'; import { loggerSpy } from '../testhelpers/loggerSpy'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { minimalConfig } from '../utils/minimalConfig'; import { BindingRule, @@ -21,9 +19,8 @@ describe('ProfileExporter', () => { let pkg: Package; let exporter: StructureDefinitionExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { diff --git a/test/export/StructureDefinition.ResourceExporter.test.ts b/test/export/StructureDefinition.ResourceExporter.test.ts index c1cb3286a..8018ee167 100644 --- a/test/export/StructureDefinition.ResourceExporter.test.ts +++ b/test/export/StructureDefinition.ResourceExporter.test.ts @@ -1,11 +1,9 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { StructureDefinitionExporter, Package } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { FshCode, FshValueSet, Invariant, Profile, Resource } from '../../src/fshtypes'; import { loggerSpy } from '../testhelpers/loggerSpy'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { minimalConfig } from '../utils/minimalConfig'; import { AddElementRule, @@ -26,9 +24,8 @@ describe('ResourceExporter', () => { let pkg: Package; let exporter: StructureDefinitionExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts index c3d714aea..bf357210f 100644 --- a/test/export/StructureDefinitionExporter.test.ts +++ b/test/export/StructureDefinitionExporter.test.ts @@ -1,4 +1,4 @@ -import { loadFromPath } from 'fhir-package-loader'; +import { InMemoryVirtualPackage } from 'fhir-package-loader'; import { StructureDefinitionExporter, Package } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; @@ -31,81 +31,46 @@ import { ConceptRule, AddElementRule } from '../../src/fshtypes/rules'; -import { assertCardRule, assertContainsRule, loggerSpy, TestFisher } from '../testhelpers'; +import { + assertCardRule, + assertContainsRule, + getTestFHIRDefinitions, + loggerSpy, + testDefsPath, + TestFHIRDefinitions, + TestFisher +} from '../testhelpers'; import { ElementDefinitionType, StructureDefinition, StructureDefinitionMapping } from '../../src/fhirtypes'; -import path from 'path'; import { cloneDeep } from 'lodash'; import { withDebugLogging } from '../testhelpers/withDebugLogging'; import { minimalConfig } from '../utils/minimalConfig'; import { ValidationError } from '../../src/errors'; -import { readFileSync } from 'fs-extra'; +import { + PREDEFINED_PACKAGE_NAME, + PREDEFINED_PACKAGE_VERSION +} from '../../src/ig/predefinedResources'; +import { logMessage } from '../../src/utils/FSHLogger'; describe('StructureDefinitionExporter R4', () => { - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let fisher: TestFisher; let doc: FSHDocument; let pkg: Package; let exporter: StructureDefinitionExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - const characteristicCS = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'CodeSystem-type-characteristics-code.json' - ), - 'utf-8' - ).trim() - ); - defs.add(characteristicCS); - const futurePlanet = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'StructureDefinition-FuturePlanet.json' - ), - 'utf-8' - ).trim() - ); - defs.add(futurePlanet); - const pastPlanet = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'StructureDefinition-PastPlanet.json' - ), - 'utf-8' - ).trim() + beforeAll(async () => { + defs = await getTestFHIRDefinitions( + true, + testDefsPath('r4-definitions'), + testDefsPath('CodeSystem-type-characteristics-code.json'), + testDefsPath('StructureDefinition-FuturePlanet.json'), + testDefsPath('StructureDefinition-PastPlanet.json'), + testDefsPath('StructureDefinition-elementdefinition-type-must-support.json') ); - defs.add(pastPlanet); - const typeMustSupport = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'StructureDefinition-elementdefinition-type-must-support.json' - ), - 'utf-8' - ).trim() - ); - defs.add(typeMustSupport); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); }); beforeEach(() => { @@ -888,7 +853,7 @@ describe('StructureDefinitionExporter R4', () => { }); it('should remove inherited top-level underscore-prefixed metadata properties for a profile', () => { - const jsonModifiedObservation = defs.fishForFHIR('Observation'); + const jsonModifiedObservation = cloneDeep(defs.fishForFHIR('Observation')); jsonModifiedObservation.id = 'ModifiedObservation'; jsonModifiedObservation.name = 'ModifiedObservation'; jsonModifiedObservation.url = 'http://example.org/sd/ModifiedObservation'; @@ -1350,7 +1315,9 @@ describe('StructureDefinitionExporter R4', () => { }); it('should remove inherited top-level underscore-prefixed metadata properties for an extension', () => { - const jsonModifiedPatientMothersMaidenName = defs.fishForFHIR('patient-mothersMaidenName'); + const jsonModifiedPatientMothersMaidenName = cloneDeep( + defs.fishForFHIR('patient-mothersMaidenName') + ); jsonModifiedPatientMothersMaidenName.id = 'ModifiedPatientMothersMaidenName'; jsonModifiedPatientMothersMaidenName.name = 'ModifiedPatientMothersMaidenName'; jsonModifiedPatientMothersMaidenName.url = @@ -1638,7 +1605,7 @@ describe('StructureDefinitionExporter R4', () => { }); it('should remove inherited top-level underscore-prefixed metadata properties for a logical model', () => { - const jsonModifiedAltID = defs.fishForFHIR('AlternateIdentification'); + const jsonModifiedAltID = cloneDeep(defs.fishForFHIR('AlternateIdentification')); jsonModifiedAltID.id = 'ModifiedAlternateIdentification'; jsonModifiedAltID.name = 'ModifiedAlternateIdentification'; jsonModifiedAltID.url = 'http://example.org/sd/ModifiedAlternateIdentification'; @@ -2008,7 +1975,7 @@ describe('StructureDefinitionExporter R4', () => { }); it('should remove inherited top-level underscore-prefixed metadata properties for a resource', () => { - const jsonModifiedResource = defs.fishForFHIR('Resource'); + const jsonModifiedResource = cloneDeep(defs.fishForFHIR('Resource')); jsonModifiedResource.id = 'ModifiedResource'; jsonModifiedResource.name = 'ModifiedResource'; jsonModifiedResource.url = 'http://example.org/sd/ModifiedResource'; @@ -7977,15 +7944,22 @@ describe('StructureDefinitionExporter R4', () => { ); }); - it('should not report an error for an extension Contains rule with an extension that is missing a snapshot when checking if its a modifierExtension', () => { + it('should not report an error for an extension Contains rule with an extension that is missing a snapshot when checking if its a modifierExtension', async () => { // Create an extension without a snapshot for testing // Note: SUSHI wouldn't create an extension like this, but it might be provided by a package or custom resource + const predefinedResourceMap = new Map(); const noSnapshotExtension = cloneDeep(defs.fishForFHIR('familymemberhistory-type')); noSnapshotExtension.id = 'familymemberhistory-type-no-snapshot'; noSnapshotExtension.url = 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-type-no-snapshot'; delete noSnapshotExtension.snapshot; - defs.add(noSnapshotExtension); + predefinedResourceMap.set('familymemberhistory-type-no-snapshot', noSnapshotExtension); + const predefinedPkg = new InMemoryVirtualPackage( + { name: PREDEFINED_PACKAGE_NAME, version: PREDEFINED_PACKAGE_VERSION }, + predefinedResourceMap, + { log: logMessage, allowNonResources: true } + ); + await defs.loadVirtualPackage(predefinedPkg); const profile = new Profile('Foo'); profile.parent = 'Observation'; @@ -10787,16 +10761,15 @@ describe('StructureDefinitionExporter R5', () => { let fisher: TestFisher; let exporter: StructureDefinitionExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); }); beforeEach(() => { doc = new FSHDocument('fileName'); const input = new FSHTank([doc], minimalConfig); pkg = new Package(input.config); - fisher = new TestFisher(input, defs, pkg, 'hl7.fhir.r5.core#5.0.0', 'r5-definitions'); + fisher = new TestFisher(input, defs, pkg); exporter = new StructureDefinitionExporter(input, pkg, fisher); loggerSpy.reset(); }); diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts index 8f640cb04..6e069058c 100644 --- a/test/export/ValueSetExporter.test.ts +++ b/test/export/ValueSetExporter.test.ts @@ -1,4 +1,3 @@ -import { loadFromPath } from 'fhir-package-loader'; import { ValueSetExporter, Package } from '../../src/export'; import { FSHDocument, FSHTank } from '../../src/import'; import { @@ -10,9 +9,8 @@ import { Instance } from '../../src/fshtypes'; import { loggerSpy } from '../testhelpers/loggerSpy'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { FHIRDefinitions } from '../../src/fhirdefs'; -import path from 'path'; import { cloneDeep } from 'lodash'; import { CaretValueRule, @@ -31,9 +29,8 @@ describe('ValueSetExporter', () => { let pkg: Package; let exporter: ValueSetExporter; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { diff --git a/test/fhirdefs/FHIRDefinitions.test.ts b/test/fhirdefs/FHIRDefinitions.test.ts index 208328523..50113d4c9 100644 --- a/test/fhirdefs/FHIRDefinitions.test.ts +++ b/test/fhirdefs/FHIRDefinitions.test.ts @@ -1,29 +1,43 @@ -import { loadFromPath } from 'fhir-package-loader'; -import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; -import path from 'path'; +import { FHIRDefinitions, createFHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { Type } from '../../src/utils/Fishable'; -import { loggerSpy } from '../testhelpers'; -import { cloneDeep } from 'lodash'; +import { + getLocalVirtualPackage, + getTestFHIRDefinitions, + loggerSpy, + testDefsPath, + TestFHIRDefinitions +} from '../testhelpers'; +import { R5_DEFINITIONS_NEEDED_IN_R4 } from '../../src/fhirdefs/R5DefsForR4'; +import { InMemoryVirtualPackage } from 'fhir-package-loader'; describe('FHIRDefinitions', () => { let defs: FHIRDefinitions; let r4bDefs: FHIRDefinitions; let r5Defs: FHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await createFHIRDefinitions(); + // Add the R5toR4 resources. This mirrors what happens in Processing.ts. + const R5forR4Map = new Map(); + R5_DEFINITIONS_NEEDED_IN_R4.forEach(def => R5forR4Map.set(def.id, def)); + const virtualR5forR4Package = new InMemoryVirtualPackage( + { name: 'sushi-r5forR4', version: '1.0.0' }, + R5forR4Map + ); + await defs.loadVirtualPackage(virtualR5forR4Package); + await defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r4-definitions'))); // Supplemental R3 defs needed to test fishing for implied extensions - const r3Defs = new FHIRDefinitions(true); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r3-definitions', r3Defs); + const r3Defs = await createFHIRDefinitions(true); + await r3Defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r3-definitions'))); defs.addSupplementalFHIRDefinitions('hl7.fhir.r3.core#3.0.2', r3Defs); - r4bDefs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4b-definitions', r4bDefs); - r5Defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', r5Defs); + r4bDefs = await createFHIRDefinitions(); + // Add the R5toR4 resources. This mirrors what happens in Processing.ts. + await r4bDefs.loadVirtualPackage(virtualR5forR4Package); + await r4bDefs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r4b-definitions'))); + r5Defs = await createFHIRDefinitions(); + await r5Defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r5-definitions'))); }); beforeEach(() => { - defs.resetPredefinedResources(); loggerSpy.reset(); }); @@ -494,7 +508,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/Condition', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-Condition.json')}` }); expect( defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Condition', Type.Resource) @@ -518,7 +533,8 @@ describe('FHIRDefinitions', () => { imposeProfiles: [ 'http://example.org/impose/StructureDefinition/named-patient', 'http://example.org/impose/StructureDefinition/gendered-patient' - ] + ], + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-named-and-gendered-patient.json')}` }); expect(defs.fishForMetadata('NamedAndGenderedPatient', Type.Profile)).toEqual( namedAndGenderedPatientByID @@ -543,7 +559,8 @@ describe('FHIRDefinitions', () => { parent: 'http://hl7.org/fhir/StructureDefinition/Element', resourceType: 'StructureDefinition', canBeTarget: false, - canBind: false + canBind: false, + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-eLTSSServiceModel.json')}` }); expect( defs.fishForMetadata( @@ -563,7 +580,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/boolean', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-boolean.json')}` }); expect( defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/boolean', Type.Type) @@ -580,7 +598,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/Address', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-Address.json')}` }); expect( defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Address', Type.Type) @@ -597,7 +616,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/vitalsigns', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Observation', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-vitalsigns.json')}` }); expect(defs.fishForMetadata('observation-vitalsigns', Type.Profile)).toEqual(vitalSignsByID); expect( @@ -618,7 +638,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Extension', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-patient-mothersMaidenName.json')}` }); expect(defs.fishForMetadata('mothersMaidenName', Type.Extension)).toEqual( maidenNameExtensionByID @@ -641,7 +662,8 @@ describe('FHIRDefinitions', () => { name: 'AllergyIntoleranceClinicalStatusCodes', url: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical', version: '4.0.1', - resourceType: 'ValueSet' + resourceType: 'ValueSet', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'ValueSet-allergyintolerance-clinical.json')}` }); expect(defs.fishForMetadata('AllergyIntoleranceClinicalStatusCodes', Type.ValueSet)).toEqual( allergyStatusValueSetByID @@ -664,7 +686,8 @@ describe('FHIRDefinitions', () => { name: 'AllergyIntoleranceClinicalStatusCodes', url: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical', version: '4.0.1', - resourceType: 'CodeSystem' + resourceType: 'CodeSystem', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'CodeSystem-allergyintolerance-clinical.json')}` }); expect( defs.fishForMetadata('AllergyIntoleranceClinicalStatusCodes', Type.CodeSystem) @@ -688,7 +711,8 @@ describe('FHIRDefinitions', () => { url: `http://hl7.org/fhir/StructureDefinition/${r}`, version: '5.0.0', parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:sushi-r5forR4#1.0.0:${r}` }); expect(defs.fishForMetadata(`http://hl7.org/fhir/StructureDefinition/${r}`)).toEqual( resourceById @@ -712,7 +736,8 @@ describe('FHIRDefinitions', () => { : r === 'CodeableReference' ? 'http://hl7.org/fhir/StructureDefinition/DataType' : 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:sushi-r5forR4#1.0.0:${r}` }); expect(defs.fishForMetadata(`http://hl7.org/fhir/StructureDefinition/${r}`)).toEqual( typeById @@ -731,7 +756,11 @@ describe('FHIRDefinitions', () => { url: `http://hl7.org/fhir/StructureDefinition/${r}`, version: r === 'SubscriptionTopic' ? '4.3.0' : '5.0.0', parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: + r === 'SubscriptionTopic' + ? `virtual:sushi-test-2#0.0.1:${testDefsPath('r4b-definitions', 'package', 'StructureDefinition-SubscriptionTopic.json')}` + : `virtual:sushi-r5forR4#1.0.0:${r}` }); expect(r4bDefs.fishForMetadata(`http://hl7.org/fhir/StructureDefinition/${r}`)).toEqual( resourceById @@ -750,7 +779,11 @@ describe('FHIRDefinitions', () => { url: `http://hl7.org/fhir/StructureDefinition/${r}`, version: r === 'CodeableReference' ? '4.3.0' : '5.0.0', parent: r === 'Base' ? undefined : 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: + r === 'CodeableReference' + ? `virtual:sushi-test-2#0.0.1:${testDefsPath('r4b-definitions', 'package', 'StructureDefinition-CodeableReference.json')}` + : `virtual:sushi-r5forR4#1.0.0:${r}` }); expect(r4bDefs.fishForMetadata(`http://hl7.org/fhir/StructureDefinition/${r}`)).toEqual( typeById @@ -769,7 +802,8 @@ describe('FHIRDefinitions', () => { url: `http://hl7.org/fhir/StructureDefinition/${r}`, version: '5.0.0', parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:sushi-test-3#0.0.1:${testDefsPath('r5-definitions', 'package', `StructureDefinition-${r}.json`)}` }); expect(r5Defs.fishForMetadata(`http://hl7.org/fhir/StructureDefinition/${r}`)).toEqual( resourceById @@ -793,7 +827,8 @@ describe('FHIRDefinitions', () => { : r === 'CodeableReference' ? 'http://hl7.org/fhir/StructureDefinition/DataType' : 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:sushi-test-3#0.0.1:${testDefsPath('r5-definitions', 'package', `StructureDefinition-${r}.json`)}` }); expect(r5Defs.fishForMetadata(`http://hl7.org/fhir/StructureDefinition/${r}`)).toEqual( typeById @@ -929,7 +964,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/Condition', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-Condition.json')}` }); expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Condition')).toEqual( conditionByID @@ -944,7 +980,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/boolean', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-boolean.json')}` }); expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/boolean')).toEqual( booleanByID @@ -959,7 +996,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/Address', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Element', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-Address.json')}` }); expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/Address')).toEqual( addressByID @@ -974,7 +1012,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/vitalsigns', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Observation', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-vitalsigns.json')}` }); expect(defs.fishForMetadata('observation-vitalsigns')).toEqual(vitalSignsProfileByID); expect(defs.fishForMetadata('http://hl7.org/fhir/StructureDefinition/vitalsigns')).toEqual( @@ -990,7 +1029,8 @@ describe('FHIRDefinitions', () => { url: 'http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/Extension', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-patient-mothersMaidenName.json')}` }); expect(defs.fishForMetadata('mothersMaidenName')).toEqual(maidenNameExtensionByID); expect( @@ -1005,7 +1045,8 @@ describe('FHIRDefinitions', () => { name: 'AllergyIntoleranceClinicalStatusCodes', url: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical', version: '4.0.1', - resourceType: 'ValueSet' + resourceType: 'ValueSet', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'ValueSet-allergyintolerance-clinical.json')}` }); expect(defs.fishForMetadata('AllergyIntoleranceClinicalStatusCodes')).toEqual( allergyStatusValueSetByID @@ -1020,7 +1061,8 @@ describe('FHIRDefinitions', () => { name: 'W3cProvenanceActivityType', url: 'http://hl7.org/fhir/w3c-provenance-activity-type', version: '4.0.1', - resourceType: 'CodeSystem' + resourceType: 'CodeSystem', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'CodeSystem-w3c-provenance-activity-type.json')}` }); expect(defs.fishForMetadata('W3cProvenanceActivityType')).toEqual( w3cProvenanceCodeSystemByID @@ -1040,7 +1082,8 @@ describe('FHIRDefinitions', () => { version: '0.1.0', resourceType: 'StructureDefinition', canBeTarget: false, - canBind: false + canBind: false, + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-eLTSSServiceModel.json')}` }); expect(defs.fishForMetadata('ELTSSServiceModel')).toEqual(eLTSSServiceModelByID); expect( @@ -1059,7 +1102,8 @@ describe('FHIRDefinitions', () => { parent: 'http://hl7.org/fhir/StructureDefinition/Base', resourceType: 'StructureDefinition', canBeTarget: false, - canBind: true // BindableLM has can-bind type-characteristics extension + canBind: true, // BindableLM has can-bind type-characteristics extension + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-BindableLM.json')}` }); expect( defs.fishForMetadata('http://example.org/StructureDefinition/BindableLM', Type.Logical) @@ -1067,162 +1111,73 @@ describe('FHIRDefinitions', () => { }); }); - describe('#fishForPredefinedResource', () => { - it('should not find resources that are not predefined', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - const predefinedCondition = defs.fishForPredefinedResource('Condition'); - expect(predefinedCondition).toBeUndefined(); - }); - - it('should not find resources that are predefined with different resourceTypes', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: 'foo', - id: condition.id, - url: condition.url - }); - const predefinedCondition = defs.fishForPredefinedResource('Condition'); - expect(predefinedCondition).toBeUndefined(); - }); - - it('should not find resources that are predefined with different ids', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: condition.resourceType, - id: 'foo', - url: condition.url - }); - const predefinedCondition = defs.fishForPredefinedResource('Condition'); - expect(predefinedCondition).toBeUndefined(); - }); - - it('should not find resources that are predefined with different urls', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: condition.resourceType, - id: condition.id, - url: 'foo' - }); - const predefinedCondition = defs.fishForPredefinedResource('Condition'); - expect(predefinedCondition).toBeUndefined(); - }); - - it('should find resources that are predefined', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: condition.resourceType, - id: condition.id, - url: condition.url - }); - const predefinedCondition = defs.fishForPredefinedResource('Condition'); - expect(predefinedCondition.id).toBe('Condition'); - }); - }); - - describe('#fishForPredefinedResourceMetadata', () => { - it('should not find resources that are not predefined', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - const predefinedCondition = defs.fishForPredefinedResourceMetadata('Condition'); - expect(predefinedCondition).toBeUndefined(); - }); + describe('#loadSupplementalFHIRPackage()', () => { + let testDefs: FHIRDefinitions; + let supplementalFHIRDefinitionsFactoryMock: jest.Mock; - it('should not find resources that are predefined with different resourceTypes', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: 'foo', - id: condition.id, - url: condition.url + beforeEach(async () => { + supplementalFHIRDefinitionsFactoryMock = jest.fn().mockImplementation(async () => { + // We don't want the supplemental loader making real network calls or accessing the FHIR cache + const testDefs = new TestFHIRDefinitions(true); + await testDefs.initialize(); + return testDefs; }); - const predefinedCondition = defs.fishForPredefinedResourceMetadata('Condition'); - expect(predefinedCondition).toBeUndefined(); + testDefs = await createFHIRDefinitions(false, supplementalFHIRDefinitionsFactoryMock); + loggerSpy.reset(); }); - it('should not find resources that are predefined with different ids', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: condition.resourceType, - id: 'foo', - url: condition.url - }); - const predefinedCondition = defs.fishForPredefinedResourceMetadata('Condition'); - expect(predefinedCondition).toBeUndefined(); - }); - - it('should not find resources that are predefined with different urls', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: condition.resourceType, - id: condition.id, - url: 'foo' - }); - const predefinedCondition = defs.fishForPredefinedResourceMetadata('Condition'); - expect(predefinedCondition).toBeUndefined(); + it('should load specified supplemental FHIR version', async () => { + await testDefs.loadSupplementalFHIRPackage('hl7.fhir.r3.core#3.0.2'); + expect(testDefs.supplementalFHIRPackages).toEqual(['hl7.fhir.r3.core#3.0.2']); + expect(testDefs.isSupplementalFHIRDefinitions).toBeFalsy(); + expect(loggerSpy.getAllLogs('error')).toHaveLength(0); }); - it('should find resources that are predefined', () => { - const condition = defs.fishForFHIR('Condition'); - expect(condition.id).toBe('Condition'); - defs.addPredefinedResource('', { - resourceType: condition.resourceType, - id: condition.id, - url: condition.url + it('should load multiple supplemental FHIR versions', async () => { + const promises = [ + 'hl7.fhir.r2.core#1.0.2', + 'hl7.fhir.r3.core#3.0.2', + 'hl7.fhir.r5.core#5.0.0' + ].map(version => { + return testDefs.loadSupplementalFHIRPackage(version); }); - const predefinedCondition = defs.fishForPredefinedResourceMetadata('Condition'); - expect(predefinedCondition.id).toBe('Condition'); + await Promise.all(promises); + expect(testDefs.supplementalFHIRPackages).toEqual([ + 'hl7.fhir.r2.core#1.0.2', + 'hl7.fhir.r3.core#3.0.2', + 'hl7.fhir.r5.core#5.0.0' + ]); + expect(defs.isSupplementalFHIRDefinitions).toBeFalsy(); + expect(loggerSpy.getAllLogs('error')).toHaveLength(0); }); - it('should find profiles with declared imposeProfiles', () => { - const namedAndGenderedPatient = cloneDeep(defs.fishForFHIR('NamedAndGenderedPatient')); - defs.addPredefinedResource('', cloneDeep(namedAndGenderedPatient)); - - const predefinedNamedAndGenderedPatient = - defs.fishForPredefinedResourceMetadata('NamedAndGenderedPatient'); - expect(predefinedNamedAndGenderedPatient).toEqual({ - abstract: false, - id: 'named-and-gendered-patient', - name: 'NamedAndGenderedPatient', - sdType: 'Patient', - url: 'http://example.org/impose/StructureDefinition/named-and-gendered-patient', - version: '0.1.0', - parent: 'http://hl7.org/fhir/StructureDefinition/Patient', - resourceType: 'StructureDefinition', - imposeProfiles: [ - 'http://example.org/impose/StructureDefinition/named-patient', - 'http://example.org/impose/StructureDefinition/gendered-patient' - ] + it('should log an error when it fails to load a FHIR version', async () => { + supplementalFHIRDefinitionsFactoryMock.mockReset().mockImplementation(async () => { + const supplementalDefs = new TestFHIRDefinitions(true); + await supplementalDefs.initialize(); + const loadSpy = jest.spyOn(supplementalDefs, 'loadPackage'); + loadSpy.mockRejectedValue(new Error()); + return supplementalDefs; }); - expect( - defs.fishForPredefinedResourceMetadata('NamedAndGenderedPatient', Type.Profile) - ).toEqual(predefinedNamedAndGenderedPatient); - expect( - defs.fishForPredefinedResourceMetadata( - 'http://example.org/impose/StructureDefinition/named-and-gendered-patient', - Type.Profile - ) - ).toEqual(predefinedNamedAndGenderedPatient); + await testDefs.loadSupplementalFHIRPackage('hl7.fhir.r999.core#999.9.9'); + expect(testDefs.supplementalFHIRPackages).toHaveLength(0); + expect(testDefs.isSupplementalFHIRDefinitions).toBeFalsy(); + expect(loggerSpy.getLastMessage('error')).toMatch( + /Failed to load supplemental FHIR package hl7\.fhir\.r999\.core#999.9.9/s + ); }); }); describe('#supplementalFHIRPackages', () => { - it('should list no supplemental FHIR packages when none have been loaded', () => { - const defs = new FHIRDefinitions(); + it('should list no supplemental FHIR packages when none have been loaded', async () => { + const defs = await getTestFHIRDefinitions(); expect(defs.supplementalFHIRPackages).toEqual([]); }); - it('should list loaded supplemental FHIR packages', () => { - const defs = new FHIRDefinitions(); - // normally the loader would maintain the package array, but since we're not using the loader, we need to populate it here - const r3 = new FHIRDefinitions(true); - const r5 = new FHIRDefinitions(true); + it('should loaded multiple supplemental FHIR packages', async () => { + const defs = await createFHIRDefinitions(); + const r3 = await createFHIRDefinitions(true); + const r5 = await createFHIRDefinitions(true); defs.addSupplementalFHIRDefinitions('hl7.fhir.r3.core#3.0.2', r3); defs.addSupplementalFHIRDefinitions('hl7.fhir.r5.core#5.0.0', r5); expect(defs.supplementalFHIRPackages).toEqual([ @@ -1231,16 +1186,4 @@ describe('FHIRDefinitions', () => { ]); }); }); - - describe('#allPackageJSONs', () => { - it('should return all package jsons', () => { - const testDefs = new FHIRDefinitions(); - testDefs.addPackageJson('sushi.test.1', { name: 'sushi.test.1', version: '0.0.1' }); - testDefs.addPackageJson('sushi.test.2', { name: 'sushi.test.2', version: '0.0.2' }); - expect(testDefs.allPackageJsons()).toEqual([ - { name: 'sushi.test.1', version: '0.0.1' }, - { name: 'sushi.test.2', version: '0.0.2' } - ]); - }); - }); }); diff --git a/test/fhirdefs/impliedExtension.test.ts b/test/fhirdefs/impliedExtension.test.ts index f9c387767..6dc63f5c4 100644 --- a/test/fhirdefs/impliedExtension.test.ts +++ b/test/fhirdefs/impliedExtension.test.ts @@ -1,11 +1,10 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; -import { FHIRDefinitions } from '../../src/fhirdefs'; +import { FHIRDefinitions, createFHIRDefinitions } from '../../src/fhirdefs'; import { isImpliedExtension, materializeImpliedExtension } from '../../src/fhirdefs/impliedExtensions'; import { loggerSpy } from '../testhelpers/loggerSpy'; +import { getLocalVirtualPackage, testDefsPath } from '../testhelpers'; describe('impliedExtensions', () => { describe('#isImpliedExtension()', () => { @@ -42,17 +41,17 @@ describe('impliedExtensions', () => { describe('#materializeImpliedExtension()', () => { let defs: FHIRDefinitions; - beforeEach(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); - const r2Defs = new FHIRDefinitions(true); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r2-definitions', r2Defs); + beforeEach(async () => { + defs = await createFHIRDefinitions(); + defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r4-definitions'))); + const r2Defs = await createFHIRDefinitions(true); + r2Defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r2-definitions'))); defs.addSupplementalFHIRDefinitions('hl7.fhir.r2.core#1.0.2', r2Defs); - const r3Defs = new FHIRDefinitions(true); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r3-definitions', r3Defs); + const r3Defs = await createFHIRDefinitions(true); + r3Defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r3-definitions'))); defs.addSupplementalFHIRDefinitions('hl7.fhir.r3.core#3.0.2', r3Defs); - const r5Defs = new FHIRDefinitions(true); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', r5Defs); + const r5Defs = await createFHIRDefinitions(true); + r5Defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r5-definitions'))); defs.addSupplementalFHIRDefinitions('hl7.fhir.r5.core#5.0.0', r5Defs); loggerSpy.reset(); }); @@ -1405,11 +1404,11 @@ describe('impliedExtensions', () => { describe('#materializeImpliedExtensionFromR5Project()', () => { let defs: FHIRDefinitions; - beforeEach(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); - const r4Defs = new FHIRDefinitions(true); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', r4Defs); + beforeEach(async () => { + defs = await createFHIRDefinitions(); + defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r5-definitions'))); + const r4Defs = await createFHIRDefinitions(true); + r4Defs.loadVirtualPackage(getLocalVirtualPackage(testDefsPath('r4-definitions'))); defs.addSupplementalFHIRDefinitions('hl7.fhir.r4.core#4.0.1', r4Defs); loggerSpy.reset(); }); diff --git a/test/fhirdefs/load.test.ts b/test/fhirdefs/load.test.ts deleted file mode 100644 index 360a8bd75..000000000 --- a/test/fhirdefs/load.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { PackageLoadError } from 'fhir-package-loader'; -import { loadCustomResources, loadSupplementalFHIRPackage } from '../../src/fhirdefs/load'; -import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; -import { ImplementationGuideDefinitionParameter } from '../../src/fhirtypes'; -import { loggerSpy } from '../testhelpers'; -import path from 'path'; - -jest.mock('fhir-package-loader', () => { - const original = jest.requireActual('fhir-package-loader'); - return { - ...original, - mergeDependency: jest.fn( - async (packageName: string, version: string, FHIRDefs: FHIRDefinitions) => { - // the mock loader can find R2, R3, and R5 - if (/hl7\.fhir\.r(2|3|5).core/.test(packageName)) { - return Promise.resolve(FHIRDefs); - } else { - throw new PackageLoadError(`${packageName}#${version}`); - } - } - ) - }; -}); - -describe('#loadCustomResources', () => { - let defs: FHIRDefinitions; - let pathToInput: string; - beforeAll(() => { - loggerSpy.reset(); - defs = new FHIRDefinitions(); - pathToInput = path.join(__dirname, 'fixtures', 'customized-ig-with-resources', 'input'); - const configParamater: ImplementationGuideDefinitionParameter = { - code: 'path-resource', - value: 'path-resource-test' - }; - loadCustomResources(pathToInput, pathToInput, [configParamater], defs); - }); - - it('should load custom JSON and XML resources', () => { - // Only StructureDefinitions, ValueSets, and CodeSystems are loaded in - const profiles = defs.allProfiles(); - const valueSets = defs.allValueSets(); - const extensions = defs.allExtensions(); - expect(profiles).toHaveLength(3); - expect(profiles[0].id).toBe('MyPatient'); - expect(profiles[2].id).toBe('MyObservation'); - expect(valueSets).toHaveLength(1); - expect(valueSets[0].id).toBe('MyVS'); - expect(extensions).toHaveLength(2); - const birthPlace = extensions[0]; - expect(birthPlace.id).toBe('patient-birthPlace'); - const birthPlaceFromXML = extensions[1]; - expect(birthPlaceFromXML.id).toBe('patient-birthPlaceXML'); - // The extension converted from xml should match the native json extension - // except for the identifying fields - Object.keys(birthPlaceFromXML).forEach(key => { - let expectedValue = birthPlace[key]; - if (key === 'id' || key === 'url' || key === 'name') { - expectedValue += 'XML'; - } - expect(birthPlaceFromXML[key]).toEqual(expectedValue); - }); - }); - - it('should add all predefined resources to the FHIRDefs with file information', () => { - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'capabilities', 'CapabilityStatement-MyCS.json') - ).id - ).toBe('MyCS'); - expect( - defs.getPredefinedResource(path.join(pathToInput, 'examples', 'Patient-MyPatient.json')).id - ).toBe('MyPatient'); - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'path-resource-test', 'StructureDefinition-MyObservation.json') - ).id - ).toBe('MyObservation'); - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'examples', 'Binary-LogicalModelExample.json') - ).id - ).toBe('example-logical-model'); - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'extensions', 'StructureDefinition-patient-birthPlace.json') - ).id - ).toBe('patient-birthPlace'); - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'extensions', 'StructureDefinition-patient-birthPlaceXML.xml') - ).id - ).toBe('patient-birthPlaceXML'); - expect( - defs.getPredefinedResource(path.join(pathToInput, 'models', 'StructureDefinition-MyLM.json')) - .id - ).toBe('MyLM'); - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'operations', 'OperationDefinition-MyOD.json') - ).id - ).toBe('MyOD'); - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'profiles', 'StructureDefinition-MyPatient.json') - ).id - ).toBe('MyPatient'); - expect( - defs.getPredefinedResource(path.join(pathToInput, 'resources', 'Patient-BazPatient.json')).id - ).toBe('BazPatient'); - // NOTE: It loads nested predefined resources even thought the IG Publisher doesn't handle them well - expect( - defs.getPredefinedResource( - path.join(pathToInput, 'resources', 'nested', 'StructureDefinition-MyNestedPatient.json') - ).id - ).toBe('MyNestedPatient'); - - expect( - defs.getPredefinedResource(path.join(pathToInput, 'vocabulary', 'ValueSet-MyVS.json')).id - ).toBe('MyVS'); - }); - - it('should log an info message for non JSON or XML input files', () => { - expect(loggerSpy.getLastMessage('info')).toMatch( - /Found 1 file in an input\/\* resource folder that was neither XML nor JSON/ - ); - }); - - it('should log an error for invalid XML files', () => { - expect(loggerSpy.getLastMessage('error')).toMatch( - /Loading .*InvalidFile.xml failed with the following error:/ - ); - }); - - it('should not log an error for spreadsheet XML files following standard naming convention', () => { - loggerSpy.getAllMessages('error').forEach(m => { - expect(m).not.toMatch(/Loading resources-spreadsheet.xml failed with the following error:/); - }); - }); - - it('should not log an error for spreadsheet XML files NOT following standard naming convention', () => { - loggerSpy.getAllMessages('error').forEach(m => { - expect(m).not.toMatch( - /Loading sneaky-spread-like-bread-sheet.xml failed with the following error:/ - ); - }); - }); - - it('should not log an error for invalid FHIR types parsed from XML', () => { - loggerSpy.getAllMessages('error').forEach(m => { - expect(m).not.toMatch(/Unknown resource type:/); - }); - }); - - it('should log an info message when it finds spreadsheets', () => { - expect(loggerSpy.getFirstMessage('info')).toMatch(/Found spreadsheets in directory/); - }); - - it('should log an error for invalid JSON files', () => { - expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch( - /Loading .*InvalidFile.json failed with the following error:/ - ); - }); -}); - -describe('#loadSupplementalFHIRPackage()', () => { - beforeEach(() => { - loggerSpy.reset(); - }); - - it('should load specified supplemental FHIR version', () => { - const defs = new FHIRDefinitions(); - return loadSupplementalFHIRPackage('hl7.fhir.r3.core#3.0.2', defs).then(() => { - expect(defs.supplementalFHIRPackages).toEqual(['hl7.fhir.r3.core#3.0.2']); - expect(defs.isSupplementalFHIRDefinitions).toBeFalsy(); - expect(loggerSpy.getAllLogs('error')).toHaveLength(0); - }); - }); - - it('should load multiple supplemental FHIR versions', () => { - const defs = new FHIRDefinitions(); - const promises = [ - 'hl7.fhir.r2.core#1.0.2', - 'hl7.fhir.r3.core#3.0.2', - 'hl7.fhir.r5.core#5.0.0' - ].map(version => { - return loadSupplementalFHIRPackage(version, defs); - }); - return Promise.all(promises).then(() => { - expect(defs.supplementalFHIRPackages).toEqual([ - 'hl7.fhir.r2.core#1.0.2', - 'hl7.fhir.r3.core#3.0.2', - 'hl7.fhir.r5.core#5.0.0' - ]); - expect(defs.isSupplementalFHIRDefinitions).toBeFalsy(); - expect(loggerSpy.getAllLogs('error')).toHaveLength(0); - }); - }); - - it('should log an error when it fails to load a FHIR version', () => { - const defs = new FHIRDefinitions(); - // Although the real one should support R4, the mock loader does not - return loadSupplementalFHIRPackage('hl7.fhir.r4.core#4.0.1', defs).then(() => { - expect(defs.supplementalFHIRPackages.length).toBe(0); - expect(defs.isSupplementalFHIRDefinitions).toBeFalsy(); - expect(loggerSpy.getLastMessage('error')).toMatch( - /Failed to load supplemental FHIR package hl7\.fhir\.r4\.core#4.0.1/s - ); - }); - }); -}); diff --git a/test/fhirtypes/ElementDefinition.applyAddElementRule.test.ts b/test/fhirtypes/ElementDefinition.applyAddElementRule.test.ts index 482cc4e4c..3bb5812e5 100644 --- a/test/fhirtypes/ElementDefinition.applyAddElementRule.test.ts +++ b/test/fhirtypes/ElementDefinition.applyAddElementRule.test.ts @@ -1,8 +1,6 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { ElementDefinition, ElementDefinitionType, StructureDefinition } from '../../src/fhirtypes'; -import { loggerSpy, TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, loggerSpy, testDefsPath, TestFisher } from '../testhelpers'; import { AddElementRule } from '../../src/fshtypes/rules'; describe('ElementDefinition', () => { @@ -10,9 +8,8 @@ describe('ElementDefinition', () => { let alternateIdentification: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); diff --git a/test/fhirtypes/ElementDefinition.applyFlags.test.ts b/test/fhirtypes/ElementDefinition.applyFlags.test.ts index 027e051ad..934c610c7 100644 --- a/test/fhirtypes/ElementDefinition.applyFlags.test.ts +++ b/test/fhirtypes/ElementDefinition.applyFlags.test.ts @@ -1,18 +1,15 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { InvalidMustSupportError, MultipleStandardsStatusError } from '../../src/errors'; -import { TestFisher } from '../testhelpers'; -import path from 'path'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; let observation: StructureDefinition; let obsResource: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.applyMapping.test.ts b/test/fhirtypes/ElementDefinition.applyMapping.test.ts index 174c2946c..331f67863 100644 --- a/test/fhirtypes/ElementDefinition.applyMapping.test.ts +++ b/test/fhirtypes/ElementDefinition.applyMapping.test.ts @@ -1,17 +1,14 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher } from '../testhelpers'; -import path from 'path'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { FshCode } from '../../src/fshtypes'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; let observation: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignBoolean.test.ts b/test/fhirtypes/ElementDefinition.assignBoolean.test.ts index e1dcc01ff..45fe6ecd0 100644 --- a/test/fhirtypes/ElementDefinition.assignBoolean.test.ts +++ b/test/fhirtypes/ElementDefinition.assignBoolean.test.ts @@ -1,9 +1,7 @@ -import path from 'path'; import { omit } from 'lodash'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; @@ -11,9 +9,8 @@ describe('ElementDefinition', () => { let location: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignFshCode.test.ts b/test/fhirtypes/ElementDefinition.assignFshCode.test.ts index fd62fa775..7b1350e7f 100644 --- a/test/fhirtypes/ElementDefinition.assignFshCode.test.ts +++ b/test/fhirtypes/ElementDefinition.assignFshCode.test.ts @@ -1,7 +1,5 @@ -import path from 'path'; import omit from 'lodash/omit'; -import { loadFromPath } from 'fhir-package-loader'; -import { TestFisher, loggerSpy } from '../testhelpers'; +import { TestFisher, getTestFHIRDefinitions, loggerSpy, testDefsPath } from '../testhelpers'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { FshCode } from '../../src/fshtypes/FshCode'; @@ -16,9 +14,8 @@ describe('ElementDefinition', () => { let versionedCode: FshCode; let codeWithDisplay: FshCode; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { @@ -905,13 +902,8 @@ describe('ElementDefinition', () => { let r5Fisher: TestFisher; let carePlan: StructureDefinition; - beforeAll(() => { - r5Defs = new FHIRDefinitions(); - loadFromPath( - path.join(__dirname, '..', 'testhelpers', 'testdefs'), - 'r5-definitions', - r5Defs - ); + beforeAll(async () => { + r5Defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); r5Fisher = new TestFisher().withFHIR(r5Defs); }); diff --git a/test/fhirtypes/ElementDefinition.assignFshQuantity.test.ts b/test/fhirtypes/ElementDefinition.assignFshQuantity.test.ts index 933c02ccd..56530e7d5 100644 --- a/test/fhirtypes/ElementDefinition.assignFshQuantity.test.ts +++ b/test/fhirtypes/ElementDefinition.assignFshQuantity.test.ts @@ -1,10 +1,8 @@ -import path from 'path'; import { omit } from 'lodash'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { FshQuantity, FshCode } from '../../src/fshtypes'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { ElementDefinitionType } from '../../src/fhirtypes'; describe('ElementDefinition', () => { @@ -16,9 +14,8 @@ describe('ElementDefinition', () => { let fshQuantityAge: FshQuantity; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignFshRatio.test.ts b/test/fhirtypes/ElementDefinition.assignFshRatio.test.ts index 97fbcad81..a8614b4f1 100644 --- a/test/fhirtypes/ElementDefinition.assignFshRatio.test.ts +++ b/test/fhirtypes/ElementDefinition.assignFshRatio.test.ts @@ -1,9 +1,7 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { FshQuantity, FshCode, FshRatio } from '../../src/fshtypes'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; @@ -13,9 +11,8 @@ describe('ElementDefinition', () => { let fshRatioNoUnits: FshRatio; let differentFshRatio: FshRatio; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignFshReference.test.ts b/test/fhirtypes/ElementDefinition.assignFshReference.test.ts index bb105ab4d..098e35144 100644 --- a/test/fhirtypes/ElementDefinition.assignFshReference.test.ts +++ b/test/fhirtypes/ElementDefinition.assignFshReference.test.ts @@ -1,9 +1,7 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { FshReference, Instance } from '../../src/fshtypes'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { FSHDocument } from '../../src/import'; import { minimalConfig } from '../utils/minimalConfig'; import { FSHTank } from '../../src/import'; @@ -17,9 +15,8 @@ describe('ElementDefinition', () => { let fshReference2: FshReference; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { @@ -195,23 +192,14 @@ describe('ElementDefinition', () => { }); describe('R5 CodeableReference', () => { - let r5Defs: FHIRDefinitions; let doc: FSHDocument; let r5Fisher: TestFisher; let carePlan: StructureDefinition; - beforeAll(() => { - r5Defs = new FHIRDefinitions(); - loadFromPath( - path.join(__dirname, '..', 'testhelpers', 'testdefs'), - 'r5-definitions', - r5Defs - ); - }); - - beforeEach(() => { + beforeEach(async () => { doc = new FSHDocument('Conditions.fsh'); const input = new FSHTank([doc], minimalConfig); + const r5Defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); const pkg = new Package(input.config); r5Fisher = new TestFisher(input, r5Defs, pkg); carePlan = r5Fisher.fishForStructureDefinition('CarePlan'); diff --git a/test/fhirtypes/ElementDefinition.assignInstanceDefinition.test.ts b/test/fhirtypes/ElementDefinition.assignInstanceDefinition.test.ts index 23225c7fe..54ef5f62d 100644 --- a/test/fhirtypes/ElementDefinition.assignInstanceDefinition.test.ts +++ b/test/fhirtypes/ElementDefinition.assignInstanceDefinition.test.ts @@ -1,8 +1,6 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition, InstanceDefinition } from '../../src/fhirtypes'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; @@ -10,9 +8,8 @@ describe('ElementDefinition', () => { let observation: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignNumber.test.ts b/test/fhirtypes/ElementDefinition.assignNumber.test.ts index 92e555246..08b4fbd61 100644 --- a/test/fhirtypes/ElementDefinition.assignNumber.test.ts +++ b/test/fhirtypes/ElementDefinition.assignNumber.test.ts @@ -1,10 +1,9 @@ import path from 'path'; import fs from 'fs-extra'; import { omit } from 'lodash'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { ElementDefinition } from '../../src/fhirtypes'; describe('ElementDefinition', () => { @@ -15,9 +14,8 @@ describe('ElementDefinition', () => { let appointment: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignString.test.ts b/test/fhirtypes/ElementDefinition.assignString.test.ts index 003a4d6da..ae1c93f5f 100644 --- a/test/fhirtypes/ElementDefinition.assignString.test.ts +++ b/test/fhirtypes/ElementDefinition.assignString.test.ts @@ -1,10 +1,9 @@ import path from 'path'; import fs from 'fs-extra'; import { omit } from 'lodash'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition, ElementDefinition } from '../../src/fhirtypes'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { loggerSpy } from '../testhelpers/loggerSpy'; describe('ElementDefinition', () => { @@ -21,9 +20,8 @@ describe('ElementDefinition', () => { let binary: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.assignValue.test.ts b/test/fhirtypes/ElementDefinition.assignValue.test.ts index 103bb1fc4..20eaca643 100644 --- a/test/fhirtypes/ElementDefinition.assignValue.test.ts +++ b/test/fhirtypes/ElementDefinition.assignValue.test.ts @@ -1,9 +1,7 @@ -import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { FshCode } from '../../src/fshtypes/FshCode'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { Package } from '../../src/export'; import { FshReference, Instance } from '../../src/fshtypes'; import { AssignmentRule } from '../../src/fshtypes/rules'; @@ -15,9 +13,8 @@ describe('ElementDefinition', () => { let medicationRequest: StructureDefinition; let medication: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { @@ -153,23 +150,14 @@ describe('ElementDefinition', () => { }); describe('R5 CodeableReference', () => { - let r5Defs: FHIRDefinitions; let doc: FSHDocument; let r5Fisher: TestFisher; let carePlan: StructureDefinition; - beforeAll(() => { - r5Defs = new FHIRDefinitions(); - loadFromPath( - path.join(__dirname, '..', 'testhelpers', 'testdefs'), - 'r5-definitions', - r5Defs - ); - }); - - beforeEach(() => { + beforeEach(async () => { doc = new FSHDocument('Conditions.fsh'); const input = new FSHTank([doc], minimalConfig); + const r5Defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); const pkg = new Package(input.config); r5Fisher = new TestFisher(input, r5Defs, pkg); carePlan = r5Fisher.fishForStructureDefinition('CarePlan'); diff --git a/test/fhirtypes/ElementDefinition.bindToVS.test.ts b/test/fhirtypes/ElementDefinition.bindToVS.test.ts index c76f95f4f..ffce3fe8c 100644 --- a/test/fhirtypes/ElementDefinition.bindToVS.test.ts +++ b/test/fhirtypes/ElementDefinition.bindToVS.test.ts @@ -1,17 +1,14 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher, loggerSpy } from '../testhelpers'; +import { TestFisher, getTestFHIRDefinitions, loggerSpy, testDefsPath } from '../testhelpers'; import omit from 'lodash/omit'; -import path from 'path'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; let observation: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { @@ -241,13 +238,9 @@ describe('ElementDefinition R5', () => { let defs: FHIRDefinitions; let r5CarePlan: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); - fisher = new TestFisher() - .withFHIR(defs) - .withCachePackageName('hl7.fhir.r5.core#5.0.0') - .withTestPackageName('r5-definitions'); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); + fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { r5CarePlan = fisher.fishForStructureDefinition('CarePlan'); diff --git a/test/fhirtypes/ElementDefinition.checkAssignInlineInstance.test.ts b/test/fhirtypes/ElementDefinition.checkAssignInlineInstance.test.ts index 99ceddb07..02bd76884 100644 --- a/test/fhirtypes/ElementDefinition.checkAssignInlineInstance.test.ts +++ b/test/fhirtypes/ElementDefinition.checkAssignInlineInstance.test.ts @@ -1,8 +1,6 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher } from '../testhelpers'; -import path from 'path'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { InstanceDefinition } from '../../src/fhirtypes'; describe('ElementDefinition', () => { @@ -12,9 +10,8 @@ describe('ElementDefinition', () => { let inlineCodeable: InstanceDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.constrainCardinality.test.ts b/test/fhirtypes/ElementDefinition.constrainCardinality.test.ts index bcb40219f..30bf45d64 100644 --- a/test/fhirtypes/ElementDefinition.constrainCardinality.test.ts +++ b/test/fhirtypes/ElementDefinition.constrainCardinality.test.ts @@ -1,18 +1,15 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher, loggerSpy } from '../testhelpers'; +import { TestFisher, getTestFHIRDefinitions, loggerSpy, testDefsPath } from '../testhelpers'; import omit from 'lodash/omit'; -import path from 'path'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; let observation: StructureDefinition; let respRate: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.constrainType.test.ts b/test/fhirtypes/ElementDefinition.constrainType.test.ts index 32bc59527..ddbabf500 100644 --- a/test/fhirtypes/ElementDefinition.constrainType.test.ts +++ b/test/fhirtypes/ElementDefinition.constrainType.test.ts @@ -1,21 +1,23 @@ -import { loadFromPath } from 'fhir-package-loader'; -import { TestFisher, loggerSpy } from '../testhelpers'; -import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; +import { + TestFisher, + loggerSpy, + getTestFHIRDefinitions, + testDefsPath, + TestFHIRDefinitions +} from '../testhelpers'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { ElementDefinitionType, LooseElementDefJSON } from '../../src/fhirtypes'; import { Type } from '../../src/utils'; import { CaretValueRule, OnlyRule } from '../../src/fshtypes/rules'; -import { readFileSync } from 'fs-extra'; import { Package, StructureDefinitionExporter } from '../../src/export'; import { minimalConfig } from '../utils/minimalConfig'; import { FshCanonical, Profile } from '../../src/fshtypes'; import { FSHTank } from '../../src/import'; import cloneDeep from 'lodash/cloneDeep'; import omit from 'lodash/omit'; -import path from 'path'; describe('ElementDefinition', () => { - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let observation: StructureDefinition; let jsonModifiedObservation: any; let modifiedObservation: StructureDefinition; @@ -25,9 +27,8 @@ describe('ElementDefinition', () => { let exporter: StructureDefinitionExporter; let pkg: Package; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); pkg = new Package(minimalConfig); fisher = new TestFisher().withFHIR(defs).withPackage(pkg); jsonModifiedObservation = cloneDeep(defs.fishForFHIR('Observation', Type.Resource)); @@ -168,20 +169,8 @@ describe('ElementDefinition', () => { expect(loggerSpy.getAllLogs('error')).toHaveLength(0); }); - it('should allow a choice to be constrained to a profile of Reference', () => { - const def = JSON.parse( - readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'StructureDefinition-reference-with-type.json' - ), - 'utf-8' - ).trim() - ); - defs.add(def); + it('should allow a choice to be constrained to a profile of Reference', async () => { + await defs.loadLocalPaths(testDefsPath('StructureDefinition-reference-with-type.json')); const valueX = extension.elements.find(e => e.id === 'Extension.value[x]'); const valueConstraint = new OnlyRule('value[x]'); valueConstraint.types = [{ type: 'ReferenceWithType' }]; @@ -1868,21 +1857,16 @@ describe('ElementDefinition', () => { }); describe('ElementDefinition R5', () => { - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let r5CarePlan: StructureDefinition; let exporter: StructureDefinitionExporter; let fisher: TestFisher; let pkg: Package; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); pkg = new Package(minimalConfig); - fisher = new TestFisher() - .withFHIR(defs) - .withPackage(pkg) - .withCachePackageName('hl7.fhir.r5.core#5.0.0') - .withTestPackageName('r5-definitions'); + fisher = new TestFisher().withFHIR(defs).withPackage(pkg); exporter = new StructureDefinitionExporter(new FSHTank([], minimalConfig), pkg, fisher); }); diff --git a/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts b/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts index 2e2574335..43bd21929 100644 --- a/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts +++ b/test/fhirtypes/ElementDefinition.setInstancePropertyByPath.test.ts @@ -1,19 +1,16 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { ElementDefinition, ElementDefinitionType } from '../../src/fhirtypes/ElementDefinition'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, testDefsPath, TestFisher } from '../testhelpers'; import { FshCode, FshQuantity } from '../../src/fshtypes'; -import path from 'path'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; let observation: StructureDefinition; let status: ElementDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { diff --git a/test/fhirtypes/ElementDefinition.slicing.test.ts b/test/fhirtypes/ElementDefinition.slicing.test.ts index 2b497225c..1820e5975 100644 --- a/test/fhirtypes/ElementDefinition.slicing.test.ts +++ b/test/fhirtypes/ElementDefinition.slicing.test.ts @@ -1,22 +1,20 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { ElementDefinitionType } from '../../src/fhirtypes'; -import { loggerSpy, TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, loggerSpy, testDefsPath, TestFisher } from '../testhelpers'; import findLastIndex from 'lodash/findLastIndex'; -import path from 'path'; describe('ElementDefinition', () => { let defs: FHIRDefinitions; let observation: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); }); beforeEach(() => { observation = fisher.fishForStructureDefinition('Observation'); + loggerSpy.reset(); }); describe('#sliceIt()', () => { diff --git a/test/fhirtypes/ElementDefinition.test.ts b/test/fhirtypes/ElementDefinition.test.ts index 9fbcb38d1..6bd102bc4 100644 --- a/test/fhirtypes/ElementDefinition.test.ts +++ b/test/fhirtypes/ElementDefinition.test.ts @@ -1,11 +1,9 @@ -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { ElementDefinition, ElementDefinitionType } from '../../src/fhirtypes/ElementDefinition'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; -import { loggerSpy, TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, loggerSpy, testDefsPath, TestFisher } from '../testhelpers'; import { Type } from '../../src/utils/Fishable'; import { Invariant, FshCode } from '../../src/fshtypes'; -import path from 'path'; import { cloneDeep } from 'lodash'; import { OnlyRule } from '../../src/fshtypes/rules'; @@ -26,9 +24,8 @@ describe('ElementDefinition', () => { let modifiedSubject: ElementDefinition; let practitioner: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); // resolve observation once to ensure it is present in defs observation = fisher.fishForStructureDefinition('Observation'); @@ -1137,9 +1134,8 @@ describe('ElementDefinition R5', () => { let valueX: ElementDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); fisher = new TestFisher().withFHIR(defs); // resolve observation once to ensure it is present in defs observation = fisher.fishForStructureDefinition('Observation'); diff --git a/test/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts index e159e56e8..c0c409a9c 100644 --- a/test/fhirtypes/StructureDefinition.test.ts +++ b/test/fhirtypes/StructureDefinition.test.ts @@ -1,10 +1,9 @@ import fs from 'fs-extra'; import path from 'path'; -import { loadFromPath } from 'fhir-package-loader'; import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; import { StructureDefinition } from '../../src/fhirtypes/StructureDefinition'; import { ElementDefinition, ElementDefinitionType } from '../../src/fhirtypes/ElementDefinition'; -import { loggerSpy, TestFisher } from '../testhelpers'; +import { getTestFHIRDefinitions, loggerSpy, testDefsPath, TestFisher } from '../testhelpers'; import { FshCode, Invariant, Logical, Mapping, Profile, Resource } from '../../src/fshtypes'; import { Type } from '../../src/utils/Fishable'; import { AddElementRule, ObeysRule, OnlyRule } from '../../src/fshtypes/rules'; @@ -28,9 +27,8 @@ describe('StructureDefinition', () => { let jsonUsCoreObservation: StructureDefinition; let fisher: TestFisher; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new TestFisher().withFHIR(defs); // resolve observation once to ensure it is present in defs observation = fisher.fishForStructureDefinition('Observation'); @@ -145,7 +143,7 @@ describe('StructureDefinition', () => { }); it('should throw a MissingSnapshotError when the StructureDefinition to load is missing a snapshot', () => { - const noSnapshotJsonObservation = defs.fishForFHIR('Observation', Type.Resource); + const noSnapshotJsonObservation = cloneDeep(defs.fishForFHIR('Observation', Type.Resource)); delete noSnapshotJsonObservation.snapshot; expect(() => StructureDefinition.fromJSON(noSnapshotJsonObservation)).toThrow( /http:\/\/hl7.org\/fhir\/StructureDefinition\/Observation is missing a snapshot/ diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts index b1f83ca1d..b9dccf7b4 100644 --- a/test/ig/IGExporter.IG.test.ts +++ b/test/ig/IGExporter.IG.test.ts @@ -1,7 +1,6 @@ import fs from 'fs-extra'; import path from 'path'; import temp from 'temp'; -import { loadFromPath } from 'fhir-package-loader'; import { IGExporter } from '../../src/ig'; import { StructureDefinition, @@ -13,11 +12,18 @@ import { } from '../../src/fhirtypes'; import { Package } from '../../src/export'; import { Configuration } from '../../src/fshtypes'; -import { FHIRDefinitions, loadCustomResources } from '../../src/fhirdefs'; -import { loggerSpy, TestFisher } from '../testhelpers'; +import { FHIRDefinitions } from '../../src/fhirdefs'; +import { + getTestFHIRDefinitions, + testDefsPath, + loggerSpy, + TestFisher, + TestFHIRDefinitions +} from '../testhelpers'; import { cloneDeep } from 'lodash'; import { minimalConfig } from '../utils/minimalConfig'; import { minimalConfigWithMenu } from '../utils/minimalConfigWithMenu'; +import { DiskBasedVirtualPackage } from 'fhir-package-loader'; describe('IGExporter', () => { temp.track(); @@ -35,14 +41,12 @@ describe('IGExporter', () => { const pkgInstances: InstanceDefinition[] = []; const pkgCodeSystems: CodeSystem[] = []; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath( - path.join(__dirname, '..', 'testhelpers', 'testdefs'), - 'fhir.no.ig.package#1.0.1', - defs + beforeAll(async () => { + defs = await getTestFHIRDefinitions( + true, + testDefsPath('fhir.no.ig.package#1.0.1'), + testDefsPath('r4-definitions') ); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); const profiles = path.join(fixtures, 'profiles'); @@ -1012,9 +1016,8 @@ describe('IGExporter', () => { const pkgLogicals: StructureDefinition[] = []; const pkgResources: StructureDefinition[] = []; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'simple-ig-plus'); const profiles = path.join(fixtures, 'profiles'); @@ -1353,14 +1356,12 @@ describe('IGExporter', () => { const pkgProfiles: StructureDefinition[] = []; const pkgInstances: InstanceDefinition[] = []; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath( - path.join(__dirname, '..', 'testhelpers', 'testdefs'), - 'fhir.no.ig.package#1.0.1', - defs + beforeAll(async () => { + defs = await getTestFHIRDefinitions( + true, + testDefsPath('fhir.no.ig.package#1.0.1'), + testDefsPath('r4-definitions') ); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); fixtures = path.join(__dirname, 'fixtures', 'simple-ig-meta-profile', 'input'); const profiles = path.join(fixtures, 'profiles'); @@ -1526,13 +1527,12 @@ describe('IGExporter', () => { let tempOut: string; let fixtures: string; let config: Configuration; - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'simple-ig-meta-profile'); - loadCustomResources(path.join(fixtures, 'input'), undefined, undefined, defs); + await defs.loadCustomResources(path.join(fixtures, 'input')); }); beforeEach(() => { @@ -1628,10 +1628,9 @@ describe('IGExporter', () => { let config: Configuration; let defs: FHIRDefinitions; - beforeAll(() => { + beforeAll(async () => { fixtures = path.join(__dirname, 'fixtures', 'customized-ig'); - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -1900,14 +1899,13 @@ describe('IGExporter', () => { let tempOut: string; let fixtures: string; let config: Configuration; - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let testScriptInstance: InstanceDefinition; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-resources'); - loadCustomResources(path.join(fixtures, 'input'), undefined, undefined, defs); + await defs.loadCustomResources(path.join(fixtures, 'input')); }); beforeEach(() => { @@ -2914,14 +2912,13 @@ describe('IGExporter', () => { let exporter: IGExporter; let tempOut: string; let fixtures: string; - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let config: Configuration; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-nested-resources'); - loadCustomResources(path.join(fixtures, 'input'), undefined, undefined, defs); + await defs.loadCustomResources(path.join(fixtures, 'input')); }); beforeEach(() => { @@ -2993,11 +2990,10 @@ describe('IGExporter', () => { expect(warning).not.toInclude('StructureDefinition-MyPatient.json'); }); - it('should not warn on deeply nested resources when implicated by the path-resource parameter', () => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + it('should not warn on deeply nested resources when implicated by the path-resource parameter', async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-nested-resources'); - loadCustomResources(path.join(fixtures, 'input'), fixtures, config.parameters, defs); + await defs.loadCustomResources(path.join(fixtures, 'input')); exporter.export(tempOut); const warning = loggerSpy.getFirstMessage('warn'); expect(warning).toInclude( @@ -3070,12 +3066,12 @@ describe('IGExporter', () => { let tempOut: string; let fixtures: string; let config: Configuration; - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-logical-model-example'); - loadCustomResources(path.join(fixtures, 'input'), undefined, undefined, defs); + await defs.loadCustomResources(path.join(fixtures, 'input')); }); beforeEach(() => { @@ -3229,12 +3225,12 @@ describe('IGExporter', () => { let tempOut: string; let fixtures: string; let config: Configuration; - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-binary-example'); - loadCustomResources(path.join(fixtures, 'input'), undefined, undefined, defs); + await defs.loadCustomResources(path.join(fixtures, 'input')); }); beforeEach(() => { @@ -3320,9 +3316,8 @@ describe('IGExporter', () => { let tempOut: string; let defs: FHIRDefinitions; - beforeAll(() => { - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -3815,10 +3810,9 @@ describe('IGExporter', () => { let config: Configuration; let defs: FHIRDefinitions; - beforeAll(() => { + beforeAll(async () => { fixtures = path.join(__dirname, 'fixtures', 'pages-folder-ig'); - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -3911,10 +3905,9 @@ describe('IGExporter', () => { let config: Configuration; let defs: FHIRDefinitions; - beforeAll(() => { + beforeAll(async () => { fixtures = path.join(__dirname, 'fixtures', 'invalid-pages-folder-ig'); - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); }); beforeEach(() => { @@ -3959,11 +3952,10 @@ describe('IGExporter', () => { let defs: FHIRDefinitions; let pkg: Package; - beforeAll(() => { + beforeAll(async () => { tempOut = temp.mkdirSync('sushi-test'); fixtures = path.join(__dirname, 'fixtures', 'sorted-pages-ig'); - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); pkg = new Package(minimalConfig); }); @@ -4059,12 +4051,11 @@ describe('IGExporter', () => { describe('#name-collision-ig', () => { let tempOut: string; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOut = temp.mkdirSync('sushi-test'); const fixtures = path.join(__dirname, 'fixtures', 'name-collision-ig'); - const defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + const defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); const pkg = new Package(minimalConfig); const exporter = new IGExporter(pkg, defs, fixtures); // No need to regenerate the IG on every test -- generate it once and inspect what you @@ -4122,12 +4113,11 @@ describe('IGExporter', () => { describe('#devious-id-ig', () => { let tempOut: string; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOut = temp.mkdirSync('sushi-test'); const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + const defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); const deviousConfig = cloneDeep(minimalConfig); deviousConfig.id = '/../../../arenticlever'; const pkg = new Package(deviousConfig); @@ -4155,13 +4145,12 @@ describe('IGExporter', () => { describe('#r5-ig-format', () => { let tempOut: string; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOut = temp.mkdirSync('sushi-test'); const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); // r5-definitions contains the guide-parameter-code CodeSystem, which was originally included in 5.0.0-ballot - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + const defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); const r5config = cloneDeep(minimalConfig); r5config.fhirVersion = ['5.0.0-ballot']; @@ -4542,14 +4531,14 @@ describe('IGExporter', () => { expect(igContent.copyrightLabel).toEqual('Shorty Fsh 2022+'); }); - it('should set versionAlgorithmString when provided in configuration', () => { + it('should set versionAlgorithmString when provided in configuration', async () => { // Export IG in this test so can test all variations of versionAlgorithm[x] const configWithVersionAlgorithm = cloneDeep(minimalConfig); configWithVersionAlgorithm.fhirVersion = ['5.0.0-ballot']; configWithVersionAlgorithm.versionAlgorithmString = 'date'; const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); const pkg = new Package(configWithVersionAlgorithm); const exporter = new IGExporter(pkg, defs, fixtures); const tempOut = temp.mkdirSync('sushi-test-version-alg'); @@ -4566,7 +4555,7 @@ describe('IGExporter', () => { expect(igContent.versionAlgorithmString).toEqual('date'); }); - it('should set versionAlgorithmCoding when provided as FSH Code in configuration', () => { + it('should set versionAlgorithmCoding when provided as FSH Code in configuration', async () => { // Export IG in this test so can test all variations of versionAlgorithm[x] const configWithVersionAlgorithm = cloneDeep(minimalConfig); configWithVersionAlgorithm.fhirVersion = ['5.0.0-ballot']; @@ -4576,7 +4565,7 @@ describe('IGExporter', () => { }; const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); const pkg = new Package(configWithVersionAlgorithm); const exporter = new IGExporter(pkg, defs, fixtures); const tempOut = temp.mkdirSync('sushi-test-version-alg'); @@ -4620,13 +4609,12 @@ describe('IGExporter', () => { describe('#r5-properties-on-r4-igs', () => { let tempOut: string; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOut = temp.mkdirSync('sushi-test'); const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); // r5-definitions contains the guide-parameter-code CodeSystem, which was originally included in 5.0.0-ballot - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + const defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); const r4WithR5propsConfig = cloneDeep(minimalConfig); r4WithR5propsConfig.copyrightLabel = 'Shorty Fsh 2022+'; @@ -4844,13 +4832,12 @@ describe('IGExporter', () => { let fixtures: string; let defs: FHIRDefinitions; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOutPages = temp.mkdirSync('sushi-test-pages'); fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - defs = new FHIRDefinitions(); // r5-definitions contains the guide-parameter-code CodeSystem, which was originally included in 5.0.0-ballot - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r5-definitions', defs); + defs = await getTestFHIRDefinitions(false, testDefsPath('r5-definitions')); }); it('should set sourceUrl to extension if it differs from nameUrl', () => { @@ -5295,13 +5282,13 @@ describe('IGExporter', () => { }); }); - it('should add versionAlgorithmString to an extension if provided', () => { + it('should add versionAlgorithmString to an extension if provided', async () => { // Export IG in this test so can test all variations of versionAlgorithm[x] const configWithVersionAlgorithm = cloneDeep(minimalConfig); configWithVersionAlgorithm.versionAlgorithmString = 'date'; const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); const pkg = new Package(configWithVersionAlgorithm); const exporter = new IGExporter(pkg, defs, fixtures); const tempOut = temp.mkdirSync('sushi-test-version-alg'); @@ -5323,7 +5310,7 @@ describe('IGExporter', () => { ]); }); - it('should add versionAlgorithmCoding to an extension if provided', () => { + it('should add versionAlgorithmCoding to an extension if provided', async () => { // Export IG in this test so can test all variations of versionAlgorithm[x] const configWithVersionAlgorithm = cloneDeep(minimalConfig); configWithVersionAlgorithm.versionAlgorithmCoding = { @@ -5332,7 +5319,7 @@ describe('IGExporter', () => { }; const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); const pkg = new Package(configWithVersionAlgorithm); const exporter = new IGExporter(pkg, defs, fixtures); const tempOut = temp.mkdirSync('sushi-test-version-alg'); @@ -5383,24 +5370,25 @@ describe('IGExporter', () => { describe('#resolve-latest', () => { let ig: any; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); const tempOut = temp.mkdirSync('sushi-test'); const fixtures = path.join(__dirname, 'fixtures', 'simple-ig'); - const defs = new FHIRDefinitions(); // r4-definitions contains ImplementationGuide-hl7.fhir.us.core.json used for resolution - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + const defs = await getTestFHIRDefinitions(false, testDefsPath('r4-definitions')); // add de.medizininformatikinitiative.kerndatensatz.consent package.json used for resolution - defs.addPackageJson('de.medizininformatikinitiative.kerndatensatz.consent', { - name: 'de.medizininformatikinitiative.kerndatensatz.consent', - version: '1.0.6', - description: 'Put a description here', - author: 'sebastianstubert', - fhirVersions: ['4.0.1'], - dependencies: { - 'de.einwilligungsmanagement': '1.0.1' - } - }); + await defs.loadVirtualPackage( + new DiskBasedVirtualPackage({ + name: 'de.medizininformatikinitiative.kerndatensatz.consent', + version: '1.0.6', + description: 'Put a description here', + author: 'sebastianstubert', + fhirVersions: ['4.0.1'], + dependencies: { + 'de.einwilligungsmanagement': '1.0.1' + } + }) + ); const config = cloneDeep(minimalConfig); config.dependencies = [ { packageId: 'hl7.fhir.us.core', version: 'latest' }, diff --git a/test/ig/IGExporter.addConfiguredPageContent.test.ts b/test/ig/IGExporter.addConfiguredPageContent.test.ts index 487f8ab1c..40510af33 100644 --- a/test/ig/IGExporter.addConfiguredPageContent.test.ts +++ b/test/ig/IGExporter.addConfiguredPageContent.test.ts @@ -8,6 +8,7 @@ import { loggerSpy } from '../testhelpers/loggerSpy'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { Configuration } from '../../src/fshtypes'; import { minimalConfig } from '../utils/minimalConfig'; +import { getTestFHIRDefinitions } from '../testhelpers'; describe('IGExporter', () => { // Track temp files/folders for cleanup @@ -16,8 +17,12 @@ describe('IGExporter', () => { describe('#configured-pagecontent', () => { let tempOut: string; let config: Configuration; + let defs: FHIRDefinitions; const outputFileSyncSpy = jest.spyOn(fs, 'outputFileSync'); - const defs = new FHIRDefinitions(); + + beforeAll(async () => { + defs = await getTestFHIRDefinitions(); + }); beforeEach(() => { tempOut = temp.mkdirSync('sushi-test'); diff --git a/test/ig/IGExporter.link-references.test.ts b/test/ig/IGExporter.link-references.test.ts index 584b67839..de3aaa2c0 100644 --- a/test/ig/IGExporter.link-references.test.ts +++ b/test/ig/IGExporter.link-references.test.ts @@ -3,10 +3,10 @@ import path from 'path'; import temp from 'temp'; import { Package } from '../../src/export'; import { IGExporter } from '../../src/ig'; -import { FHIRDefinitions, loadCustomResources } from '../../src/fhirdefs'; import { loggerSpy } from '../testhelpers/loggerSpy'; import { minimalConfig } from '../utils/minimalConfig'; import { CodeSystem } from '../../src/fhirtypes'; +import { getTestFHIRDefinitions, TestFHIRDefinitions } from '../../test/testhelpers'; describe('IGExporter', () => { temp.track(); @@ -14,18 +14,19 @@ describe('IGExporter', () => { describe('#link-references', () => { let pkg: Package; let exporter: IGExporter; - let defs: FHIRDefinitions; + let defs: TestFHIRDefinitions; let tempOut: string; - beforeEach(() => { + beforeEach(async () => { tempOut = temp.mkdirSync('sushi-test'); - defs = new FHIRDefinitions(); - loadCustomResources( - path.resolve(__dirname, 'fixtures', 'customized-ig-with-resources', 'input'), - undefined, - undefined, - defs + defs = await getTestFHIRDefinitions(); + const inputPath = path.resolve( + __dirname, + 'fixtures', + 'customized-ig-with-resources', + 'input' ); + await defs.loadCustomResources(inputPath); loggerSpy.reset(); }); @@ -75,9 +76,9 @@ describe('IGExporter', () => { expect(content).toMatch(/^\[nameless-cs\]: CodeSystem-nameless-cs\.html$/m); }); - it('should not create a file if there are no resources in the IG', () => { + it('should not create a file if there are no resources in the IG', async () => { // reset defs so we have no predefined resources - defs = new FHIRDefinitions(); + defs = await getTestFHIRDefinitions(); // configure the little codesystem to be omitted from the IG pkg = new Package({ ...minimalConfig, diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts index c4bd5884b..ba6c69557 100644 --- a/test/ig/IGExporter.test.ts +++ b/test/ig/IGExporter.test.ts @@ -1,25 +1,24 @@ import temp from 'temp'; import path from 'path'; import fs from 'fs-extra'; -import { loadFromPath } from 'fhir-package-loader'; import { Package } from '../../src/export'; import { IGExporter } from '../../src/ig'; import { importConfiguration } from '../../src/import'; -import { FHIRDefinitions } from '../../src/fhirdefs'; -import { loggerSpy } from '../testhelpers'; +import { getTestFHIRDefinitions, loggerSpy, testDefsPath } from '../testhelpers'; describe('IGExporter', () => { describe('#minimal-config', () => { let tempOut: string; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOut = temp.mkdirSync('sushi-test'); const configPath = path.join(__dirname, '..', 'import', 'fixtures', 'minimal-config.yaml'); const configYaml = fs.readFileSync(configPath, 'utf8'); const config = importConfiguration(configYaml, configPath); const pkg = new Package(config); - const exporter = new IGExporter(pkg, null, __dirname); + const defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); + const exporter = new IGExporter(pkg, defs, __dirname); exporter.export(tempOut); }); @@ -66,15 +65,14 @@ describe('IGExporter', () => { describe('#additional-config', () => { let tempOut: string; - beforeAll(() => { + beforeAll(async () => { loggerSpy.reset(); tempOut = temp.mkdirSync('sushi-test'); const configPath = path.join(__dirname, '..', 'import', 'fixtures', 'example-config.yaml'); const configYaml = fs.readFileSync(configPath, 'utf8'); const config = importConfiguration(configYaml, configPath); const pkg = new Package(config); - const defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + const defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); const exporter = new IGExporter(pkg, defs, __dirname); exporter.export(tempOut); }); diff --git a/test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/resources/StructureDefinition-MyPatient.json b/test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/resources/StructureDefinition-MyPatient.json new file mode 100644 index 000000000..4f65cc481 --- /dev/null +++ b/test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/resources/StructureDefinition-MyPatient.json @@ -0,0 +1,25 @@ +{ + "resourceType": "StructureDefinition", + "id": "MyPatient", + "url": "http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient", + "name": "MyPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "differential": { + "element": [ + { + "id": "Patient.deceased[x]", + "path": "Patient.deceased[x]", + "type": [ + { + "code": "boolean" + } + ] + } + ] + } +} diff --git a/test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/stuff/Patient-BarPatient.json b/test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/stuff/Patient-BarPatient.json new file mode 100644 index 000000000..c5bb3c640 --- /dev/null +++ b/test/ig/fixtures/customized-ig-with-non-standard-resource-path/input/stuff/Patient-BarPatient.json @@ -0,0 +1,8 @@ +{ + "resourceType": "Patient", + "id": "BarPatientInStuff", + "meta": { + "profile": ["http://hl7.org/fhir/sushi-test/StructureDefinition/MyPatient"] + }, + "name": [{ "family": "Bar" }] +} diff --git a/test/ig/predefinedResources.test.ts b/test/ig/predefinedResources.test.ts new file mode 100644 index 000000000..5b3477fe4 --- /dev/null +++ b/test/ig/predefinedResources.test.ts @@ -0,0 +1,218 @@ +import path from 'path'; +import { + getPredefinedResourcePaths, + loadPredefinedResources, + PREDEFINED_PACKAGE_NAME, + PREDEFINED_PACKAGE_VERSION +} from '../../src/ig'; +import { FHIRDefinitions } from '../../src/fhirdefs'; +import { getTestFHIRDefinitions } from '../testhelpers'; + +describe('#getPredefinedResourcePaths', () => { + it('should return all typical IG resource paths when they all exist', () => { + const inputDir = path.join(__dirname, 'fixtures', 'customized-ig-with-resources', 'input'); + const paths = getPredefinedResourcePaths(inputDir); + expect(paths).toEqual([ + path.join(inputDir, 'capabilities'), + path.join(inputDir, 'extensions'), + path.join(inputDir, 'models'), + path.join(inputDir, 'operations'), + path.join(inputDir, 'profiles'), + path.join(inputDir, 'resources'), + path.join(inputDir, 'vocabulary'), + path.join(inputDir, 'examples') + ]); + }); + + it('should return only the subset of typical IG resource paths that exist', () => { + const inputDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-logical-model-example', + 'input' + ); + const paths = getPredefinedResourcePaths(inputDir); + expect(paths).toEqual([path.join(inputDir, 'models'), path.join(inputDir, 'examples')]); + }); + + it('should return no paths when none of the typical IG paths exist', () => { + const inputDir = path.join(__dirname, 'fixtures', 'customized-ig', 'input'); + const paths = getPredefinedResourcePaths(inputDir); + expect(paths).toBeEmpty(); + }); + + it('should ignore non-standard paths when no path-resource param is passed in', () => { + const inputDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-non-standard-resource-path', + 'input' + ); + const paths = getPredefinedResourcePaths(inputDir); + expect(paths).toEqual([path.join(inputDir, 'resources')]); + }); + + it('should return non-standard paths that are specified by path-resource param', () => { + const projectDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-non-standard-resource-path' + ); + const inputDir = path.join(projectDir, 'input'); + const paths = getPredefinedResourcePaths(inputDir, projectDir, [ + { code: 'path-resource', value: 'input/stuff' } + ]); + expect(paths).toEqual([path.join(inputDir, 'resources'), path.join(inputDir, 'stuff')]); + }); + + it('should not return nested folders when no path-resource param is passed in', () => { + const inputDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-nested-resources', + 'input' + ); + const paths = getPredefinedResourcePaths(inputDir); + expect(paths).toEqual([path.join(inputDir, 'resources'), path.join(inputDir, 'examples')]); + }); + + it('should return nested folders specifically listed in the path-resource params', () => { + const projectDir = path.join(__dirname, 'fixtures', 'customized-ig-with-nested-resources'); + const inputDir = path.join(projectDir, 'input'); + const paths = getPredefinedResourcePaths(inputDir, projectDir, [ + { code: 'path-resource', value: 'input/resources/nested1' }, + { code: 'path-resource', value: 'input/resources/nested2' } + ]); + expect(paths).toEqual([ + path.join(inputDir, 'resources'), + path.join(inputDir, 'examples'), + path.join(inputDir, 'resources', 'nested1'), + path.join(inputDir, 'resources', 'nested2') + ]); + }); + + it('should return all nested folders matching a wildcard directory in the path-resource params', () => { + const projectDir = path.join(__dirname, 'fixtures', 'customized-ig-with-nested-resources'); + const inputDir = path.join(projectDir, 'input'); + const paths = getPredefinedResourcePaths(inputDir, projectDir, [ + { code: 'path-resource', value: 'input/resources/*' } + ]); + expect(paths).toEqual([ + path.join(inputDir, 'resources'), + path.join(inputDir, 'examples'), + path.join(inputDir, 'resources', 'nested1'), + path.join(inputDir, 'resources', 'nested2'), + path.join(inputDir, 'resources', 'path-resource-double-nest'), + path.join(inputDir, 'resources', 'path-resource-nest'), + path.join(inputDir, 'resources', 'path-resource-double-nest', 'jack'), + path.join(inputDir, 'resources', 'path-resource-double-nest', 'john'), + path.join(inputDir, 'resources', 'path-resource-double-nest', 'jack', 'examples') + ]); + }); + + it('should ignore invalid directories in the path-resource params', () => { + const projectDir = path.join(__dirname, 'fixtures', 'customized-ig-with-nested-resources'); + const inputDir = path.join(projectDir, 'input'); + const paths = getPredefinedResourcePaths(inputDir, projectDir, [ + { code: 'path-resource', value: 'input/i-dont-exist' } + ]); + expect(paths).toEqual([path.join(inputDir, 'resources'), path.join(inputDir, 'examples')]); + }); +}); + +describe('#loadPredefinedResources', () => { + let defs: FHIRDefinitions; + + beforeEach(async () => { + defs = await getTestFHIRDefinitions(); + }); + + it('should load all the resources in the typical resource paths for IGs', async () => { + const inputDir = path.join(__dirname, 'fixtures', 'customized-ig-with-resources', 'input'); + const status = await loadPredefinedResources(defs, inputDir); + expect(status).toBe('LOADED'); + expect(defs.getPackageLoadStatus(PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION)).toBe( + 'LOADED' + ); + const all = defs.findResourceInfos('*').map(info => `${info.resourceType}-${info.id}`); + expect(all).toEqual([ + 'CapabilityStatement-MyCS', + 'Goal-GoalWithDescription', + 'Patient-BarPatient', + 'Patient-MetaExtensionPatient', + 'Patient-FooPatient', + 'TestScript-MyTestScript', + 'StructureDefinition-patient-birthPlace', + 'StructureDefinition-patient-birthPlaceXML', + 'StructureDefinition-MyLM', + 'OperationDefinition-AnotherOD', + 'OperationDefinition-MyOD', + 'StructureDefinition-MyPatient', + 'StructureDefinition-MyTitlePatient', + 'Patient-null', + 'Unknown-Patient', + 'Unknown-null', + 'Patient-MetaExtensionNotExamplePatient', + 'Patient-BazPatient', + 'ValueSet-MyVS' + ]); + }); + + it('should load nested resources by default so it matches legacy behavior', async () => { + const inputDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-nested-resources', + 'input' + ); + const status = await loadPredefinedResources(defs, inputDir); + expect(status).toBe('LOADED'); + expect(defs.getPackageLoadStatus(PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION)).toBe( + 'LOADED' + ); + const all = defs.findResourceInfos('*').map(info => `${info.resourceType}-${info.id}`); + expect(all).toEqual([ + 'Patient-BarPatient', + 'StructureDefinition-MyPatient', + 'StructureDefinition-MyTitlePatient', + 'ValueSet-MyVS', + 'Patient-Jack', + 'Patient-John', + 'StructureDefinition-MyCorrectlyNestedPatient' + ]); + }); + + it('should ignore non-standard paths when no path-resource param is passed in', async () => { + const inputDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-non-standard-resource-path', + 'input' + ); + const status = await loadPredefinedResources(defs, inputDir); + expect(status).toBe('LOADED'); + expect(defs.getPackageLoadStatus(PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION)).toBe( + 'LOADED' + ); + const all = defs.findResourceInfos('*').map(info => `${info.resourceType}-${info.id}`); + expect(all).toEqual(['StructureDefinition-MyPatient']); + }); + + it('should load non-standard paths that are specified by path-resource param', async () => { + const projectDir = path.join( + __dirname, + 'fixtures', + 'customized-ig-with-non-standard-resource-path' + ); + const inputDir = path.join(projectDir, 'input'); + const status = await loadPredefinedResources(defs, inputDir, projectDir, [ + { code: 'path-resource', value: 'input/stuff' } + ]); + expect(status).toBe('LOADED'); + expect(defs.getPackageLoadStatus(PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION)).toBe( + 'LOADED' + ); + const all = defs.findResourceInfos('*').map(info => `${info.resourceType}-${info.id}`); + expect(all).toEqual(['StructureDefinition-MyPatient', 'Patient-BarPatientInStuff']); + }); +}); diff --git a/test/run/FshToFhir.test.ts b/test/run/FshToFhir.test.ts index 9afbfe9eb..5e172470a 100644 --- a/test/run/FshToFhir.test.ts +++ b/test/run/FshToFhir.test.ts @@ -1,12 +1,10 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { loggerSpy } from '../testhelpers'; +import { getLocalVirtualPackages, loggerSpy, testDefsPath } from '../testhelpers'; import { logger } from '../../src/utils/'; import { fshToFhir } from '../../src/run'; import * as processing from '../../src/utils/Processing'; import { Configuration } from '../../src/fshtypes'; -import { FHIRDefinitions } from '../../src/fhirdefs'; import { leftAlign } from '../utils/leftAlign'; +import { FHIRDefinitions } from '../../src/fhirdefs'; describe('#FshToFhir', () => { let loadSpy: jest.SpyInstance; @@ -100,45 +98,23 @@ describe('#FshToFhir', () => { }); it('should load external dependencies', async () => { - fshToFhir(''); + await fshToFhir(''); expect(loadSpy.mock.calls).toHaveLength(1); - expect(loadSpy.mock.calls[0]).toEqual([new FHIRDefinitions(), defaultConfig]); + expect(loadSpy.mock.calls[0]).toHaveLength(2); + expect(loadSpy.mock.calls[0][0]).toBeInstanceOf(FHIRDefinitions); + expect(loadSpy.mock.calls[0][1]).toEqual(defaultConfig); }); describe('#Conversion', () => { beforeAll(() => { - const sd = JSON.parse( - fs.readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'r4-definitions', - 'package', - 'StructureDefinition-StructureDefinition.json' - ), - 'utf-8' - ) - ); - const patient = JSON.parse( - fs.readFileSync( - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - 'r4-definitions', - 'package', - 'StructureDefinition-Patient.json' - ), - 'utf-8' - ) - ); - loadSpy.mockImplementation(defs => { - defs.add(sd); - defs.add(patient); - return Promise.resolve(); + loadSpy.mockImplementation(async (defs: FHIRDefinitions) => { + const vps = getLocalVirtualPackages( + testDefsPath('r4-definitions', 'package', 'StructureDefinition-StructureDefinition.json'), + testDefsPath('r4-definitions', 'package', 'StructureDefinition-Patient.json') + ); + for (const vp of vps) { + await defs.loadVirtualPackage(vp); + } }); }); diff --git a/test/testhelpers/TestFHIRDefinitions.ts b/test/testhelpers/TestFHIRDefinitions.ts new file mode 100644 index 000000000..e2b8fdf38 --- /dev/null +++ b/test/testhelpers/TestFHIRDefinitions.ts @@ -0,0 +1,180 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { mock, MockProxy } from 'jest-mock-extended'; +import { + CurrentBuildClient, + DiskBasedVirtualPackage, + InMemoryVirtualPackage, + PackageCache, + RegistryClient +} from 'fhir-package-loader'; +import { FHIRDefinitions, R5_DEFINITIONS_NEEDED_IN_R4 } from '../../src/fhirdefs'; +import { logMessage } from '../../src/utils'; +import { Readable } from 'stream'; +import { PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION } from '../../src//ig'; + +let VP_COUNTER = 0; + +const VIRTUAL_PACKAGE_OPTIONS = { + recursive: true, + allowNonResources: true, + log: logMessage +}; + +export class TestFHIRDefinitions extends FHIRDefinitions { + packageCacheMock: MockProxy; + registryClientMock: MockProxy; + currentBuildClientMock: MockProxy; + supplementalFHIRDefinitionsFactoryMock: jest.Mock; + private cachedPackages: string[] = []; + + constructor(isSupplementalFHIRDefinitions = false) { + // Mock out stuff so we don't make network calls or corrupt our FHIR cache + const packageCacheMock = mock(); + const registryClientMock = mock(); + const currentBuildClientMock = mock(); + const supplementalFHIRDefinitionsFactoryMock = jest.fn(); + super(isSupplementalFHIRDefinitions, supplementalFHIRDefinitionsFactoryMock, { + packageCache: packageCacheMock, + registryClient: registryClientMock, + currentBuildClient: currentBuildClientMock + }); + + // build out the PackageCache mock + packageCacheMock.cachePackageTarball.mockImplementation((name: string, version: string) => { + this.cachedPackages.push(`${name}#${version}`); + return Promise.resolve(`/mock/path/to/${name}#${version}`); + }); + packageCacheMock.isPackageInCache.mockImplementation((name: string, version: string) => + this.cachedPackages.includes(`${name}#${version}`) + ); + packageCacheMock.getPackagePath.mockImplementation((name: string, version: string) => { + return `/mock/path/to/${name}#${version}`; + }); + packageCacheMock.getPackageJSONPath.mockImplementation((name: string, version: string) => { + return `/mock/path/to/${name}#${version}/package/package.json`; + }); + packageCacheMock.getPotentialResourcePaths.mockImplementation( + (/*name: string, version: string*/) => { + return []; + } + ); + packageCacheMock.getResourceAtPath.mockImplementation((resourcePath: string) => { + const match = resourcePath.match(/\/mock\/path\/to\/([^#]+)#([^/]+)\/package\/package.json/); + if (match) { + return { name: match[1], version: match[2] }; + } + }); + this.packageCacheMock = packageCacheMock; + + // build out the RegistryClient mock + registryClientMock.download.mockResolvedValue(Readable.from(['mock-data'])); + registryClientMock.resolveVersion.mockImplementation((name: string, version: string) => { + if (version === 'latest') { + return Promise.resolve('9.9.9'); + } else if (version.endsWith('.x')) { + return Promise.resolve(version.replace('.x', '.9')); + } + return Promise.resolve(version); + }); + this.registryClientMock = registryClientMock; + + // build out the CurrentBuild mock + currentBuildClientMock.downloadCurrentBuild.mockResolvedValue(Readable.from(['mock-data'])); + currentBuildClientMock.getCurrentBuildDate.mockResolvedValue('20240824230227'); + this.currentBuildClientMock = currentBuildClientMock; + + // build out the supplementatlFHIRDefinitionsFactoryMock + supplementalFHIRDefinitionsFactoryMock.mockImplementation(async () => { + const testDefs = new TestFHIRDefinitions(true); + await testDefs.initialize(); + return testDefs; + }); + this.supplementalFHIRDefinitionsFactoryMock = supplementalFHIRDefinitionsFactoryMock; + } + + async loadLocalPaths(...localPaths: string[]) { + const virtualPackages = getLocalVirtualPackages(...localPaths); + for (const vp of virtualPackages) { + await this.loadVirtualPackage(vp); + } + } + + async loadCustomResources(...paths: string[]) { + await this.loadVirtualPackage( + new DiskBasedVirtualPackage( + { name: PREDEFINED_PACKAGE_NAME, version: PREDEFINED_PACKAGE_VERSION }, + paths, + { + log: logMessage, + allowNonResources: true, // support for logical instances + recursive: true + } + ) + ); + } +} + +export async function getTestFHIRDefinitions( + includeR5forR4 = false, + ...localPaths: string[] +): Promise { + const defs = new TestFHIRDefinitions(); + await defs.initialize(); + + if (includeR5forR4) { + // This mirrors what happens in Processing.ts for R4 and R4B + const R5forR4Map = new Map(); + R5_DEFINITIONS_NEEDED_IN_R4.forEach(def => R5forR4Map.set(def.id, def)); + const virtualR5forR4Package = new InMemoryVirtualPackage( + { name: 'sushi-r5forR4', version: '1.0.0' }, + R5forR4Map, + VIRTUAL_PACKAGE_OPTIONS + ); + await defs.loadVirtualPackage(virtualR5forR4Package); + } + + // Then load the specifically requested resource paths + if (localPaths.length > 0) { + await defs.loadLocalPaths(...localPaths); + } + return defs; +} + +export function getLocalVirtualPackage(localPath: string): DiskBasedVirtualPackage { + let packageJSON: { name: string; version: string }; + if (fs.existsSync(path.join(localPath, 'package.json'))) { + packageJSON = fs.readJSONSync(path.join(localPath, 'package.json')); + } else if (fs.existsSync(path.join(localPath, 'package', 'package.json'))) { + packageJSON = fs.readJSONSync(path.join(localPath, 'package', 'package.json')); + } else { + packageJSON = { name: `sushi-test-${++VP_COUNTER}`, version: '0.0.1' }; + } + return new DiskBasedVirtualPackage(packageJSON, [localPath], VIRTUAL_PACKAGE_OPTIONS); +} + +export function getLocalVirtualPackages(...localPaths: string[]): DiskBasedVirtualPackage[] { + const virtualPackages: DiskBasedVirtualPackage[] = []; + const leftoverPaths: string[] = []; + localPaths.forEach(p => { + if ( + fs.existsSync(path.join(p, 'package.json')) || + fs.existsSync(path.join(p, 'package', 'package.json')) + ) { + virtualPackages.push(getLocalVirtualPackage(p)); + } else { + leftoverPaths.push(p); + } + }); + if (leftoverPaths.length) { + const packageJSON = { name: `sushi-test-${++VP_COUNTER}`, version: '0.0.1' }; + virtualPackages.push( + new DiskBasedVirtualPackage(packageJSON, localPaths, VIRTUAL_PACKAGE_OPTIONS) + ); + } + return virtualPackages; +} + +export function testDefsPath(...subpathPart: string[]) { + return path.join(__dirname, 'testdefs', ...subpathPart); +} diff --git a/test/testhelpers/TestFisher.ts b/test/testhelpers/TestFisher.ts index f233b1c5b..2c102dc23 100644 --- a/test/testhelpers/TestFisher.ts +++ b/test/testhelpers/TestFisher.ts @@ -1,23 +1,21 @@ import { FHIRDefinitions } from '../../src/fhirdefs/FHIRDefinitions'; -import { loadFromPath } from 'fhir-package-loader'; -import { Type, Metadata } from '../../src/utils/Fishable'; +import { Type } from '../../src/utils/Fishable'; import { StructureDefinition } from '../../src/fhirtypes'; import { MasterFisher } from '../../src/utils'; import { FSHTank } from '../../src/import'; import { Package } from '../../src/export'; -import path from 'path'; -import os from 'os'; -import fs from 'fs-extra'; - -const defsCache = new FHIRDefinitions(); +// NOTE: This class used to have a capability to automatically load requested core FHIR resources +// from the FHIR cache and then save them to the fixtures if they weren't already there. This was +// primarily for convenience in early test writing. This feature has been removed since it was +// not used consistently, had some implementation bugs, and was difficult to reconcile with the new +// FHIR Package Loader that has asynchrounous loading methods. Future implementers can add this +// feature back if/when desired. export class TestFisher extends MasterFisher { constructor( public tank?: FSHTank, public fhir?: FHIRDefinitions, - public pkg?: Package, - public cachePkgName = 'hl7.fhir.r4.core#4.0.1', - public testPkgName = 'package' + public pkg?: Package ) { super(tank, fhir, pkg); } @@ -37,71 +35,16 @@ export class TestFisher extends MasterFisher { return this; } - withCachePackageName(pkgName: string) { - this.cachePkgName = pkgName; - return this; - } - - withTestPackageName(pkgName: string) { - this.testPkgName = pkgName; - return this; - } - fishForStructureDefinition( item: string, ...types: (Type.Resource | Type.Type | Type.Profile | Type.Extension | Type.Logical)[] ) { + if (!types?.length) { + types = [Type.Resource, Type.Type, Type.Profile, Type.Extension, Type.Logical]; + } const json = this.fishForFHIR(item, ...types); if (json) { return StructureDefinition.fromJSON(json); } } - - fishForFHIR(item: string, ...types: Type[]): any | undefined { - let json = super.fishForFHIR(item, ...types); - if (!json) { - // try loading it from the cache and fishing again - this.loadFromCache(item, ...types); - json = super.fishForFHIR(item, ...types); - } - return json; - } - - fishForMetadata(item: string, ...types: Type[]): Metadata { - let json = super.fishForMetadata(item, ...types); - if (!json) { - // try loading it from the cache and fishing again - this.loadFromCache(item, ...types); - json = super.fishForMetadata(item, ...types); - } - return json; - } - - loadFromCache(item: string, ...types: Type[]): void { - const cachePath = path.join(os.homedir(), '.fhir', 'packages', this.cachePkgName, 'package'); - // If there is no defsCache, load the FHIR defs from ~/.fhir - if (defsCache.size() === 0) { - loadFromPath(cachePath, 'temp', defsCache); - } - if (defsCache.size() > 0) { - // If the cache has been loaded, and we use a resource from the cache, - // make sure that resource is now copied into the test case package. - const json = defsCache.fishForFHIR(item, ...types); - if (json) { - console.log(`!!! RESOURCE LOADED FROM LOCAL FHIR CACHE: ${item} !!!`); - this.fhir.add(json); - fs.copyFileSync( - path.join(cachePath, `${json.resourceType}-${json.id}.json`), - path.join( - __dirname, - '..', - 'testhelpers', - 'testdefs', - this.testPkgName, - `${json.resourceType}-${json.id}.json` - ) - ); - } - } - } } diff --git a/test/testhelpers/asserts.ts b/test/testhelpers/asserts.ts index 08f41885a..e0431b580 100644 --- a/test/testhelpers/asserts.ts +++ b/test/testhelpers/asserts.ts @@ -303,11 +303,12 @@ export function assertConceptRule( } export function assertAutomaticR4Dependencies(packages: string[]) { + expect(packages).toContain('sushi-r5forR4#1.0.0'); AUTOMATIC_DEPENDENCIES.forEach(dep => { if (dep.packageId === 'hl7.terminology.r4' && dep.version === 'latest') { - expect(packages).toContain('hl7.terminology.r4#1.2.3-test'); + expect(packages).toContain('hl7.terminology.r4#9.9.9'); } else if (dep.packageId === 'hl7.fhir.uv.extensions.r4' && dep.version === 'latest') { - expect(packages).toContain('hl7.fhir.uv.extensions.r4#4.5.6-test'); + expect(packages).toContain('hl7.fhir.uv.extensions.r4#9.9.9'); } else if (!dep.packageId.endsWith('.r5')) { expect(packages).toContain(`${dep.packageId}#${dep.version}`); } @@ -317,9 +318,9 @@ export function assertAutomaticR4Dependencies(packages: string[]) { export function assertAutomaticR5Dependencies(packages: string[]) { AUTOMATIC_DEPENDENCIES.forEach(dep => { if (dep.packageId === 'hl7.terminology.r5' && dep.version === 'latest') { - expect(packages).toContain('hl7.terminology.r5#1.2.3-test'); + expect(packages).toContain('hl7.terminology.r5#9.9.9'); } else if (dep.packageId === 'hl7.fhir.uv.extensions.r5' && dep.version === 'latest') { - expect(packages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); + expect(packages).toContain('hl7.fhir.uv.extensions.r5#9.9.9'); } else if (!dep.packageId.endsWith('.r4')) { expect(packages).toContain(`${dep.packageId}#${dep.version}`); } diff --git a/test/testhelpers/index.ts b/test/testhelpers/index.ts index 7872c5a53..11b797577 100644 --- a/test/testhelpers/index.ts +++ b/test/testhelpers/index.ts @@ -1,4 +1,5 @@ export * from './asserts'; export * from './importSingleText'; export * from './loggerSpy'; +export * from './TestFHIRDefinitions'; export * from './TestFisher'; diff --git a/test/utils/FishingUtils.test.ts b/test/utils/FishingUtils.test.ts index 1ccaec9f3..859e979ab 100644 --- a/test/utils/FishingUtils.test.ts +++ b/test/utils/FishingUtils.test.ts @@ -1,7 +1,6 @@ -import { TestFisher, loggerSpy } from '../testhelpers'; +import { TestFisher, getTestFHIRDefinitions, loggerSpy } from '../testhelpers'; import { Package } from '../../src/export'; import { FSHDocument, FSHTank } from '../../src/import'; -import { FHIRDefinitions } from '../../src/fhirdefs'; import { Profile } from '../../src/fshtypes'; import { CaretValueRule } from '../../src/fshtypes/rules'; import { minimalConfig } from './minimalConfig'; @@ -20,10 +19,10 @@ describe('FishingUtils', () => { let fisher: TestFisher; let tank: FSHTank; - beforeAll(() => { + beforeAll(async () => { const doc = new FSHDocument('fileName'); tank = new FSHTank([doc], minimalConfig); - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); const pkg = new Package(tank.config); fisher = new TestFisher(tank, defs, pkg); }); diff --git a/test/utils/MasterFisher.test.ts b/test/utils/MasterFisher.test.ts index c62854d71..0e1f8e4e9 100644 --- a/test/utils/MasterFisher.test.ts +++ b/test/utils/MasterFisher.test.ts @@ -1,19 +1,19 @@ -import { loadFromPath } from 'fhir-package-loader'; +import fs from 'fs-extra'; import { FSHDocument, FSHTank } from '../../src/import'; import { Profile, Instance } from '../../src/fshtypes'; -import { FHIRDefinitions } from '../../src/fhirdefs'; import { Package } from '../../src/export'; import { StructureDefinition } from '../../src/fhirtypes'; import { MasterFisher } from '../../src/utils/MasterFisher'; import { loggerSpy } from '../testhelpers/loggerSpy'; -import path from 'path'; import { minimalConfig } from './minimalConfig'; import { cloneDeep } from 'lodash'; +import { getTestFHIRDefinitions, testDefsPath, TestFHIRDefinitions } from '../testhelpers'; +import { PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION } from '../../src/ig'; describe('MasterFisher', () => { let fisher: MasterFisher; - let defs: FHIRDefinitions; - beforeAll(() => { + let defs: TestFHIRDefinitions; + beforeAll(async () => { const doc1 = new FSHDocument('doc.fsh'); doc1.aliases.set('TankProfile1', 'http://hl7.org/fhir/us/minimal/StructureDefinition/prf1'); doc1.aliases.set('PkgProfile3', 'http://hl7.org/fhir/us/minimal/StructureDefinition/profile3'); @@ -59,8 +59,7 @@ describe('MasterFisher', () => { profile4.fhirVersion = '4.0.1'; pkg.profiles.push(profile3, profile4); - defs = new FHIRDefinitions(); - loadFromPath(path.join(__dirname, '..', 'testhelpers', 'testdefs'), 'r4-definitions', defs); + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); fisher = new MasterFisher(tank, defs, pkg); }); @@ -69,11 +68,12 @@ describe('MasterFisher', () => { expect(fisher.defaultFHIRVersion).toBe('4.0.1'); }); - it('should fallback to the config when it cannot determine its FHIR version from a loaded StructureDefinition', () => { + it('should fallback to the config when it cannot determine its FHIR version from a loaded StructureDefinition', async () => { const r4bConfig = cloneDeep(minimalConfig); r4bConfig.fhirVersion = ['4.3.0']; const r4bTank = new FSHTank([new FSHDocument('doc.fsh')], r4bConfig); - const r4bFisher = new MasterFisher(r4bTank, new FHIRDefinitions(), new Package(r4bTank.config)); + const defs = await getTestFHIRDefinitions(); + const r4bFisher = new MasterFisher(r4bTank, defs, new Package(r4bTank.config)); expect(r4bFisher.defaultFHIRVersion).toBe('4.3.0'); }); @@ -158,7 +158,8 @@ describe('MasterFisher', () => { url: 'http://hl7.org/fhir/StructureDefinition/Patient', version: '4.0.1', parent: 'http://hl7.org/fhir/StructureDefinition/DomainResource', - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:hl7.fhir.r4.core#4.0.1:${testDefsPath('r4-definitions', 'package', 'StructureDefinition-Patient.json')}` }); }); @@ -178,12 +179,18 @@ describe('MasterFisher', () => { }); }); - it('should return a profile that is predefined when it also exists in the package', () => { + it('should return a profile that is predefined when it also exists in the package', async () => { + const fhirVitalSignsPath = testDefsPath( + 'r4-definitions', + 'package', + 'StructureDefinition-vitalsigns.json' + ); + // Mark vital signs as predefined - const fhirDefinedVitalSigns = defs.fishForFHIR('vitalsigns'); - defs.addPredefinedResource('', defs.fishForFHIR('vitalsigns')); + defs.loadCustomResources(fhirVitalSignsPath); // Result should match the fhir defined version, not our defined version + const fhirDefinedVitalSigns = fs.readJSONSync(fhirVitalSignsPath); const result = fisher.fishForFHIR('vitalsigns'); expect(result.name).toBe(fhirDefinedVitalSigns.name); @@ -196,9 +203,9 @@ describe('MasterFisher', () => { url: fhirDefinedVitalSigns.url, version: fhirDefinedVitalSigns.version, parent: fhirDefinedVitalSigns.baseDefinition, - resourceType: 'StructureDefinition' + resourceType: 'StructureDefinition', + resourcePath: `virtual:${PREDEFINED_PACKAGE_NAME}#${PREDEFINED_PACKAGE_VERSION}:${fhirVitalSignsPath}` }); - defs.resetPredefinedResources(); }); it('should find an Instance that is only in the Tank', () => { diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts index 0821f876f..2c88b9910 100644 --- a/test/utils/Processing.test.ts +++ b/test/utils/Processing.test.ts @@ -1,7 +1,5 @@ import axios from 'axios'; -import nock from 'nock'; import child_process from 'child_process'; -import process from 'process'; import fs from 'fs-extra'; import path from 'path'; import temp from 'temp'; @@ -13,7 +11,6 @@ import { } from '../testhelpers/asserts'; import readlineSync from 'readline-sync'; import { - AUTOMATIC_DEPENDENCIES, isSupportedFHIRVersion, ensureInputDir, findInputDir, @@ -37,7 +34,6 @@ import { import { FHIRDefinitions } from '../../src/fhirdefs'; import { Package } from '../../src/export'; import { StructureDefinition, ValueSet, CodeSystem, InstanceDefinition } from '../../src/fhirtypes'; -import { PackageLoadError } from 'fhir-package-loader'; import { cloneDeep } from 'lodash'; import { FSHTank, FSHDocument } from '../../src/import'; import { @@ -54,161 +50,17 @@ import { Configuration } from '../../src/fshtypes'; import { EOL } from 'os'; +import { PREDEFINED_PACKAGE_NAME, PREDEFINED_PACKAGE_VERSION } from '../../src/ig'; +import { getTestFHIRDefinitions } from '../../test/testhelpers'; +import { InMemoryVirtualPackage, RegistryClient } from 'fhir-package-loader'; +import { logMessage } from '../../src/utils'; +import { mock, MockProxy } from 'jest-mock-extended'; -const NUM_R4_AUTO_DEPENDENCIES = 3; +const NUM_R4_AUTO_DEPENDENCIES = 4; const NUM_R5_AUTO_DEPENDENCIES = 3; -const TERM_R4_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' - } - } -}; - -const TERM_R5_PKG_RESPONSE = { - _id: 'hl7.terminology.r5', - name: 'hl7.terminology.r5', - 'dist-tags': { latest: '1.2.3-test' }, - versions: { - '1.2.3-test': { - name: 'hl7.terminology.r5', - version: '1.2.3-test', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe74983', - tarball: 'https://packages.simplifier.net/hl7.terminology.r5/1.2.3-test' - }, - fhirVersion: 'R5', - url: 'https://packages.simplifier.net/hl7.terminology.r5/1.2.3-test' - } - } -}; - -const EXT_R4_PKG_RESPONSE = { - _id: 'hl7.fhir.uv.extensions.r4', - name: 'hl7.fhir.uv.extensions.r4', - 'dist-tags': { latest: '4.5.6-test' }, - versions: { - '4.5.6-test': { - name: 'hl7.fhir.uv.extensions.r4', - version: '4.5.6-test', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe74983', - tarball: 'https://packages.simplifier.net/hl7.fhir.uv.extensions.r4/4.5.6-test' - }, - fhirVersion: 'R4', - url: 'https://packages.simplifier.net/hl7.fhir.uv.extensions.r4/4.5.6-test' - } - } -}; - -const EXT_R5_PKG_RESPONSE = { - _id: 'hl7.fhir.uv.extensions.r5', - name: 'hl7.fhir.uv.extensions.r5', - 'dist-tags': { latest: '4.5.6-test' }, - versions: { - '4.5.6-test': { - name: 'hl7.fhir.uv.extensions.r5', - version: '4.5.6-test', - description: 'None.', - dist: { - shasum: '1a1467bce19aace45771e0a51ef2ad9c3fe74983', - tarball: 'https://packages.simplifier.net/hl7.fhir.uv.extensions.r5/4.5.6-test' - }, - fhirVersion: 'R5', - url: 'https://packages.simplifier.net/hl7.fhir.uv.extensions.r5/4.5.6-test' - } - } -}; - -let loadedPackages: string[] = []; -let loadedSupplementalFHIRPackages: string[] = []; -let forceLoadErrorOnPackages: string[] = []; -let forceCertificateErrorOnPackages: string[] = []; - -jest.mock('fhir-package-loader', () => { - const original = jest.requireActual('fhir-package-loader'); - return { - ...original, - mergeDependency: jest.fn( - async (packageName: string, version: string, FHIRDefs: FHIRDefinitions) => { - if (version === 'latest') { - version = await original.lookUpLatestVersion(packageName); - } - // the mock loader can find hl7.fhir.(r2|r3|r4|r5|us).core and auto dependencies - if (forceLoadErrorOnPackages.indexOf(packageName) !== -1) { - throw new PackageLoadError(`${packageName}#${version}`); - } else if (forceCertificateErrorOnPackages.indexOf(packageName) !== -1) { - throw new Error('self signed certificate in certificate chain'); - } else if ( - /^hl7.fhir.(r2|r3|r4|r4b|r5|us).core$/.test(packageName) || - AUTOMATIC_DEPENDENCIES.some(dep => { - const [depRootId, packageRootId] = [dep.packageId, packageName].map(id => - /\.r[4-9]$/.test(id ?? '') ? (id ?? '').slice(0, -3) : id - ); - return depRootId === packageRootId; - }) - ) { - const packages = FHIRDefs.isSupplementalFHIRDefinitions - ? loadedSupplementalFHIRPackages - : loadedPackages; - packages.push(`${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}`); - } - } - ) - }; -}); - describe('Processing', () => { temp.track(); - let termR4NockScope: nock.Interceptor; - - beforeAll(() => nock.disableNetConnect()); - - beforeEach(() => { - termR4NockScope = nock('https://packages.fhir.org').persist().get('/hl7.terminology.r4'); - termR4NockScope.reply(200, TERM_R4_PKG_RESPONSE); - nock('https://packages.fhir.org') - .persist() - .get('/hl7.terminology.r5') - .reply(200, TERM_R5_PKG_RESPONSE); - nock('https://packages.fhir.org') - .persist() - .get('/hl7.fhir.uv.extensions.r4') - .reply(200, EXT_R4_PKG_RESPONSE); - nock('https://packages.fhir.org') - .persist() - .get('/hl7.fhir.uv.extensions.r5') - .reply(200, EXT_R5_PKG_RESPONSE); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - afterAll(() => { - nock.cleanAll(); - nock.restore(); - nock.enableNetConnect(); - }); describe('#isSupportedFHIRVersion', () => { it('should support published version >= 4.0.1', () => { @@ -652,74 +504,29 @@ describe('Processing', () => { let tempRoot: string; let config: Configuration; let keyInSpy: jest.SpyInstance; + let registryClientMock: MockProxy; beforeAll(() => { tempRoot = temp.mkdirSync('sushi-test'); - delete process.env.FPL_REGISTRY; }); beforeEach(() => { - nock('https://packages.fhir.org') - .persist() - .get('/hl7.fhir.us.core') - .reply(200, { - name: 'hl7.fhir.us.core', - 'dist-tags': { - latest: '3.1.0', - beta: '3.0.0-beta' - } - }); - - nock('https://packages2.fhir.org') - .persist() - .get('/packages/hl7.fhir.uv.genomics-reporting') - .reply(200, { - name: 'hl7.fhir.uv.genomics-reporting', - 'dist-tags': { - latest: '3.5.0' - } - }); - - nock('https://packages.fhir.org') - .persist() - .get('/hl7.fhir.us.mcode') - .reply(200, { - name: 'hl7.fhir.us.mcode', - 'dist-tags': { - latest: '2.1.1' - } - }); - - nock('https://custom-registry.example.org') - .persist() - .get('/hl7.fhir.us.core') - .reply(200, { - name: 'hl7.fhir.us.core', - 'dist-tags': { - latest: '3.1.0', - beta: '3.0.0-beta' - } - }); - - nock('https://custom-registry.example.org') - .persist() - .get('/hl7.fhir.uv.genomics-reporting') - .reply(200, { - name: 'hl7.fhir.uv.genomics-reporting', - 'dist-tags': { - latest: '3.5.1' - } - }); - - nock('https://custom-registry.example.org') - .persist() - .get('/hl7.fhir.us.mcode') - .reply(200, { - name: 'hl7.fhir.us.mcode', - 'dist-tags': { - latest: '2.1.2' - } - }); + registryClientMock = mock(); + registryClientMock.resolveVersion.mockImplementation((name: string, version: string) => { + if (version !== 'latest') { + return Promise.resolve(version); + } + switch (name) { + case 'hl7.fhir.us.core': + return Promise.resolve('3.1.0'); + case 'hl7.fhir.uv.genomics-reporting': + return Promise.resolve('3.5.0'); + case 'hl7.fhir.us.mcode': + return Promise.resolve('2.1.1'); + default: + return Promise.resolve(version); + } + }); const originalInput = path.join( __dirname, @@ -733,9 +540,7 @@ describe('Processing', () => { }); afterEach(() => { - nock.cleanAll(); keyInSpy.mockReset(); - delete process.env.FPL_REGISTRY; }); afterAll(() => { @@ -744,7 +549,7 @@ describe('Processing', () => { it('should update versioned dependencies in the configuration', async () => { keyInSpy.mockReturnValueOnce(true); - const result = await updateExternalDependencies(config); + const result = await updateExternalDependencies(config, registryClientMock); expect(result).toBe(true); const updatedDependencies = [ { @@ -777,7 +582,7 @@ describe('Processing', () => { it('should display a list of the available version updates', async () => { keyInSpy.mockReturnValueOnce(true); - const result = await updateExternalDependencies(config); + const result = await updateExternalDependencies(config, registryClientMock); expect(result).toBe(true); const displayedMessage = keyInSpy.mock.calls[0][0] as string; expect(displayedMessage).toMatch('- hl7.fhir.uv.genomics-reporting: 3.5.0'); @@ -790,7 +595,7 @@ describe('Processing', () => { it('should not update dependencies if the user quits without applying updates', async () => { keyInSpy.mockReturnValueOnce(false); - const result = await updateExternalDependencies(config); + const result = await updateExternalDependencies(config, registryClientMock); expect(result).toBe(false); const originalDependencies = [ { @@ -835,122 +640,21 @@ describe('Processing', () => { version: 'dev' } ]; - const result = await updateExternalDependencies(config); - expect(result).toBe(true); - expect(keyInSpy).toHaveBeenCalledTimes(0); - }); - - it('should use a custom registry to update versioned dependencies in the configuration', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - keyInSpy.mockReturnValueOnce(true); - const result = await updateExternalDependencies(config); - expect(result).toBe(true); - const updatedDependencies = [ - { - packageId: 'hl7.fhir.us.core', - version: '3.1.0' - }, - { - packageId: 'hl7.fhir.uv.vhdir', - version: 'current' - }, - { - packageId: 'hl7.fhir.uv.genomics-reporting', - version: '3.5.1' - }, - { - packageId: 'hl7.fhir.us.mcode', - id: 'mcode', - uri: 'http://hl7.org/fhir/us/mcode/ImplementationGuide/hl7.fhir.us.mcode', - version: '2.1.2' - }, - { - packageId: 'hl7.fhir.us.davinci-pas', - version: 'dev' - } - ]; - expect(config.dependencies).toEqual(updatedDependencies); - const configOnDisk = readConfig(tempRoot); - expect(configOnDisk.dependencies).toEqual(updatedDependencies); - }); - - it('should display a list of the available version updates from a custom registry', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - keyInSpy.mockReturnValueOnce(true); - const result = await updateExternalDependencies(config); - expect(result).toBe(true); - const displayedMessage = keyInSpy.mock.calls[0][0] as string; - expect(displayedMessage).toMatch('- hl7.fhir.uv.genomics-reporting: 3.5.1'); - expect(displayedMessage).toMatch('- hl7.fhir.us.mcode: 2.1.2'); - // packages without updates should not be listed - expect(displayedMessage).not.toMatch('hl7.fhir.us.core'); - expect(displayedMessage).not.toMatch('hl7.fhir.uv.vhdir'); - expect(displayedMessage).not.toMatch('hl7.fhir.us.davinci-pas'); - }); - - it('should not update dependencies from a custom registry if the user quits without applying updates', async () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - keyInSpy.mockReturnValueOnce(false); - const result = await updateExternalDependencies(config); - expect(result).toBe(false); - const originalDependencies = [ - { - packageId: 'hl7.fhir.us.core', - version: '3.1.0' - }, - { - packageId: 'hl7.fhir.uv.vhdir', - version: 'current' - }, - { - packageId: 'hl7.fhir.uv.genomics-reporting', - version: '2.0.0' - }, - { - packageId: 'hl7.fhir.us.mcode', - id: 'mcode', - uri: 'http://hl7.org/fhir/us/mcode/ImplementationGuide/hl7.fhir.us.mcode', - version: '2.0.1' - }, - { - packageId: 'hl7.fhir.us.davinci-pas', - version: 'dev' - } - ]; - const configOnDisk = readConfig(tempRoot); - expect(configOnDisk.dependencies).toEqual(originalDependencies); - }); - - it('should return true without requiring input if no dependencies can be updated from a custom registry', async () => { - config.dependencies = [ - { - packageId: 'hl7.fhir.us.core', - version: '3.1.0' - }, - { - packageId: 'hl7.fhir.uv.vhdir', - version: 'current' - }, - { - packageId: 'hl7.fhir.us.davinci-pas', - version: 'dev' - } - ]; - const result = await updateExternalDependencies(config); + const result = await updateExternalDependencies(config, registryClientMock); expect(result).toBe(true); expect(keyInSpy).toHaveBeenCalledTimes(0); }); it('should return true without requiring input if the configuration was obtained without a sushi-config.yaml file', async () => { delete config.filePath; - const result = await updateExternalDependencies(config); + const result = await updateExternalDependencies(config, registryClientMock); expect(result).toBe(true); expect(keyInSpy).toHaveBeenCalledTimes(0); }); it('should return true without requiring input if there are no dependencies in the configuration', async () => { config.dependencies = []; - const result = await updateExternalDependencies(config); + const result = await updateExternalDependencies(config, registryClientMock); expect(result).toBe(true); expect(keyInSpy).toHaveBeenCalledTimes(0); }); @@ -959,18 +663,15 @@ describe('Processing', () => { describe('#loadExternalDependencies()', () => { beforeEach(() => { loggerSpy.reset(); - loadedPackages = []; - loadedSupplementalFHIRPackages = []; - forceLoadErrorOnPackages = []; - forceCertificateErrorOnPackages = []; }); - it('should load specified dependencies', () => { + it('should load specified dependencies', async () => { const usCoreDependencyConfig = cloneDeep(minimalConfig); usCoreDependencyConfig.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadExternalDependencies(defs, usCoreDependencyConfig).then(() => { - expect(loadedPackages.length).toBe(2 + NUM_R4_AUTO_DEPENDENCIES); + const defs = await getTestFHIRDefinitions(); + return loadExternalDependencies(defs, usCoreDependencyConfig).then(async () => { + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(2 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4.core#4.0.1'); expect(loadedPackages).toContain('hl7.fhir.us.core#3.1.0'); assertAutomaticR4Dependencies(loadedPackages); @@ -978,16 +679,18 @@ describe('Processing', () => { }); }); - it('should load automatic dependencies first so they have lowest priority', () => { + it('should load automatic dependencies first so they have lowest priority', async () => { const usCoreDependencyConfig = cloneDeep(minimalConfig); usCoreDependencyConfig.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, usCoreDependencyConfig).then(() => { - expect(loadedPackages.length).toBe(2 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(2 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toEqual([ + 'sushi-r5forR4#1.0.0', 'hl7.fhir.uv.tools#current', - 'hl7.terminology.r4#1.2.3-test', - 'hl7.fhir.uv.extensions.r4#4.5.6-test', + 'hl7.terminology.r4#9.9.9', + 'hl7.fhir.uv.extensions.r4#9.9.9', 'hl7.fhir.us.core#3.1.0', 'hl7.fhir.r4.core#4.0.1' ]); @@ -995,7 +698,7 @@ describe('Processing', () => { }); }); - it('should honor user-specified order when user puts automatic dependencies in the config', () => { + it('should honor user-specified order when user puts automatic dependencies in the config', async () => { const usCoreDependencyConfig = cloneDeep(minimalConfig); usCoreDependencyConfig.dependencies = [ { packageId: 'hl7.fhir.us.core', version: '3.1.0' }, @@ -1003,10 +706,12 @@ describe('Processing', () => { { packageId: 'hl7.terminology.r4', version: '8.8.8' }, { packageId: 'hl7.fhir.uv.tools', version: '9.9.9' } ]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, usCoreDependencyConfig).then(() => { - expect(loadedPackages.length).toBe(2 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(2 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toEqual([ + 'sushi-r5forR4#1.0.0', 'hl7.fhir.us.core#3.1.0', 'hl7.fhir.uv.extensions.r4#7.7.7', 'hl7.terminology.r4#8.8.8', @@ -1017,110 +722,120 @@ describe('Processing', () => { }); }); - it('should support prerelease FHIR R4B dependencies', () => { + it('should support prerelease FHIR R4B dependencies', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['4.1.0']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4b.core#4.1.0'); + expect(loadedPackages).toContain('sushi-r5forR4#1.0.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); + expect(loadedPackages).toContain('hl7.terminology.r4#9.9.9'); expect(loggerSpy.getLastMessage('warn')).toMatch( /support for pre-release versions of FHIR is experimental/s ); }); }); - it('should support prerelease FHIR R4B snapshot dependencies', () => { + it('should support prerelease FHIR R4B snapshot dependencies', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['4.3.0-snapshot1']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4b.core#4.3.0-snapshot1'); + expect(loadedPackages).toContain('sushi-r5forR4#1.0.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); + expect(loadedPackages).toContain('hl7.terminology.r4#9.9.9'); expect(loggerSpy.getLastMessage('warn')).toMatch( /support for pre-release versions of FHIR is experimental/s ); }); }); - it('should support official FHIR R4B dependency (will be 4.3.0)', () => { + it('should support official FHIR R4B dependency', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['4.3.0']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4b.core#4.3.0'); + expect(loadedPackages).toContain('sushi-r5forR4#1.0.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); + expect(loadedPackages).toContain('hl7.terminology.r4#9.9.9'); expect(loggerSpy.getAllLogs('warn')).toHaveLength(0); }); }); - it('should support prerelease FHIR R5 dependencies', () => { + it('should support prerelease FHIR R5 dependencies', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['4.5.0']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R5_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r5.core#4.5.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); expect(loggerSpy.getLastMessage('warn')).toMatch( /support for pre-release versions of FHIR is experimental/s ); }); }); - it('should support prerelease FHIR R5 snapshot dependencies', () => { + it('should support prerelease FHIR R5 snapshot dependencies', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['5.0.0-snapshot1']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R5_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r5.core#5.0.0-snapshot1'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); expect(loggerSpy.getLastMessage('warn')).toMatch( /support for pre-release versions of FHIR is experimental/s ); }); }); - it('should support official FHIR R5 dependency', () => { + it('should support official FHIR R5 dependency', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['5.0.0']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R5_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R5_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r5.core#5.0.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#9.9.9'); expect(loggerSpy.getAllLogs('warn')).toHaveLength(0); }); }); - it('should support FHIR current dependencies', () => { + it('should support FHIR current dependencies', async () => { const config = cloneDeep(minimalConfig); config.fhirVersion = ['current']; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, config).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R5_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R5_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r5.core#current'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#9.9.9'); expect(loggerSpy.getLastMessage('warn')).toMatch( /support for pre-release versions of FHIR is experimental/s ); }); }); - it('should support virtual FHIR extension packages', async () => { + it('should support implied FHIR extension packages', async () => { // We want to do this for each, so make a function we'll just call for each version const testExtPackage = async ( extId: string, @@ -1129,24 +844,26 @@ describe('Processing', () => { fhirId: string, fhirVersion: string ) => { - loadedPackages = []; - loadedSupplementalFHIRPackages = []; - forceLoadErrorOnPackages = []; - forceCertificateErrorOnPackages = []; - const virtualExtensionsConfig = cloneDeep(minimalConfig); - virtualExtensionsConfig.fhirVersion = [fhirVersion]; - virtualExtensionsConfig.dependencies = [{ packageId: extId, version: fhirVersion }]; - const defs = new FHIRDefinitions(); - return loadExternalDependencies(defs, virtualExtensionsConfig).then(() => { + const impliedExtensionsConfig = cloneDeep(minimalConfig); + impliedExtensionsConfig.fhirVersion = [fhirVersion]; + impliedExtensionsConfig.dependencies = [{ packageId: extId, version: fhirVersion }]; + const defs = await getTestFHIRDefinitions(); + const supplementalSpy = jest.spyOn(defs, 'loadSupplementalFHIRPackage'); + return loadExternalDependencies(defs, impliedExtensionsConfig).then(() => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); if (fhirVersion === '5.0.0') { - expect(loadedPackages.length).toBe(1 + NUM_R5_AUTO_DEPENDENCIES); + expect(loadedPackages).toHaveLength(1 + NUM_R5_AUTO_DEPENDENCIES); assertAutomaticR5Dependencies(loadedPackages); } else { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); assertAutomaticR4Dependencies(loadedPackages); } expect(loadedPackages).toContain(`${fhirId}#${fhirVersion}`); - expect(loadedSupplementalFHIRPackages).toEqual([`${suppFhirId}#${suppFhirVersion}`]); + expect(supplementalSpy).toHaveBeenCalledExactlyOnceWith( + `${suppFhirId}#${suppFhirVersion}` + ); expect(loggerSpy.getAllLogs('error')).toHaveLength(0); }); }; @@ -1180,18 +897,20 @@ describe('Processing', () => { ); }); - it('should log a warning if wrong virtual FHIR extension package version is used', () => { - const virtualExtensionsConfig = cloneDeep(minimalConfig); - virtualExtensionsConfig.fhirVersion = ['5.0.0']; - virtualExtensionsConfig.dependencies = [ + it('should log a warning if wrong implied FHIR extension package version is used', async () => { + const impliedExtensionsConfig = cloneDeep(minimalConfig); + impliedExtensionsConfig.fhirVersion = ['5.0.0']; + impliedExtensionsConfig.dependencies = [ { packageId: 'hl7.fhir.extensions.r2', version: '1.0.2' } ]; - const defs = new FHIRDefinitions(); - return loadExternalDependencies(defs, virtualExtensionsConfig).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R5_AUTO_DEPENDENCIES); + const defs = await getTestFHIRDefinitions(); + const supplementalSpy = jest.spyOn(defs, 'loadSupplementalFHIRPackage'); + return loadExternalDependencies(defs, impliedExtensionsConfig).then(() => { + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R5_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r5.core#5.0.0'); assertAutomaticR5Dependencies(loadedPackages); - expect(loadedSupplementalFHIRPackages).toEqual(['hl7.fhir.r2.core#1.0.2']); + expect(supplementalSpy).toHaveBeenCalledExactlyOnceWith('hl7.fhir.r2.core#1.0.2'); expect(loggerSpy.getLastMessage('warn')).toMatch( /Incorrect package version: hl7\.fhir\.extensions\.r2#1\.0\.2\./ ); @@ -1199,12 +918,17 @@ describe('Processing', () => { }); }); - it('should log an error when it fails to load a dependency', () => { + it('should log an error when it fails to load a dependency', async () => { const badDependencyConfig = cloneDeep(minimalConfig); badDependencyConfig.dependencies = [{ packageId: 'hl7.does.not.exist', version: 'current' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); + defs.currentBuildClientMock.downloadCurrentBuild + .mockReset() + .calledWith('hl7.does.not.exist') + .mockRejectedValue(new Error()); return loadExternalDependencies(defs, badDependencyConfig).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4.core#4.0.1'); assertAutomaticR4Dependencies(loadedPackages); expect(loggerSpy.getLastMessage('error')).toMatch( @@ -1215,14 +939,19 @@ describe('Processing', () => { }); }); - it('should log a more detailed error when it fails to load a dependency due to certificate issue', () => { + it('should log a more detailed error when it fails to load a dependency due to certificate issue', async () => { const selfSignedDependencyConfig = cloneDeep(minimalConfig); selfSignedDependencyConfig.dependencies = [ { packageId: 'self-signed.package', version: '1.0.0' } ]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); + defs.registryClientMock.download + .mockReset() + .calledWith('self-signed.package', '1.0.0') + .mockRejectedValue(new Error('self signed certificate in certificate chain')); return loadExternalDependencies(defs, selfSignedDependencyConfig).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4.core#4.0.1'); assertAutomaticR4Dependencies(loadedPackages); expect(loggerSpy.getLastMessage('error')).toMatch( @@ -1235,12 +964,13 @@ describe('Processing', () => { }); }); - it('should log an error when a dependency has no specified version', () => { + it('should log an error when a dependency has no specified version', async () => { const badDependencyConfig = cloneDeep(minimalConfig); badDependencyConfig.dependencies = [{ packageId: 'hl7.fhir.r4.core' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadExternalDependencies(defs, badDependencyConfig).then(() => { - expect(loadedPackages.length).toBe(1 + NUM_R4_AUTO_DEPENDENCIES); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(1 + NUM_R4_AUTO_DEPENDENCIES); expect(loadedPackages).toContain('hl7.fhir.r4.core#4.0.1'); assertAutomaticR4Dependencies(loadedPackages); expect(loggerSpy.getLastMessage('error')).toMatch( @@ -1253,130 +983,91 @@ describe('Processing', () => { }); describe('#loadAutomaticDependencies', () => { - let customTermNockScope: nock.Interceptor; - - beforeAll(() => { - delete process.env.FPL_REGISTRY; - }); - beforeEach(() => { loggerSpy.reset(); - loadedPackages = []; - loadedSupplementalFHIRPackages = []; - forceLoadErrorOnPackages = []; - forceCertificateErrorOnPackages = []; - customTermNockScope = nock('https://custom-registry.example.org') - .persist() - .get('/hl7.terminology.r4'); - customTermNockScope.reply(200, TERM_R4_PKG_RESPONSE); - nock('https://custom-registry.example.org') - .persist() - .get('/hl7.terminology.r5') - .reply(200, TERM_R5_PKG_RESPONSE); - nock('https://custom-registry.example.org') - .persist() - .get('/hl7.fhir.uv.extensions.r4') - .reply(200, EXT_R4_PKG_RESPONSE); - nock('https://custom-registry.example.org') - .persist() - .get('/hl7.fhir.uv.extensions.r5') - .reply(200, EXT_R5_PKG_RESPONSE); }); - afterEach(() => { - delete process.env.FPL_REGISTRY; - }); - - it('should load each automatic dependency for FHIR R4', () => { + it('should load each automatic dependency for FHIR R4', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies('4.0.1', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(4); + expect(loadedPackages).toContain('sushi-r5forR4#1.0.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r4#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r4#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r4#9.9.9'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); }); }); - it('should load each automatic dependency for FHIR R4B', () => { + it('should load each automatic dependency for FHIR R4B', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies('4.3.0', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); + expect(loadedPackages).toHaveLength(4); + expect(loadedPackages).toContain('sushi-r5forR4#1.0.0'); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r4#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r4#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r4#9.9.9'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); }); }); - it('should load each automatic dependency for FHIR R5 Final Draft', () => { + it('should load each automatic dependency for FHIR R5 Final Draft', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies('5.0.0-draft-final', config.dependencies, defs).then(() => { + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(3); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#9.9.9'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); }); }); - it('should load each automatic dependency for FHIR R5', () => { + it('should load each automatic dependency for FHIR R5', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies('5.0.0', config.dependencies, defs).then(() => { + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(3); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#9.9.9'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); }); }); - it('should load each automatic dependency for FHIR R6 prerelease', () => { + it('should load each automatic dependency for FHIR R6 prerelease', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies('6.0.0-ballot2', config.dependencies, defs).then(() => { + const loadedPackages = defs.findPackageInfos('*').map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(3); expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); + expect(loadedPackages).toContain('hl7.terminology.r5#9.9.9'); + expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#9.9.9'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); }); }); - it('should should use the package server query to get the terminology version', () => { - // Change the version to 2.4.6-test just to be sure - nock.removeInterceptor(termR4NockScope); - const otherResponse = cloneDeep(TERM_R4_PKG_RESPONSE); - otherResponse['dist-tags'].latest = '2.4.6-test'; - nock('https://packages.fhir.org').get('/hl7.terminology.r4').reply(200, otherResponse); - - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( - () => { - expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES); - expect(loadedPackages).toContain('hl7.terminology.r4#2.4.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); - } - ); - }); - - it('should not load dependencies that are present in the config', () => { + it('should not load dependencies that are present in the config', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.uv.tools', version: '2.2.0-test' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( () => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); expect(loadedPackages).not.toContain('hl7.fhir.uv.tools#current'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); @@ -1384,12 +1075,15 @@ describe('Processing', () => { ); }); - it('should not load dependencies that are present in the config even if they have an r{x} suffix and the auto dependency does not', () => { + it('should not load dependencies that are present in the config even if they have an r{x} suffix and the auto dependency does not', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.uv.tools.r4', version: '4.0.0-test' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( () => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); expect(loadedPackages).not.toContain('hl7.fhir.uv.tools#current'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); @@ -1397,12 +1091,15 @@ describe('Processing', () => { ); }); - it('should not load dependencies that are present in the config even if they do not have an r{x} suffix and the auto dependency does', () => { + it('should not load dependencies that are present in the config even if they do not have an r{x} suffix and the auto dependency does', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.terminology', version: '4.0.0-test' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( () => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); expect(loadedPackages).not.toContain('hl7.terminology.r4#1.2.3-test'); expect(loadedPackages).not.toContain('hl7.terminology.r4#latest'); @@ -1411,12 +1108,15 @@ describe('Processing', () => { ); }); - it('should not load dependencies that are present in the config even if they do have an r{x} suffix that does not match the auto dependency r{x} suffix', () => { + it('should not load dependencies that are present in the config even if they do have an r{x} suffix that does not match the auto dependency r{x} suffix', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.terminology.r5', version: '4.0.0-test' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( () => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); expect(loadedPackages).not.toContain('hl7.terminology.r4#1.2.3-test'); expect(loadedPackages).not.toContain('hl7.terminology.r4#latest'); @@ -1425,13 +1125,19 @@ describe('Processing', () => { ); }); - it('should log a warning when it fails to load an automatic dependency', () => { - forceLoadErrorOnPackages.push('hl7.fhir.uv.tools'); + it('should log a warning when it fails to load an automatic dependency', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); + defs.currentBuildClientMock.downloadCurrentBuild + .mockReset() + .calledWith('hl7.fhir.uv.tools') + .mockRejectedValue(new Error()); return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( () => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); expect(loadedPackages).not.toContain('hl7.fhir.uv.tools#current'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); @@ -1440,42 +1146,29 @@ describe('Processing', () => { ); // But don't log the warning w/ details about proxies expect(loggerSpy.getLastMessage('warn')).not.toMatch(/SSL/s); + // And don't log an error + expect(loggerSpy.getAllLogs('error')).toHaveLength(0); } ); }); - it('should log a warning when it fails to find the latest version of an automatic dependency because of wrong JSON format', () => { - // Make the package server return an invalid package entry - nock.removeInterceptor(termR4NockScope); - nock('https://packages.fhir.org').get('/hl7.terminology.r4').reply(200, {}); - - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( - () => { - expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); - expect(loadedPackages).not.toContain('hl7.terminology.r4#4.0.0'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); - expect(loggerSpy.getLastMessage('warn')).toMatch( - /Failed to load automatically-provided hl7\.terminology\.r4#latest/s - ); - // But don't log the warning w/ details about proxies - expect(loggerSpy.getLastMessage('warn')).not.toMatch(/SSL/s); - } - ); - }); - - it('should log a warning when it fails to find the latest version of an automatic dependency because of HTTP error', () => { - // Make the package server return an invalid package entry - nock.removeInterceptor(termR4NockScope); - nock('https://packages.fhir.org').get('/hl7.terminology.r4').reply(500); - + it('should log a warning when it fails to find the latest version of an automatic dependency due to an error', async () => { const config = cloneDeep(minimalConfig); config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); + const defs = await getTestFHIRDefinitions(); + defs.registryClientMock.resolveVersion + .mockReset() + .mockImplementation((name: string, version: string) => { + if (name === 'hl7.terminology.r4' && version === 'latest') { + return Promise.reject(new Error()); + } + return Promise.resolve(version === 'latest' ? '9.9.9' : version); + }); return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( () => { + const loadedPackages = defs + .findPackageInfos('*') + .map(pkg => `${pkg.name}#${pkg.version}`); expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); expect(loadedPackages).not.toContain('hl7.terminology.r4#4.0.0'); expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); @@ -1484,119 +1177,8 @@ describe('Processing', () => { ); // But don't log the warning w/ details about proxies expect(loggerSpy.getLastMessage('warn')).not.toMatch(/SSL/s); - } - ); - }); - - it('should log a more detailed warning when it fails to load an automatic dependency due to certificate issue', () => { - forceCertificateErrorOnPackages.push('hl7.fhir.uv.tools'); - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( - () => { - expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES - 1); - expect(loadedPackages).not.toContain('hl7.fhir.uv.tools#current'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(1); - expect(loggerSpy.getLastMessage('warn')).toMatch( - /Failed to load automatically-provided hl7\.fhir\.uv\.tools#current/s - ); - // AND it should log the detailed message about SSL - expect(loggerSpy.getLastMessage('warn')).toMatch( - /Sometimes this error occurs in corporate or educational environments that use proxies and\/or SSL inspection/s - ); - } - ); - }); - - it('should load each automatic dependency for FHIR R4 from a custom registry', () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies('4.0.1', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); - expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r4#4.5.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); - }); - }); - - it('should load each automatic dependency for FHIR R4B from a custom registry', () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies('4.3.0', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); - expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r4#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r4#4.5.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); - }); - }); - - it('should load each automatic dependency for FHIR R5 Final Draft from a custom registry', () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies('5.0.0-draft-final', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); - expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); - }); - }); - - it('should load each automatic dependency for FHIR R5 from a custom registry', () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies('5.0.0', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); - expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); - }); - }); - - it('should load each automatic dependency for FHIR R6 prerelease from a custom registry', () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies('6.0.0-ballot2', config.dependencies, defs).then(() => { - expect(loadedPackages).toHaveLength(3); - expect(loadedPackages).toContain('hl7.fhir.uv.tools#current'); - expect(loadedPackages).toContain('hl7.terminology.r5#1.2.3-test'); - expect(loadedPackages).toContain('hl7.fhir.uv.extensions.r5#4.5.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); - }); - }); - - it('should should use the package server query to get the terminology version', () => { - process.env.FPL_REGISTRY = 'https://custom-registry.example.org'; - // Change the version to 2.4.6-test just to be sure - nock.removeInterceptor(customTermNockScope); - const otherResponse = cloneDeep(TERM_R4_PKG_RESPONSE); - otherResponse['dist-tags'].latest = '2.4.6-test'; - nock('https://custom-registry.example.org') - .get('/hl7.terminology.r4') - .reply(200, otherResponse); - - const config = cloneDeep(minimalConfig); - config.dependencies = [{ packageId: 'hl7.fhir.us.core', version: '3.1.0' }]; - const defs = new FHIRDefinitions(); - return loadAutomaticDependencies(config.fhirVersion[0], config.dependencies, defs).then( - () => { - expect(loadedPackages).toHaveLength(NUM_R4_AUTO_DEPENDENCIES); - expect(loadedPackages).toContain('hl7.terminology.r4#2.4.6-test'); - expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); + // And don't log an error + expect(loggerSpy.getAllLogs('error')).toHaveLength(0); } ); }); @@ -1805,12 +1387,28 @@ describe('Processing', () => { let outPackage: Package; let defs: FHIRDefinitions; - beforeAll(() => { + beforeAll(async () => { tempIGPubRoot = temp.mkdirSync('output-ig-dir'); const input = path.join(__dirname, 'fixtures', 'valid-yaml'); const config = readConfig(input); outPackage = new Package(config); - defs = new FHIRDefinitions(); + defs = await getTestFHIRDefinitions(); + + const predefinedResourceMap = new Map(); + const myPredefinedProfile = new StructureDefinition(); + myPredefinedProfile.id = 'my-duplicate-profile'; + myPredefinedProfile.url = 'http://example.com/StructureDefinition/my-duplicate-profile'; + predefinedResourceMap.set('my-duplicate-profile', myPredefinedProfile.toJSON(true)); + const myPredefinedInstance = new InstanceDefinition(); + myPredefinedInstance.id = 'my-duplicate-instance'; + myPredefinedInstance.resourceType = 'Patient'; + predefinedResourceMap.set('my-duplicate-instance', myPredefinedInstance.toJSON()); + const predefinedPkg = new InMemoryVirtualPackage( + { name: PREDEFINED_PACKAGE_NAME, version: PREDEFINED_PACKAGE_VERSION }, + predefinedResourceMap, + { log: logMessage, allowNonResources: true } + ); + await defs.loadVirtualPackage(predefinedPkg); const myProfile = new StructureDefinition(); myProfile.id = 'my-profile'; @@ -1864,22 +1462,11 @@ describe('Processing', () => { myInstanceOfLogical._instanceMeta.sdType = 'http://example.com/StructureDefinition/some-logical'; myInstanceOfLogical._instanceMeta.sdKind = 'logical'; - - const myPredefinedProfile = new StructureDefinition(); - myPredefinedProfile.id = 'my-duplicate-profile'; - myPredefinedProfile.url = 'http://example.com/StructureDefinition/my-duplicate-profile'; - defs.addPredefinedResource( - 'StructureDefinition-my-duplicate-profile.json', - myPredefinedProfile - ); + // duplicate of predefined resource added earlier const myFSHDefinedProfile = new StructureDefinition(); myFSHDefinedProfile.id = 'my-duplicate-profile'; myFSHDefinedProfile.url = 'http://example.com/StructureDefinition/my-duplicate-profile'; - - const myPredefinedInstance = new InstanceDefinition(); - myPredefinedInstance.id = 'my-duplicate-instance'; - myPredefinedInstance.resourceType = 'Patient'; - defs.addPredefinedResource('Patient-my-duplicate-instance.json', myPredefinedInstance); + // duplicate of predefined resource added earlier const myFSHDefinedInstance = new InstanceDefinition(); myFSHDefinedInstance.id = 'my-duplicate-instance'; myFSHDefinedInstance.resourceType = 'Patient'; diff --git a/tsconfig.json b/tsconfig.json index 7c372fc2d..9ee2466d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": ["ES2020"], /* Specify library files to be included in the compilation. */ + "lib": ["ES2020", "DOM"], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ @@ -51,7 +51,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -66,7 +66,7 @@ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ - "resolveJsonModule": true /* Include modules imported with '.json' extension */ + "resolveJsonModule": true /* Include modules imported with '.json' extension */ }, "include": [ "src/**/*" From 214a1f3edc5f2b3ee25e24ec721b4b3f1f85e67b Mon Sep 17 00:00:00 2001 From: Chris Moesel Date: Fri, 20 Dec 2024 14:51:59 -0500 Subject: [PATCH 2/4] v3.13.0 (#1545) --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index eea4eb61d..007583596 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "fsh-sushi", - "version": "3.12.1", + "version": "3.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fsh-sushi", - "version": "3.12.1", + "version": "3.13.0", "license": "Apache-2.0", "dependencies": { "ajv": "^8.17.1", diff --git a/package.json b/package.json index 937f135b5..ae464dedf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fsh-sushi", - "version": "3.12.1", + "version": "3.13.0", "description": "Sushi Unshortens Short Hand Inputs (FSH Compiler)", "scripts": { "build": "del-cli dist && tsc && copyfiles -u 3 \"src/utils/init-project/*\" dist/utils/init-project", From f3e66ec5c8ce6d40ffc703d72990a63c5e363067 Mon Sep 17 00:00:00 2001 From: Chris Moesel Date: Sat, 21 Dec 2024 14:08:27 -0500 Subject: [PATCH 3/4] Update FPL and avoid using readdirSync w/ recursive flag (#1546) Update FHIR Package Loader to the patch release that does not use features requiring Node JS 18.20. Also update recursive file traversal to avoid using readdirSync with recursive flag (added in Node 18.17) and Dirent.parentPath (added in Node 18.20). --- npm-shrinkwrap.json | 8 ++++---- package.json | 2 +- src/app.ts | 7 ++++++- src/ig/predefinedResources.ts | 18 ++++++++++++++---- test/ig/predefinedResources.test.ts | 4 ++-- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 007583596..fe211c3ba 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -15,7 +15,7 @@ "chalk": "^4.1.2", "commander": "^12.1.0", "fhir": "^4.12.0", - "fhir-package-loader": "^2.0.0", + "fhir-package-loader": "^2.0.1", "fs-extra": "^11.2.0", "html-minifier-terser": "5.1.1", "https-proxy-agent": "^7.0.5", @@ -3602,9 +3602,9 @@ } }, "node_modules/fhir-package-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-2.0.0.tgz", - "integrity": "sha512-26TQYVU6frmlluiDY0Nm/hjgGertOdN092BRHja+yCbAx1kvk4sBzAW9SmmaD33SeHzsDxEPWvTRBEDq/5nSFw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-2.0.1.tgz", + "integrity": "sha512-6TuYbOpaUySPa3dgpTrtWS51/KKWtzxHgvcZ5PAuRJS4Y0rhbWM8rZ5zhi87uqPqKIP9Sv5AvpxtyRpsTp0WwQ==", "dependencies": { "axios": "^1.7.8", "chalk": "^4.1.2", diff --git a/package.json b/package.json index ae464dedf..b10527978 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "chalk": "^4.1.2", "commander": "^12.1.0", "fhir": "^4.12.0", - "fhir-package-loader": "^2.0.0", + "fhir-package-loader": "^2.0.1", "fs-extra": "^11.2.0", "html-minifier-terser": "5.1.1", "https-proxy-agent": "^7.0.5", diff --git a/src/app.ts b/src/app.ts index 913d1d93f..b7c4a1255 100644 --- a/src/app.ts +++ b/src/app.ts @@ -285,7 +285,12 @@ async function runBuild(input: string, program: OptionValues, helpText: string) await loadExternalDependencies(defs, config); // Load custom resources from typical input/* paths and custom configured paths - await loadPredefinedResources(defs, path.join(input, '..'), originalInput, config.parameters); + await loadPredefinedResources( + defs, + path.resolve(input, '..'), + path.resolve(originalInput), + config.parameters + ); // Optimize the database after loading to ensure the most efficient queries defs.optimize(); diff --git a/src/ig/predefinedResources.ts b/src/ig/predefinedResources.ts index f40f9fc5b..8d971cf38 100644 --- a/src/ig/predefinedResources.ts +++ b/src/ig/predefinedResources.ts @@ -47,10 +47,20 @@ export function getPredefinedResourcePaths( predefinedResourcePaths.add(fullPath); // path-resource paths ending with /* should recursively include subfolders if (pathResource.endsWith('/*')) { - fs.readdirSync(fullPath, { withFileTypes: true, recursive: true }) - .filter(file => file.isDirectory()) - .map(dir => path.join(dir.parentPath, dir.name)) - .forEach(p => predefinedResourcePaths.add(p)); + // Note: Do not use readdirSync w/ {recursive: true} since it was only added in Node 18.17. + const addRecursiveChildFolders = (folderPath: string) => { + const stat = fs.statSync(folderPath); + if (stat.isDirectory()) { + fs.readdirSync(folderPath, { withFileTypes: true }).forEach(entry => { + if (entry.isDirectory()) { + const childFolder = path.resolve(folderPath, entry.name); + predefinedResourcePaths.add(childFolder); + addRecursiveChildFolders(childFolder); + } + }); + } + }; + addRecursiveChildFolders(fullPath); } } }); diff --git a/test/ig/predefinedResources.test.ts b/test/ig/predefinedResources.test.ts index 5b3477fe4..fd1822218 100644 --- a/test/ig/predefinedResources.test.ts +++ b/test/ig/predefinedResources.test.ts @@ -103,10 +103,10 @@ describe('#getPredefinedResourcePaths', () => { path.join(inputDir, 'resources', 'nested1'), path.join(inputDir, 'resources', 'nested2'), path.join(inputDir, 'resources', 'path-resource-double-nest'), - path.join(inputDir, 'resources', 'path-resource-nest'), path.join(inputDir, 'resources', 'path-resource-double-nest', 'jack'), + path.join(inputDir, 'resources', 'path-resource-double-nest', 'jack', 'examples'), path.join(inputDir, 'resources', 'path-resource-double-nest', 'john'), - path.join(inputDir, 'resources', 'path-resource-double-nest', 'jack', 'examples') + path.join(inputDir, 'resources', 'path-resource-nest') ]); }); From 853a04e30a67fda4b8771a7e52538289c9dd738b Mon Sep 17 00:00:00 2001 From: Chris Moesel Date: Sat, 21 Dec 2024 14:13:51 -0500 Subject: [PATCH 4/4] v3.13.1 (#1547) --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index fe211c3ba..9e52f0119 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "fsh-sushi", - "version": "3.13.0", + "version": "3.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fsh-sushi", - "version": "3.13.0", + "version": "3.13.1", "license": "Apache-2.0", "dependencies": { "ajv": "^8.17.1", diff --git a/package.json b/package.json index b10527978..f856f79d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fsh-sushi", - "version": "3.13.0", + "version": "3.13.1", "description": "Sushi Unshortens Short Hand Inputs (FSH Compiler)", "scripts": { "build": "del-cli dist && tsc && copyfiles -u 3 \"src/utils/init-project/*\" dist/utils/init-project",
Errors Warnings Time (sec)Performance Logs Diff
${reportErrors(repo)} ${reportWarnings(repo)} ${reportElapsed(repo)}${reportPerformance(repo)} ${config.version1} → ${config.version2} ${ repo.changed ? `HTML | JSON` : '' @@ -681,7 +908,7 @@ async function createReport(repos: Repo[], config: Config) { `
${repo.name}#${repo.branch}abortedaborted