diff --git a/README.md b/README.md index f8099f9..d3df4fe 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The [default PackageLoader](src/loader/DefaultPackageLoader.ts) implementation p * the standard FHIR registry is used (`packages.fhir.org`) for downloading published packages, falling back to `packages2.fhir.org` when necessary * unless an `FPL_REGISTRY` environment variable is defined, in which case its value is used as the URL for an NPM registry to use _instead_ of the standard FHIR registry * the `build.fhir.org` build server is used for downloading _current_ builds of packages -* a 500-item LRU in-memory cache is used to minimize repeated disk reads for resource files +* a 200-item LRU in-memory cache is used to minimize repeated disk reads for resource files To instantiate the default `PackageLoader`, import the asynchronous `defaultPackageLoader` function and invoke it, optionally passing in an `options` object with a log method to use for logging: @@ -119,7 +119,17 @@ if (status !== LoadStatus.LOADED) { } ``` -For more control over the `PackageLoader`, use the [BasePackageLoader](src/loader/BasePackageLoader.ts). This allows you to specify the [PackageDB](src/db), [PackageCache](src/cache), [RegistryClient](src/registry), and [CurrentBuildClient](src/current) you wish to use FHIRPackageLoader comes with implementations of each of these, but you may also provide your own implementations that adhere to the relevant interfaces. The BasePackageLoader also allows you to configure the size of the in-memory LRU resource cache. +For more control over the `PackageLoader`, use the [BasePackageLoader](src/loader/BasePackageLoader.ts). This allows you to specify the [PackageDB](src/db), [PackageCache](src/cache), [RegistryClient](src/registry), and [CurrentBuildClient](src/current) you wish to use. FHIRPackageLoader comes with implementations of each of these, but you may also provide your own implementations that adhere to the relevant interfaces. + +#### BasePackageLoader Options + +The [BasePackageLoader](src/loader/BasePackageLoader.ts) allows for an options object to be passed in with the following optional keys: +* `log`: a function with signature `(level: string, message: string) => void`. The BasePackageLoader logs messages with levels `'debug'`, `'info'`, `'warn'`, and `'error'`. +* `resourceCacheSize`: the size of the LRU cache for caching resources. If `0`, the LRU cache will not be used. The default cache size is `200`. +* `safeMode`: determines if/how returned resources can be modified without affecting subsequent calls. The default safe mode is SafeMode.OFF. + * `SafeMode.OFF`: No safety precautions are in place. This is the most performant mode but if users modify resources returned by the package loader, subsequent calls to the package loader may return the modified resources. + * `SafeMode.CLONE`: All resource results are cloned before being returned. This is the least performant but ensures that resource modifications never affect subsequent calls. + * `SafeMode.FREEZE`: All resource results are recursively frozen before being returned. This is more performant than cloning, but if users attempt to modify a returned resource, an error will be thrown. If users need to modify a returned resource, they must clone it first. ### PackageLoader Functions diff --git a/package-lock.json b/package-lock.json index 552ce67..82968a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "axios": "^1.7.7", + "axios": "^1.7.8", "chalk": "^4.1.2", "commander": "^12.1.0", "fhir": "^4.12.0", @@ -18,35 +18,35 @@ "lodash": "^4.17.21", "mnemonist": "^0.39.8", "semver": "^7.6.3", - "sql.js": "^1.11.0", + "sql.js": "^1.12.0", "tar": "^7.4.3", "temp": "^0.9.1", - "winston": "^3.14.2" + "winston": "^3.17.0" }, "bin": { "fpl": "dist/app.js" }, "devDependencies": { "@types/fs-extra": "^11.0.4", - "@types/jest": "^29.5.11", - "@types/lodash": "^4.14.202", - "@types/node": "^20.16.5", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.13", + "@types/node": "^20.17.8", "@types/opener": "^1.4.3", "@types/semver": "^7.5.6", "@types/sql.js": "^1.4.9", "@types/temp": "^0.9.4", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "eslint": "^9.9.1", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", "jest-extended": "^4.0.2", "jest-mock-extended": "^4.0.0-beta1", "opener": "^1.5.2", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" } }, "node_modules/@ampproject/remapping": { @@ -696,18 +696,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", @@ -740,10 +740,19 @@ "node": "*" } }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true, + "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", @@ -786,9 +795,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", - "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -803,6 +812,53 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -817,9 +873,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "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" @@ -1510,6 +1566,12 @@ "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", "dev": true }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -1554,15 +1616,21 @@ } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/jsonfile": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", @@ -1573,17 +1641,16 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "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.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "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" } @@ -1649,17 +1716,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz", - "integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==", + "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.4.0", - "@typescript-eslint/type-utils": "8.4.0", - "@typescript-eslint/utils": "8.4.0", - "@typescript-eslint/visitor-keys": "8.4.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", @@ -1683,16 +1749,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz", - "integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==", + "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.4.0", - "@typescript-eslint/types": "8.4.0", - "@typescript-eslint/typescript-estree": "8.4.0", - "@typescript-eslint/visitor-keys": "8.4.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": { @@ -1712,14 +1777,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz", - "integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==", + "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.4.0", - "@typescript-eslint/visitor-keys": "8.4.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" @@ -1730,14 +1794,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz", - "integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==", + "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.4.0", - "@typescript-eslint/utils": "8.4.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1748,6 +1811,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -1755,11 +1821,10 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz", - "integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==", + "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" }, @@ -1769,14 +1834,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz", - "integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==", + "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.4.0", - "@typescript-eslint/visitor-keys": "8.4.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", @@ -1798,16 +1862,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz", - "integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==", + "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.4.0", - "@typescript-eslint/types": "8.4.0", - "@typescript-eslint/typescript-estree": "8.4.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" @@ -1818,17 +1881,21 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz", - "integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==", + "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.4.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" @@ -1838,10 +1905,22 @@ "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/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1983,10 +2062,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", @@ -2431,9 +2509,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2602,27 +2680,31 @@ } }, "node_modules/eslint": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", - "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.9.1", + "@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.0", - "@nodelib/fs.walk": "^1.2.8", + "@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.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.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", @@ -2632,15 +2714,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2673,9 +2751,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "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, "dependencies": { "esrecurse": "^4.3.0", @@ -2711,9 +2789,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "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" @@ -2735,14 +2813,14 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2752,9 +2830,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "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" @@ -2877,7 +2955,6 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2894,7 +2971,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3495,15 +3571,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4399,9 +4466,9 @@ "dev": true }, "node_modules/logform": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", - "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "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", @@ -4469,7 +4536,6 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -4919,9 +4985,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" @@ -5310,9 +5376,9 @@ "dev": true }, "node_modules/sql.js": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.11.0.tgz", - "integrity": "sha512-GsLUDU3vhOo14Pd5ME0y2te49JQyby6HuoCuadevEV+CGgTUjmYRrm7B7lhRyzOgrmcWmspUfyjNb6sOAEqdsA==" + "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", @@ -5575,12 +5641,6 @@ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5764,9 +5824,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -5878,32 +5938,32 @@ } }, "node_modules/winston": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", - "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "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.1", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", - "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "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.6.1", + "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, diff --git a/package.json b/package.json index 8ba4b88..2eca015 100644 --- a/package.json +++ b/package.json @@ -38,28 +38,28 @@ ], "devDependencies": { "@types/fs-extra": "^11.0.4", - "@types/jest": "^29.5.11", - "@types/lodash": "^4.14.202", - "@types/node": "^20.16.5", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.13", + "@types/node": "^20.17.8", "@types/opener": "^1.4.3", "@types/semver": "^7.5.6", "@types/sql.js": "^1.4.9", "@types/temp": "^0.9.4", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "eslint": "^9.9.1", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", "jest-extended": "^4.0.2", "jest-mock-extended": "^4.0.0-beta1", "opener": "^1.5.2", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.7.2" }, "dependencies": { - "axios": "^1.7.7", + "axios": "^1.7.8", "chalk": "^4.1.2", "commander": "^12.1.0", "fhir": "^4.12.0", @@ -68,9 +68,9 @@ "lodash": "^4.17.21", "mnemonist": "^0.39.8", "semver": "^7.6.3", - "sql.js": "^1.11.0", + "sql.js": "^1.12.0", "tar": "^7.4.3", "temp": "^0.9.1", - "winston": "^3.14.2" + "winston": "^3.17.0" } } diff --git a/src/loader/BasePackageLoader.ts b/src/loader/BasePackageLoader.ts index 9665124..1eb2ea3 100644 --- a/src/loader/BasePackageLoader.ts +++ b/src/loader/BasePackageLoader.ts @@ -9,8 +9,15 @@ import { RegistryClient } from '../registry'; import { LogFunction } from '../utils'; import { PackageCache } from '../cache/PackageCache'; import { LoadStatus, PackageLoader } from './PackageLoader'; +import { cloneDeep } from 'lodash'; -const DEFAULT_RESOURCE_CACHE_SIZE = 500; +const DEFAULT_RESOURCE_CACHE_SIZE = 200; + +export enum SafeMode { + OFF = 'OFF', + FREEZE = 'FREEZE', + CLONE = 'CLONE' +} const CERTIFICATE_MESSAGE = '\n\nSometimes this error occurs in corporate or educational environments that use proxies and/or SSL ' + @@ -22,12 +29,14 @@ const CERTIFICATE_MESSAGE = export type BasePackageLoaderOptions = { log?: LogFunction; resourceCacheSize?: number; + safeMode?: SafeMode; }; export class BasePackageLoader implements PackageLoader { private log: LogFunction; private virtualPackages: Map; private resourceCache?: LRUCache; + private safeMode: SafeMode; constructor( private packageDB: PackageDB, @@ -42,6 +51,7 @@ export class BasePackageLoader implements PackageLoader { if (resourceCacheSize > 0) { this.resourceCache = new LRUCache(resourceCacheSize); } + this.safeMode = options.safeMode ?? SafeMode.OFF; } async loadPackage(name: string, version: string): Promise { @@ -373,9 +383,12 @@ export class BasePackageLoader implements PackageLoader { } else { resource = this.packageCache.getResourceAtPath(resourcePath); } + if (this.safeMode === SafeMode.FREEZE) { + resource = deepFreeze(resource); + } this.resourceCache?.set(resourcePath, resource); } - return resource; + return this.safeMode === SafeMode.CLONE ? cloneDeep(resource) : resource; } getPackageLoadStatus(name: string, version: string): LoadStatus { @@ -474,3 +487,17 @@ function getSDFlavor(resourceJSON: any) { } } } + +// See: https://www.geeksforgeeks.org/how-to-deep-freeze-an-object-in-javascript/ +function deepFreeze(obj: any) { + Object.keys(obj).forEach(property => { + if ( + typeof obj[property] === 'object' && + obj[property] !== null && + !Object.isFrozen(obj[property]) + ) { + deepFreeze(obj[property]); + } + }); + return Object.freeze(obj); +} diff --git a/src/loader/DefaultPackageLoader.ts b/src/loader/DefaultPackageLoader.ts index 2e8462c..552d4b8 100644 --- a/src/loader/DefaultPackageLoader.ts +++ b/src/loader/DefaultPackageLoader.ts @@ -16,7 +16,5 @@ export async function defaultPackageLoader(options: BasePackageLoaderOptions) { }); const registryClient = new DefaultRegistryClient({ log: options.log }); const buildClient = new BuildDotFhirDotOrgClient({ log: options.log }); - return new BasePackageLoader(packageDB, packageCache, registryClient, buildClient, { - log: options.log - }); + return new BasePackageLoader(packageDB, packageCache, registryClient, buildClient, options); } diff --git a/test/loader/BasePackageLoader.test.ts b/test/loader/BasePackageLoader.test.ts index 7b0f82c..b932259 100644 --- a/test/loader/BasePackageLoader.test.ts +++ b/test/loader/BasePackageLoader.test.ts @@ -1,7 +1,7 @@ import path from 'path'; import { mock, mockReset } from 'jest-mock-extended'; import { Readable } from 'stream'; -import { BasePackageLoader } from '../../src/loader/BasePackageLoader'; +import { BasePackageLoader, SafeMode } from '../../src/loader/BasePackageLoader'; import { LoadStatus } from '../../src/loader/PackageLoader'; import { PackageDB } from '../../src/db'; import { PackageCache } from '../../src/cache'; @@ -10,6 +10,7 @@ import { CurrentBuildClient } from '../../src/current'; import { loggerSpy } from '../testhelpers'; import fs from 'fs-extra'; import { VirtualPackage } from '../../src/package'; +import { cloneDeep } from 'lodash'; describe('BasePackageLoader', () => { let loader: BasePackageLoader; @@ -1379,6 +1380,108 @@ describe('BasePackageLoader', () => { expect(result[1]).toEqual({ name: 'some.ig', version: '2.3.4' }); expect(result[2]).toEqual({ name: 'some.ig', version: '3.4.5' }); }); + + const setupSafeModeTest = (safeMode: SafeMode): BasePackageLoader => { + const smLoader = new BasePackageLoader( + packageDBMock, + packageCacheMock, + registryClientMock, + currentBuildClientMock, + { safeMode, log: loggerSpy.log } + ); + smLoader.getPackageLoadStatus = jest.fn().mockReturnValueOnce(LoadStatus.NOT_LOADED); + // Virtual Package + const vPackMock = mock(); + vPackMock.getPackageJSON.mockReturnValue({ name: 'some.ig', version: '1.2.3' }); + vPackMock.registerResources.mockResolvedValue(); + packageDBMock.getPackageStats + .calledWith('some.ig', '1.2.3') + .mockReturnValue({ name: 'some.ig', version: '1.2.3', resourceCount: 0 }); + smLoader.loadVirtualPackage(vPackMock); + // Normal (non-virtual) Package + packageCacheMock.getResourceAtPath + .calledWith('/local/package/package.json') + .mockReturnValue({ name: 'some.other.ig', version: '3.4.5' }); + + packageDBMock.findPackageInfos.calledWith('*').mockReturnValue([ + { + name: 'some.ig', + version: '1.2.3', + packageJSONPath: 'virtual:some.ig#1.2.3:package.json' + }, + { + name: 'some.other.ig', + version: '3.4.5', + packageJSONPath: '/local/package/package.json' + } + ]); + + return smLoader; + }; + + it('should return modified results on second find when safe mode is OFF', () => { + const unsafeLoader = setupSafeModeTest(SafeMode.OFF); + + // Initial result should be as expected + const result = unsafeLoader.findPackageJSONs('*'); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ name: 'some.ig', version: '1.2.3' }); + expect(result[1]).toEqual({ name: 'some.other.ig', version: '3.4.5' }); + + // Modify the package JSONs. When safe mode is OFF, these modifications will stick since they're cached. + result[0].modified = true; + result[1].version = '6.7.8'; + + // Get them again and confirm results are modified. + const result2 = unsafeLoader.findPackageJSONs('*'); + expect(result2).toHaveLength(2); + expect(result2[0]).toEqual({ name: 'some.ig', version: '1.2.3', modified: true }); + expect(result2[1]).toEqual({ name: 'some.other.ig', version: '6.7.8' }); + }); + + it('should return unmodified results on second find when safe mode is CLONE', () => { + const cloneLoader = setupSafeModeTest(SafeMode.CLONE); + + // Initial result should be as expected + const result = cloneLoader.findPackageJSONs('*'); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ name: 'some.ig', version: '1.2.3' }); + expect(result[1]).toEqual({ name: 'some.other.ig', version: '3.4.5' }); + + // Modify the package JSONs. When safe mode is CLONE, these modifications should not affect future calls. + result[0].modified = true; + result[1].version = '6.7.8'; + + // Get them again and confirm results are not modified. + const result2 = cloneLoader.findPackageJSONs('*'); + expect(result2).toHaveLength(2); + expect(result2[0]).toEqual({ name: 'some.ig', version: '1.2.3' }); + expect(result2[1]).toEqual({ name: 'some.other.ig', version: '3.4.5' }); + }); + + it('should throw on attempted modifications when safe mode is FREEZE', () => { + const frozenLoader = setupSafeModeTest(SafeMode.FREEZE); + + // Initial result should be as expected + const result = frozenLoader.findPackageJSONs('*'); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ name: 'some.ig', version: '1.2.3' }); + expect(result[1]).toEqual({ name: 'some.other.ig', version: '3.4.5' }); + + // Modify the package JSONs. When safe mode is FREEZE, these modifications should throw an error. + expect(() => (result[0].modified = true)).toThrow(/Cannot add property/); + expect(() => (result[1].version = '6.7.8')).toThrow(/Cannot assign to read only property/); + + // But end users can clone the results if they need/want to make modifications + cloneDeep(result[0]).modified = true; + cloneDeep(result[1]).version = '6.7.8'; + + // Get them again and confirm results are not modified. + const result2 = frozenLoader.findPackageJSONs('*'); + expect(result2).toHaveLength(2); + expect(result2[0]).toEqual({ name: 'some.ig', version: '1.2.3' }); + expect(result2[1]).toEqual({ name: 'some.other.ig', version: '3.4.5' }); + }); }); describe('#findPackageJSON', () => { @@ -1428,6 +1531,98 @@ describe('BasePackageLoader', () => { expect(result).toEqual({ name: 'some.ig', version: '1.2.3', date: '20240824230227' }); }); + const setupSafeModeTest = (safeMode: SafeMode): BasePackageLoader => { + const smLoader = new BasePackageLoader( + packageDBMock, + packageCacheMock, + registryClientMock, + currentBuildClientMock, + { safeMode, log: loggerSpy.log } + ); + packageDBMock.findPackageInfo.calledWith('some.ig', '1.2.3').mockReturnValue({ + name: 'some.ig', + version: '1.2.3', + packageJSONPath: '/some/package/package.json' + }); + packageCacheMock.getResourceAtPath.calledWith('/some/package/package.json').mockReturnValue({ + name: 'some.ig', + version: '1.2.3' + }); + return smLoader; + }; + + it('should return modified result on second find when safe mode is OFF', () => { + const unsafeLoader = setupSafeModeTest(SafeMode.OFF); + + // Initial result should be as expected + const result = unsafeLoader.findPackageJSON('some.ig', '1.2.3'); + expect(result).toEqual({ + name: 'some.ig', + version: '1.2.3' + }); + + // Modify the package JSON. When safe mode is OFF, these modifications will stick since it's cached. + result.modified = true; + result.version = '4.5.6'; + + // Get it again and confirm result is modified. + const result2 = unsafeLoader.findPackageJSON('some.ig', '1.2.3'); + expect(result2).toEqual({ + name: 'some.ig', + version: '4.5.6', + modified: true + }); + }); + + it('should return unmodified result on second find when safe mode is CLONE', () => { + const cloneLoader = setupSafeModeTest(SafeMode.CLONE); + + // Initial result should be as expected + const result = cloneLoader.findPackageJSON('some.ig', '1.2.3'); + expect(result).toEqual({ + name: 'some.ig', + version: '1.2.3' + }); + + // Modify the package JSON. When safe mode is CLONE, these modifications should not affect future calls. + result.modified = true; + result.version = '4.5.6'; + + // Get it again and confirm result is not modified. + const result2 = cloneLoader.findPackageJSON('some.ig', '1.2.3'); + expect(result2).toEqual({ + name: 'some.ig', + version: '1.2.3' + }); + }); + + it('should throw on attempted modifications when safe mode is FREEZE', () => { + const frozenLoader = setupSafeModeTest(SafeMode.FREEZE); + + // Initial result should be as expected + const result = frozenLoader.findPackageJSON('some.ig', '1.2.3'); + expect(result).toEqual({ + name: 'some.ig', + version: '1.2.3' + }); + + // Modify the package JSON. When safe mode is FREEZE, these modifications should throw an error. + expect(() => (result.modified = true)).toThrow(/Cannot add property/); + expect(() => (result.version = '6.7.8')).toThrow(/Cannot assign to read only property/); + + // But end users can clone the result if they need/want to make modifications + const clone = cloneDeep(result); + clone.modified = true; + clone.version = '6.7.8'; + + // Get it again and confirm result is not modified. + const result2 = frozenLoader.findPackageJSON('some.ig', '1.2.3'); + expect(result2).toEqual({ + name: 'some.ig', + version: '1.2.3' + }); + }); + it('should return undefined when the info does not contain a packageJSONPath', () => { const name = 'some.ig'; const version = '1.2.3'; @@ -1717,6 +1912,190 @@ describe('BasePackageLoader', () => { expect(vPackMock.getResourceByKey).toHaveBeenCalledTimes(2); // still just 2 expect(packageCacheMock.getResourceAtPath).toHaveBeenCalledTimes(1); // still just 1 }); + + const setupSafeModeTest = (safeMode: SafeMode): BasePackageLoader => { + const smLoader = new BasePackageLoader( + packageDBMock, + packageCacheMock, + registryClientMock, + currentBuildClientMock, + { safeMode, log: loggerSpy.log } + ); + + smLoader.getPackageLoadStatus = jest.fn().mockReturnValueOnce(LoadStatus.NOT_LOADED); + // Virtual Package + const vPackMock = mock(); + vPackMock.getPackageJSON.mockReturnValue({ name: 'some.ig', version: '1.2.3' }); + vPackMock.registerResources.mockResolvedValue(); + vPackMock.getResourceByKey.calledWith('firstResource.json').mockReturnValue({ + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3' + }); + packageDBMock.getPackageStats + .calledWith('some.ig', '1.2.3') + .mockReturnValue({ name: 'some.ig', version: '1.2.3', resourceCount: 1 }); + smLoader.loadVirtualPackage(vPackMock); + // Resource from normal (non-virtual) Package + packageCacheMock.getResourceAtPath + .calledWith('/some/package/second-thing.json') + .mockReturnValue({ + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3' + }); + + const resourceInfos = [ + { + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3', + resourcePath: 'virtual:some.ig#1.2.3:firstResource.json' + }, + { + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3', + resourcePath: '/some/package/second-thing.json' + } + ]; + packageDBMock.findResourceInfos.calledWith('*').mockReturnValue(resourceInfos); + + return smLoader; + }; + + it('should return modified results on second find when safe mode is OFF', () => { + const unsafeLoader = setupSafeModeTest(SafeMode.OFF); + + // Initial result should be as expected + const result = unsafeLoader.findResourceJSONs('*'); + expect(result).toEqual([ + { + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3' + }, + { + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3' + } + ]); + + // Modify the resources. When safe mode is OFF, these modifications will stick since they're cached. + result[0].modified = true; + result[1].version = '3.4.5'; + + // Get them again and confirm results are modified. + const result2 = unsafeLoader.findResourceJSONs('*'); + expect(result2).toEqual([ + { + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3', + modified: true + }, + { + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '3.4.5' + } + ]); + }); + + it('should return unmodified results on second find when safe mode is CLONE', () => { + const cloneLoader = setupSafeModeTest(SafeMode.CLONE); + + // Initial result should be as expected + const result = cloneLoader.findResourceJSONs('*'); + expect(result).toEqual([ + { + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3' + }, + { + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3' + } + ]); + + // Modify the resources. When safe mode is CLONE, these modifications should not affect future calls. + result[0].modified = true; + result[1].version = '3.4.5'; + + // Get them again and confirm results are not modified. + const result2 = cloneLoader.findResourceJSONs('*'); + expect(result2).toEqual([ + { + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3' + }, + { + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3' + } + ]); + }); + + it('should throw on attempted modifications when safe mode is FREEZE', () => { + const frozenLoader = setupSafeModeTest(SafeMode.FREEZE); + + // Initial result should be as expected + const result = frozenLoader.findResourceJSONs('*'); + expect(result).toEqual([ + { + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3' + }, + { + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3' + } + ]); + + // Modify the resources. When safe mode is FREEZE, these modifications should throw an error. + expect(() => (result[0].modified = true)).toThrow(/Cannot add property/); + expect(() => (result[1].version = '3.4.5')).toThrow(/Cannot assign to read only property/); + + // But end users can clone the results if they need/want to make modifications + cloneDeep(result[0]).modified = true; + cloneDeep(result[1]).version = '3.4.5'; + + // Get them again and confirm results are not modified. + const result2 = frozenLoader.findResourceJSONs('*'); + expect(result2).toEqual([ + { + id: '1', + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3' + }, + { + id: '2', + name: 'secondResource', + resourceType: 'CodeSystem', + version: '1.2.3' + } + ]); + }); }); describe('#findResourceJSON', () => { @@ -1876,6 +2255,81 @@ describe('BasePackageLoader', () => { expect(vPackMock.getResourceByKey).toHaveBeenCalledTimes(1); // still 1 }); + const setupSafeModeTest = (safeMode: SafeMode): BasePackageLoader => { + const smLoader = new BasePackageLoader( + packageDBMock, + packageCacheMock, + registryClientMock, + currentBuildClientMock, + { safeMode, log: loggerSpy.log } + ); + + packageDBMock.findResourceInfo.calledWith('firstResource').mockReturnValue({ + name: 'firstResource', + resourceType: 'StructureDefinition', + version: '1.2.3', + resourcePath: '/some/package/first-thing.json' + }); + packageCacheMock.getResourceAtPath + .calledWith('/some/package/first-thing.json') + .mockReturnValue({ id: 'first-thing', version: '1.2.3' }); + + return smLoader; + }; + + it('should return modified results on second find when safe mode is OFF', () => { + const unsafeLoader = setupSafeModeTest(SafeMode.OFF); + + // Initial result should be as expected + const result = unsafeLoader.findResourceJSON('firstResource'); + expect(result).toEqual({ id: 'first-thing', version: '1.2.3' }); + + // Modify the resource. When safe mode is OFF, these modifications will stick since they're cached. + result.modified = true; + result.version = '3.4.5'; + + // Get it again and confirm result is modified. + const result2 = unsafeLoader.findResourceJSON('firstResource'); + expect(result2).toEqual({ id: 'first-thing', version: '3.4.5', modified: true }); + }); + + it('should return unmodified results on second find when safe mode is CLONE', () => { + const cloneLoader = setupSafeModeTest(SafeMode.CLONE); + + // Initial result should be as expected + const result = cloneLoader.findResourceJSON('firstResource'); + expect(result).toEqual({ id: 'first-thing', version: '1.2.3' }); + + // Modify the resource. When safe mode is CLONE, these modifications should not affect future calls. + result.modified = true; + result.version = '3.4.5'; + + // Get it again and confirm result is not modified. + const result2 = cloneLoader.findResourceJSON('firstResource'); + expect(result2).toEqual({ id: 'first-thing', version: '1.2.3' }); + }); + + it('should throw on attempted modifications when safe mode is FREEZE', () => { + const frozenLoader = setupSafeModeTest(SafeMode.FREEZE); + + // Initial result should be as expected + const result = frozenLoader.findResourceJSON('firstResource'); + expect(result).toEqual({ id: 'first-thing', version: '1.2.3' }); + + // Modify the resource. When safe mode is FREEZE, these modifications should throw an error. + expect(() => (result.modified = true)).toThrow(/Cannot add property/); + expect(() => (result.version = '3.4.5')).toThrow(/Cannot assign to read only property/); + + // But end users can clone the result if they need/want to make modifications + const clone = cloneDeep(result); + clone.modified = true; + clone.version = '3.4.5'; + + // Get it again and confirm result is not modified. + const result2 = frozenLoader.findResourceJSON('firstResource'); + expect(result2).toEqual({ id: 'first-thing', version: '1.2.3' }); + }); + it('should return undefined when the info does not contain a resourcePath', () => { packageDBMock.findResourceInfo.calledWith('first-thing').mockReturnValueOnce({ name: 'firstResource',