diff --git a/.npmrc b/.npmrc
index 7417ed69..78c50d20 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1 @@
-min-release-age=7
\ No newline at end of file
+min-release-age=3
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index f63016e8..91ff55ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "fhirsmith",
- "version": "0.8.6",
+ "version": "0.9.0",
"license": "BSD-3",
"dependencies": {
"axios": "^1.13.4",
@@ -30,7 +30,7 @@
"fs-extra": "^11.3.3",
"ini": "^6.0.0",
"inquirer": "^8.2.5",
- "liquidjs": "^10.24.0",
+ "liquidjs": "^10.25.5",
"lusca": "^1.7.0",
"natural": "^6.12.0",
"node-cron": "^3.0.3",
@@ -2271,14 +2271,14 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.13.5",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
- "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+ "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
- "proxy-from-env": "^1.1.0"
+ "proxy-from-env": "^2.1.0"
}
},
"node_modules/babel-jest": {
@@ -7897,10 +7897,13 @@
}
},
"node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/pstree.remy": {
"version": "1.1.8",
diff --git a/root-bare-template.html b/root-bare-template.html
index fe625e8b..7c558c2b 100644
--- a/root-bare-template.html
+++ b/root-bare-template.html
@@ -10,9789 +10,72 @@
-
-
-
-
-
-
-
-
-
+
-
[%title%]
+[%title%]
[%content%]
-
-
-
-
-
-
-
-
diff --git a/translations/Messages.properties b/translations/Messages.properties
index 326f90c1..9f8314fb 100644
--- a/translations/Messages.properties
+++ b/translations/Messages.properties
@@ -1298,7 +1298,7 @@ VIEWDEFINITION_SHOULD_HAVE_NAME = No name provided. A name is required in many c
VIEWDEFINITION_TYPE_MISMATCH = The path expression ''{0}'' does not return a value of the type ''{1}'' - found ''{2}''{3}
VIEWDEFINITION_UNABLE_TO_TYPE = Unable to determine a type (found ''{0}''){1}
VIEWDEFINITION_UNKNOWN_RESOURCE = The name ''{0}'' is not a valid resource{1}
-VS_EXP_FILTER_UNK = ValueSet ''{0}'' Filter by property ''{1}'' and op ''{2}'' is not supported yet
+VS_EXP_FILTER_UNK = ValueSet ''{0}'' Filter by property ''{1}'' and op ''{2}'' is not supported (yet?)
VS_EXP_IMPORT_CS = Cannot include value set ''{0}'' because it''s actually a code system
VS_EXP_IMPORT_CS_PINNED = Cannot include value set ''{0}'' version ''{1}'' because it''s actually a code system
VS_EXP_IMPORT_CS_PINNED_X = Cannot exclude value set ''{0}'' version ''{1}'' because it''s actually a code system
@@ -1400,7 +1400,7 @@ TEXT_LINK_NO_DATA = No data element was found in the textLink extension
TEXT_LINK_SELECTOR_INVALID = The textLink selector ''{0}'' is invalid: {1}
SD_CONTEXT_SHOULD_ELEMENT_NOT_FOUND = The element {0} is not valid
SD_CONTEXT_SHOULD_ELEMENT_NOT_FOUND_VER = The element {0} is not valid in version {1}
-FILTER_NOT_UNDERSTOOD = The filter "{0} {1} {2}" from the value set {3} was not understood in the context of {4}
+FILTER_NOT_UNDERSTOOD = The filter "{0} {1} {2}" is not understood or supported
XHTML_CONTROL_NO_SOURCE = The xhtml node at ''{0}'' does not have a class attribute to indicate its source (boilerplate, generated, or original) and this is required by the profile {1}
XHTML_XHTML_MIXED_LANG = The xhtml has some language sections ({0}), and also has content that is not in a language section
XHTML_CONTROL_NO_LANGS = The xhtml has some language sections ({0}), but language sections are prohibited in this context by the profile {1}
diff --git a/tx/cs/cs-cs.js b/tx/cs/cs-cs.js
index 3a9721c6..20d3e5ce 100644
--- a/tx/cs/cs-cs.js
+++ b/tx/cs/cs-cs.js
@@ -1224,7 +1224,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
// Handle concept/code hierarchy filters
if ((prop === 'concept' || prop === 'code')) {
- results = await this._handleConceptFilter(filterContext, op, value);
+ results = await this._handleConceptFilter(filterContext, prop, op, value);
}
// Handle child existence filter
@@ -1248,7 +1248,8 @@ class FhirCodeSystemProvider extends BaseCSServices {
}
if (!results) {
- throw new Error(`The filter ${prop} ${op} ${value} was not understood`)
+ throw new Issue('error', 'exception', null, 'FILTER_NOT_UNDERSTOOD',
+ this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);
}
// Add to filter context
if (!filterContext.filters) {
@@ -1267,15 +1268,17 @@ class FhirCodeSystemProvider extends BaseCSServices {
* @returns {Promise} Filter results
* @private
*/
- async _handleConceptFilter(filterContext, op, value) {
+ async _handleConceptFilter(filterContext, prop, op, value) {
const results = new FhirCodeSystemProviderFilterContext();
if (op === 'is-a' || op === 'descendent-of') {
// Find all descendants of the specified code
const includeRoot = (op === 'is-a');
await this._addDescendants(results, value, includeRoot);
- }
- else if (op === 'is-not-a') {
+ } else if (op === 'child-of') {
+ // Find all descendants of the specified code
+ await this._addChildren(results, value);
+ } else if (op === 'is-not-a') {
// Find all concepts that are NOT descendants of the specified code
const excludeDescendants = this.codeSystem.getDescendants(value);
const excludeSet = new Set([value, ...excludeDescendants]);
@@ -1323,6 +1326,9 @@ class FhirCodeSystemProvider extends BaseCSServices {
} catch (error) {
throw new Issue('error', 'exception', null, 'INVALID_REGEX', this.opContext.i18n.translate('INVALID_REGEX', this.opContext.langs, [value, error.message]), 'vs-invalid', 422);
}
+ } else {
+ throw new Issue('error', 'exception', null, 'FILTER_NOT_UNDERSTOOD',
+ this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);
}
return results;
@@ -1353,6 +1359,27 @@ class FhirCodeSystemProvider extends BaseCSServices {
}
}
+ /**
+ * Add immediate children of a code to the results
+ * @param {FhirCodeSystemProviderFilterContext} results - Results to add to
+ * @param {string} ancestorCode - The parent code
+ * @private
+ */
+ async _addChildren(results, parentCode) {
+ const concept = this.codeSystem.getConceptByCode(parentCode);
+ if (concept) {
+ const descendants = this.codeSystem.getChildren(parentCode);
+ for (const code of descendants) {
+ if (code !== parentCode) { // should not be
+ const concept = this.codeSystem.getConceptByCode(code);
+ if (concept) {
+ results.add(concept, 0);
+ }
+ }
+ }
+ }
+ }
+
/**
* Handle child exists filter
* @param {FilterExecutionContext} filterContext - Filter context
diff --git a/tx/library/extensions.js b/tx/library/extensions.js
index 82d1385c..f863f4f5 100644
--- a/tx/library/extensions.js
+++ b/tx/library/extensions.js
@@ -56,7 +56,13 @@ const Extensions = {
if (!resource) {
return undefined;
}
- const extensions = Array.isArray(resource) ? resource : (resource.extension || []);
+ let extensions = Array.isArray(resource) ? resource : (resource.extension || []);
+ for (let ext of extensions || []) {
+ if (ext.url === url) {
+ return getValuePrimitive(ext);
+ }
+ }
+ extensions = Array.isArray(resource) ? resource : (resource.modifierExtension || []);
for (let ext of extensions || []) {
if (ext.url === url) {
return getValuePrimitive(ext);
diff --git a/tx/library/renderer.js b/tx/library/renderer.js
index 9b0c69d8..f7512995 100644
--- a/tx/library/renderer.js
+++ b/tx/library/renderer.js
@@ -466,14 +466,15 @@ class Renderer {
li.tx(" "+ this.translate('VALUE_SET_WHERE')+" ");
li.startCommaList("and");
for (let f of inc.filter) {
- if (f.op == 'exists') {
+ let op = this.readFilterOp(f);
+ if (op == 'exists') {
if (f.value == "true") {
li.commaItem(f.property+" "+ this.translate('VALUE_SET_EXISTS'));
} else {
li.commaItem(f.property+" "+ this.translate('VALUE_SET_DOESNT_EXIST'));
}
} else {
- li.commaItem(f.property + " " + f.op + " ");
+ li.commaItem(f.property + " " + op + " ");
const loc = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, inc.system, inc.version, f.value) : null;
if (loc) {
li.ah(loc.link).tx(loc.description);
@@ -2243,6 +2244,14 @@ class Renderer {
return defn+' = ' + value;
}
}
+
+ readFilterOp(f) {
+ if (f._op) {
+ return Extensions.readString(f._op, 'http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op');
+ } else {
+ return f.op;
+ }
+ }
}
module.exports = { Renderer };
diff --git a/tx/vs/vs-vsac.js b/tx/vs/vs-vsac.js
index faced58d..f67ea601 100644
--- a/tx/vs/vs-vsac.js
+++ b/tx/vs/vs-vsac.js
@@ -17,6 +17,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
* @param {string} config.cacheFolder - Local folder for cached database
* @param {number} [config.refreshIntervalHours=24] - Hours between refresh scans
* @param {string} [config.baseUrl='http://cts.nlm.nih.gov/fhir'] - Base URL for VSAC FHIR server
+ * @param {number} [config.timeoutMs=120000] - HTTP request timeout in milliseconds
*/
constructor(config, stats) {
super();
@@ -43,7 +44,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
const authString = Buffer.from(`apikey:${this.apiKey}`).toString('base64');
this.httpClient = axios.create({
baseURL: this.baseUrl,
- timeout: 30000,
+ timeout: config.timeoutMs || 120000,
headers: {
'Accept': 'application/fhir+json',
'User-Agent': 'FHIR-ValueSet-Provider/1.0',
diff --git a/tx/workers/validate.js b/tx/workers/validate.js
index 74b4818b..aded791e 100644
--- a/tx/workers/validate.js
+++ b/tx/workers/validate.js
@@ -839,7 +839,7 @@ class ValueSetChecker {
} else {
bAdd = !unknownSystems.has(system + '|' + version);
if (bAdd) {
- let vl = await this.listVersions(system);
+ let vl = await this.worker.listVersions(system);
if (vl.length == 0) {
mid = 'UNKNOWN_CODESYSTEM_VERSION_NONE';
vn = system;
diff --git a/tx/xversion/xv-valueset.js b/tx/xversion/xv-valueset.js
index b6d1300a..7d6b8d1c 100644
--- a/tx/xversion/xv-valueset.js
+++ b/tx/xversion/xv-valueset.js
@@ -1,5 +1,6 @@
const {VersionUtilities} = require("../../library/version-utilities");
const {getValueName} = require("../../library/utilities");
+const {Extensions} = require("../library/extensions");
/**
* Converts input ValueSet to R5 format (modifies input object for performance)
@@ -13,6 +14,12 @@ function valueSetToR5(jsonObj, sourceVersion) {
if (VersionUtilities.isR5Ver(sourceVersion)) {
return jsonObj; // No conversion needed
}
+ for (const inc of jsonObj.compose.include || []) {
+ valueSetIncludeToR5(inc);
+ }
+ for (const inc of jsonObj.compose.exclude || []) {
+ valueSetIncludeToR5(inc);
+ }
if (VersionUtilities.isR4Ver(sourceVersion)) {
return jsonObj; // No conversion needed
}
@@ -26,6 +33,19 @@ function valueSetToR5(jsonObj, sourceVersion) {
throw new Error(`Unsupported FHIR version: ${sourceVersion}`);
}
+function valueSetIncludeToR5(inc) {
+ for (const filter of inc.filter || []) {
+ if (filter._op) {
+ let code = Extensions.readString(filter._op, 'http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op');
+ if (code) {
+ filter.op = code;
+ delete filter._op;
+ }
+ }
+ }
+}
+
+
/**
* Converts R5 ValueSet to target version format (clones object first)
* @param {Object} r5Obj - The R5 format ValueSet object
@@ -70,8 +90,8 @@ function valueSetR5ToR4(r5Obj) {
if (include.filter && Array.isArray(include.filter)) {
include.filter = include.filter.map(filter => {
if (filter.op && isR5OnlyFilterOperator(filter.op)) {
- // Remove R5-only operators
- return null;
+ filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
+ delete filter.op;
}
return filter;
}).filter(filter => filter !== null);
@@ -85,8 +105,8 @@ function valueSetR5ToR4(r5Obj) {
if (exclude.filter && Array.isArray(exclude.filter)) {
exclude.filter = exclude.filter.map(filter => {
if (filter.op && isR5OnlyFilterOperator(filter.op)) {
- // Remove R5-only operators
- return null;
+ filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
+ delete filter.op;
}
return filter;
}).filter(filter => filter !== null);
@@ -135,8 +155,8 @@ function valueSetR5ToR3(r5Obj) {
if (include.filter && Array.isArray(include.filter)) {
include.filter = include.filter.map(filter => {
if (filter.op && !isR3CompatibleFilterOperator(filter.op)) {
- // Remove non-R3-compatible operators
- return null;
+ filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
+ delete filter.op;
}
return filter;
}).filter(filter => filter !== null);
@@ -150,8 +170,8 @@ function valueSetR5ToR3(r5Obj) {
if (exclude.filter && Array.isArray(exclude.filter)) {
exclude.filter = exclude.filter.map(filter => {
if (filter.op && !isR3CompatibleFilterOperator(filter.op)) {
- // Remove non-R3-compatible operators
- return null;
+ filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
+ delete filter.op;
}
return filter;
}).filter(filter => filter !== null);