diff --git a/README.md b/README.md
index acf7f4ba..8a3b11fb 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ This server provides a set of server-side services that are useful for the FHIR
## Services useful the community as a whole
-* [TX Registry](registry/readme.md) - **Terminology System Registry** as [described by the terminology ecosystem specification](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig)(as running at http://tx.fhir.org/tx-reg)
+* [TX Registry](registry/readme.md) - **Terminology System Registry** as [described by the terminology ecosystem specification](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig) (as running at http://tx.fhir.org/tx-reg)
* [Package server](packages/readme.md) - **NPM-style FHIR package registry** with search, versioning, and downloads, consistent with the FHIR NPM Specification (as running at http://packages2.fhir.org/packages)
* [XIG server](xig/readme.md) - **Comprehensive FHIR IG analytics** with resource breakdowns by version, authority, and realm (as running at http://packages2.fhir.org/packages)
* [Publisher](publisher/readme.md) - FHIR publishing services (coming)
@@ -43,6 +43,10 @@ There are 4 executable programs:
Unless you're developing, you only need the first two
+FHIRsmith is open source - see below, and you're welcome to use it for any kind of use. Note,
+though, that if you support FHIRsmith commercially as part of a managed service or product, you
+are required to be a Commercial Partner of HL7 - see (link to be provided).
+
### Quick Start
* Install FHIRSmith (using docker, or an NPM release, or just get the code by git)
diff --git a/security.md b/security.md
index e0a72c3e..586227a0 100644
--- a/security.md
+++ b/security.md
@@ -29,4 +29,7 @@ A typical NGINX configuration would be:
limit_conn perip 50;
limit_conn perserver 500;
```
+## SSL
+
+This server doesn't provide SSL support - use an NGINX reverse proxy for that.
diff --git a/stats.js b/stats.js
index de865822..fdeedeb7 100644
--- a/stats.js
+++ b/stats.js
@@ -118,7 +118,7 @@ class ServerStats {
if (this.taskMap.size == 0) {
return "";
}
- let html = '
';
+ let html = '';
html += "| Background Task | Status | Frequency | Last Seen |
";
for (let m of this.taskMap.keys()) {
let mm = this.taskMap.get(m);
diff --git a/tx/cs/cs-api.js b/tx/cs/cs-api.js
index ca5a2cbb..bc7a54f3 100644
--- a/tx/cs/cs-api.js
+++ b/tx/cs/cs-api.js
@@ -673,11 +673,13 @@ class CodeSystemProvider {
/**
* register the concept maps that are implicitly defined as part of the code system
*
+ * @param {ConceptMap} map the map (this will have been returned from findImplicitConceptMap)
* @param {Coding} coding the coding to translate
- * @param {String} target
- * @returns {CodeTranslation[]} the list of translations
+ * @param {String} target the target code system
+ * @param {boolean} reverse - if the translation is being run backwards
+ * @returns {CodeTranslation[]} the list of translations, each CodeTranslation has map, code, system, version, display, and relationship
*/
- async getTranslations(coding, target) { return null;}
+ async getTranslations(map, coding, target, reverse) { return null;}
// ==== Parameter checking methods =========
_ensureLanguages(param) {
@@ -853,7 +855,7 @@ class CodeSystemFactoryProvider {
}
/**
- * see comemnts for registerSupplements()
+ * see comments for registerSupplements()
*
* @param {CodeSystem} supplement - the supplement to flesh out
* @returns void
@@ -865,6 +867,8 @@ class CodeSystemFactoryProvider {
/**
* build and return a known concept map from the URL, if there is one.
*
+ * the conceptmap is never visible to a user; if it has an implicitSource, then
+ * provider.getTranslations will be called when it's actually used
* @param url
* @param version
* @returns {ConceptMap}
diff --git a/tx/cs/cs-omop.js b/tx/cs/cs-omop.js
index 3349b1f5..5933852d 100644
--- a/tx/cs/cs-omop.js
+++ b/tx/cs/cs-omop.js
@@ -660,8 +660,10 @@ class OMOPServices extends BaseCSServices {
}
// Translation support
- async getTranslations(coding, target) {
-
+ async getTranslations(map, coding, target) {
+ if (map == null) {
+ return;
+ }
const vocabId = getVocabId(target);
if (vocabId === -1) {
@@ -680,7 +682,7 @@ class OMOPServices extends BaseCSServices {
reject(err);
} else {
const translations = rows.map(row => ({
- uri: target,
+ system: target,
code: row.concept_code,
display: row.concept_name,
relationship: 'equivalent',
diff --git a/tx/cs/cs-rxnorm.js b/tx/cs/cs-rxnorm.js
index 2f034833..82badecd 100644
--- a/tx/cs/cs-rxnorm.js
+++ b/tx/cs/cs-rxnorm.js
@@ -395,24 +395,24 @@ class RxNormServices extends CodeSystemProvider {
} else if (this.rels.includes(prop)) {
if (value.startsWith('CUI:')) {
const cui = value.substring(4);
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI1 FROM rxnrel WHERE REL = $rel AND RXCUI2 = $cui2)))`;
+ sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI2 FROM rxnrel WHERE REL = $rel AND RXCUI1 = $cui2)))`;
params.rel = this.#sqlWrapString(prop);
params.cui2 = this.#sqlWrapString(cui);
} else if (value.startsWith('AUI:')) {
const aui = value.substring(4);
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI1 FROM rxnrel WHERE REL = $rel AND RXAUI2 = $aui2)))`;
+ sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE REL = $rel AND RXAUI1 = $aui2)))`;
params.rel = this.#sqlWrapString(prop);
params.aui2 = this.#sqlWrapString(aui);
}
} else if (this.reltypes.includes(prop)) {
if (value.startsWith('CUI:')) {
const cui = value.substring(4);
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI1 FROM rxnrel WHERE RELA = $rela AND RXCUI2 = $cui2)))`;
+ sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI2 FROM rxnrel WHERE RELA = $rela AND RXCUI1 = $cui2)))`;
params.rela = this.#sqlWrapString(prop);
params.cui2 = this.#sqlWrapString(cui);
} else if (value.startsWith('AUI:')) {
const aui = value.substring(4);
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI1 FROM rxnrel WHERE RELA = $rela AND RXAUI2 = $aui2)))`;
+ sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE RELA = $rela AND RXAUI1 = $aui2)))`;
params.rela = this.#sqlWrapString(prop);
params.aui2 = this.#sqlWrapString(aui);
}
@@ -498,8 +498,6 @@ class RxNormServices extends CodeSystemProvider {
}
async filterSize(filterContext, set) {
-
-
if (!set.executed) {
await this.#executeFilter(set);
}
diff --git a/tx/cs/cs-snomed.js b/tx/cs/cs-snomed.js
index 814e08b0..d4f9fe81 100644
--- a/tx/cs/cs-snomed.js
+++ b/tx/cs/cs-snomed.js
@@ -12,6 +12,7 @@ const {
const {DesignationUse} = require("../library/designations");
const {BaseCSServices} = require("./cs-base");
const {formatDateMMDDYYYY} = require("../../library/utilities");
+const {ConceptMap} = require("../library/conceptmap");
// Context kinds matching Pascal enum
const SnomedProviderContextKind = {
@@ -1213,6 +1214,64 @@ class SnomedProvider extends BaseCSServices {
(cd.use.code === '900000000000013009' || cd.use.code === '900000000000003001');
}
+ async getTranslations(map, coding, target, reverse) {
+ if (!map || (target && target !== this.system()) || reverse) {
+ return [];
+ }
+ let ref = this.sct.concepts.findConcept(map.id);
+ if (!ref.found) {
+ return [];
+ }
+ let rref = this.sct.refSetIndex.getRefSetByConcept(ref.index);
+ if (rref == -1) {
+ return [];
+ }
+ let refSetRecord = this.sct.refSetIndex.getReferenceSet(rref);
+ let members = this.sct.refSetMembers.getMembers(refSetRecord.membersByRef);
+ let srcConcept = this.sct.concepts.findConcept(coding.code);
+ if (!srcConcept.found) {
+ return [];
+ }
+
+ let result = [];
+ let L = 0;
+ let H = members.length - 1;
+ while (L <= H) {
+ const I = Math.floor((L + H) / 2);
+ const ref = members[I].ref;
+ if (ref < srcConcept.index) {
+ L = I + 1;
+ } else if (ref > srcConcept.index) {
+ H = I - 1;
+ } else {
+ // Found — but scan left for first match in case of duplicates
+ let first = I;
+ while (first > 0 && members[first - 1].ref === srcConcept.index) {
+ first--;
+ }
+ // Process all matching members
+ for (let i = first; i < members.length && members[i].ref === srcConcept.index; i++) {
+ let values = this.sct.refs.getReferences(members[i].values);
+ if (values && values.length >= 1) {
+ let tgtId = String(this.sct.concepts.getConceptId(values[0]));
+ let ct = {
+ map: map.vurl,
+ code: tgtId,
+ system: this.system(),
+ version : this.version(),
+ display: await this.display(tgtId),
+ relationship: map.jsonObj.relationship
+ }
+ result.push(ct);
+ }
+ }
+ break;
+ }
+ }
+
+ return result;
+ }
+
}
/**
@@ -1339,7 +1398,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
return null;
}
let rref = this.snomedServices.refSetIndex.getRefSetByConcept(ref.index);
- if (rref == 0) {
+ if (rref == -1) {
return null;
}
return {
@@ -1448,6 +1507,59 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
return edition + ' ' + formatDateMMDDYYYY(match[2].substring(4, 6) + match[2].substring(6, 8) + match[2].substring(0, 4));
}
+
+ async findImplicitConceptMap(url, version) {
+ if (version && (version !== this.version())) {
+ return null;
+ }
+ if (!url || !url.startsWith(this.system()+"?fhir_cm=")) {
+ return null;
+ }
+ let id = url.substring(url.indexOf("=")+1);
+ if (['900000000000523009', '900000000000526001', '900000000000527005', '900000000000530003'].includes(id)) {
+ let name = '';
+ let relationship = '';
+ switch (id) {
+ case '900000000000523009':
+ name = 'POSSIBLY EQUIVALENT TO';
+ relationship = 'inexact';
+ break;
+ case '900000000000526001':
+ name = 'REPLACED BY';
+ relationship = 'equivalent';
+ break;
+ case '900000000000527005':
+ name = 'SAME AS';
+ relationship = 'equal';
+ break;
+ case '900000000000530003':
+ name = 'ALTERNATIVE';
+ relationship = 'inexact';
+ break;
+ }
+ let cm = {
+ resourceType: 'ConceptMap',
+ internalSource : this,
+ relationship: relationship,
+ id : id,
+ url: `${this.system}?fhir_cm=${id}`,
+ version: this.version(),
+ name: `SNOMED CT ${name} Concept Map`,
+ description: `The concept map implicitly defined by the ${name} Association Reference Set`,
+ copyright: 'This value set includes content from SNOMED CT, which is copyright © 2002+ International Health Terminology Standards Development Organisation (SNOMED International), and distributed by agreement between SNOMED International and HL7',
+ status: 'active',
+ sourceUri: `${this.system}?fhir_vs`,
+ targetUri: `${this.system}?fhir_vs`,
+ group: [{
+ source: 'http://snomed.info/sct',
+ target: 'http://snomed.info/sct'
+ }]
+ }
+ return new ConceptMap(cm);
+ } else {
+ return null;
+ }
+ }
}
function getEditionName(edition) {
diff --git a/tx/library/conceptmap.js b/tx/library/conceptmap.js
index 89e32f02..ae420235 100644
--- a/tx/library/conceptmap.js
+++ b/tx/library/conceptmap.js
@@ -148,6 +148,32 @@ class ConceptMap extends CanonicalResource {
}
return result;
}
+
+ listTranslationsReverse(coding, targetScope, sourceSystem) {
+ let result = [];
+ let vurl = VersionUtilities.vurl(coding.system, coding.version);
+
+ let all = this.canonicalMatches(targetScope, this.targetScope);
+ for (const g of this.jsonObj.group || []) {
+ const targetOk = this.canonicalMatches(vurl, g.target);
+ const sourceOk = !sourceSystem || this.canonicalMatches(sourceSystem, g.source);
+ if (all || (sourceOk && targetOk)) {
+ for (const em of g.element || []) {
+ for (const tm of em.target || []) {
+ if (tm.code === coding.code) {
+ let match = {
+ group: g,
+ match: em,
+ target: tm
+ };
+ result.push(match);
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
/**
* Gets the source scope (R5) or source system (R3/R4)
* @returns {string|undefined} Source scope/system
diff --git a/tx/library/extensions.js b/tx/library/extensions.js
index 5f9b86fb..82d1385c 100644
--- a/tx/library/extensions.js
+++ b/tx/library/extensions.js
@@ -29,7 +29,7 @@ const Extensions = {
}
},
- checkNoModifiers(element, place, name) {
+ checkNoModifiers(element, place, name, resource) {
if (!element) {
return;
}
@@ -41,11 +41,12 @@ const Extensions = {
for (const extension of element.modifierExtension) {
urls.add(extension.url);
}
+ const resId = resource ? resource : "";
const urlList = [...urls].join('\', \'');
if (urls.size > 1) {
- throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of modifier extensions '+urlList);
+ throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of modifier extensions '+urlList);
} else {
- throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of the modifier extension '+urlList);
+ throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of the modifier extension '+urlList);
}
}
return true;
diff --git a/tx/params.js b/tx/params.js
index dbb1f5a8..1703b881 100644
--- a/tx/params.js
+++ b/tx/params.js
@@ -114,11 +114,11 @@ class TxParameters {
break;
}
case 'force-valueset-version': {
- this.seeVersionRule().push(getValuePrimitive(p), true, 'override');
+ this.seeVersionRule(getValuePrimitive(p), true, 'override');
break;
}
case 'check-valueset-version': {
- this.seeVersionRule().push(getValuePrimitive(p), true, 'check');
+ this.seeVersionRule(getValuePrimitive(p), true, 'check');
break;
}
diff --git a/tx/provider.js b/tx/provider.js
index b99c2811..532c0605 100644
--- a/tx/provider.js
+++ b/tx/provider.js
@@ -228,7 +228,10 @@ class Provider {
for (let csp of this.codeSystemFactories.values()) {
if (!uris.has(csp.system())) {
uris.add(csp.system());
- await csp.findImplicitConceptMap(url, version);
+ let cm = await csp.findImplicitConceptMap(url, version);
+ if (cm) {
+ return cm;
+ }
}
}
}
diff --git a/tx/sct/structures.js b/tx/sct/structures.js
index 2266bdc4..7024affd 100644
--- a/tx/sct/structures.js
+++ b/tx/sct/structures.js
@@ -1177,7 +1177,7 @@ class SnomedReferenceSetIndex {
}
}
- return 0; // Not found
+ return -1; // Not found
}
count() {
diff --git a/tx/workers/expand.js b/tx/workers/expand.js
index 1d206553..5bf827e7 100644
--- a/tx/workers/expand.js
+++ b/tx/workers/expand.js
@@ -604,7 +604,7 @@ class ValueSetExpander {
async checkSource(cset, exp, filter, srcURL, ts, vsInfo) {
this.worker.deadCheck('checkSource');
- Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set');
+ Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set', srcURL);
let imp = false;
for (const u of cset.valueSet || []) {
this.worker.deadCheck('checkSource');
@@ -682,7 +682,7 @@ class ValueSetExpander {
this.worker.deadCheck('processCodes#1');
const valueSets = [];
- Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set');
+ Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set', vsSrc.vurl);
if (cset.valueSet || cset.concept || (cset.filter || []).length > 1) {
this.canBeHierarchy = false;
@@ -792,7 +792,7 @@ class ValueSetExpander {
for (const cc of cset.concept) {
this.worker.deadCheck('processCodes#3');
cds.clear();
- Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference');
+ Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl);
const cctxt = await cs.locate(cc.code, this.allAltCodes);
if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt.context)) && await this.passesFilters(cs, cctxt.context, prep, filters, 0)) {
await this.listDisplaysFromProvider(cds, cs, cctxt.context);
@@ -834,7 +834,7 @@ class ValueSetExpander {
if (!fc.value) {
throw new Issue('error', 'invalid', path + ".filter[" + i + "]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.httpLanguages, [cs.system(), fc.property, fc.op]), 'vs-invalid', 400);
}
- Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter');
+ Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
await cs.filter(prep, fc.property, fc.op, fc.value);
}
@@ -891,7 +891,7 @@ class ValueSetExpander {
this.worker.deadCheck('processCodes#1');
const valueSets = [];
- Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set');
+ Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set', vsSrc.vurl);
if (cset.valueSet || cset.concept || (cset.filter || []).length > 1) {
this.canBeHierarchy = false;
@@ -978,7 +978,7 @@ class ValueSetExpander {
for (const cc of cset.concept) {
this.worker.deadCheck('processCodes#3');
cds.clear();
- Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference');
+ Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl);
const cctxt = await cs.locate(cc.code, this.allAltCodes);
if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt)) && await this.passesFilters(cs, cctxt, prep, filters, 0)) {
if (filter.passesDesignations(cds) || filter.passes(cc.code)) {
@@ -1007,7 +1007,7 @@ class ValueSetExpander {
for (let fc of cset.filter) {
this.worker.deadCheck('processCodes#4a');
- Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter');
+ Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
await cs.filter(prep, fc.property, fc.op, fc.value);
}
@@ -1157,8 +1157,8 @@ class ValueSetExpander {
this.totalStatus = 'uninitialised';
this.total = 0;
- Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet');
- Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet');
+ Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
+ Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
this.worker.seeValueSet(source, this.params);
this.valueSet = source;
@@ -1272,7 +1272,7 @@ class ValueSetExpander {
let vsInfo = this.scanValueSet(source.jsonObj.compose);
try {
- if (source.jsonObj.compose && Extensions.checkNoModifiers(source.jsonObj.compose, 'ValueSetExpander.Expand', 'compose')
+ if (source.jsonObj.compose && Extensions.checkNoModifiers(source.jsonObj.compose, 'ValueSetExpander.Expand', 'compose', source.vurl)
&& this.worker.checkNoLockedDate(source.url, source.jsonObj.compose)) {
await this.handleCompose(source, filter, exp, notClosed, vsInfo);
}
diff --git a/tx/workers/related.js b/tx/workers/related.js
index 01abddd8..55b5e382 100644
--- a/tx/workers/related.js
+++ b/tx/workers/related.js
@@ -205,12 +205,12 @@ class RelatedWorker extends TerminologyWorker {
if (!thisC) {
return this.makeOutcome("indeterminate", `The ValueSet ${thisVS.vurl} has no compose`);
}
- Extensions.checkNoModifiers(thisC, 'RelatedWorker.doRelated', 'compose')
+ Extensions.checkNoModifiers(thisC, 'RelatedWorker.doRelated', 'compose', thisVS.vurl)
this.checkNoLockedDate(thisVS.vurl, thisC);
if (!otherC) {
return this.makeOutcome("indeterminate", `The ValueSet ${otherVS.vurl} has no compose`);
}
- Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose')
+ Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose', otherVS.vurl)
this.checkNoLockedDate(otherVS.vurl, otherC);
let systems = new Map(); // tracks whether they are version dependent or not
diff --git a/tx/workers/translate.js b/tx/workers/translate.js
index 9b839889..cd814453 100644
--- a/tx/workers/translate.js
+++ b/tx/workers/translate.js
@@ -113,6 +113,7 @@ class TranslateWorker extends TerminologyWorker {
let targetScope = null;
let sourceScope = null;
let targetSystem = null;
+ let reverse = false;
// Get the source coding
// Accept both R5 names (sourceCoding, sourceCodeableConcept, sourceCode/sourceSystem)
@@ -145,10 +146,32 @@ class TranslateWorker extends TerminologyWorker {
'system parameter is required when using code/sourceCode', null, 400);
}
const version = params.has('sourceVersion') ? params.get('sourceVersion') : params.get('version');
+ coding = {system, version, code};
+ } else if (params.has('targetCoding')) {
+ reverse = true;
+ coding = params.get('targetCoding');
+ } else if (params.has('targetCodeableConcept')) {
+ reverse = true;
+ const cc = params.get('targetCodeableConcept');
+ if (cc.coding && cc.coding.length > 0) {
+ coding = cc.coding[0]; // Use first coding
+ } else {
+ throw new Issue('error', 'invalid', null, null,
+ 'sourceCodeableConcept must contain at least one coding', null, 400);
+ }
+ } else if (params.has('targetCode')) {
+ reverse = true;
+ const code = params.get('targetCode');
+ const system = params.get('targetSystem');
+ if (!system) {
+ throw new Issue('error', 'invalid', null, null,
+ 'targetSystem parameter is required when using targetCode', null, 400);
+ }
+ const version = params.get('targetVersion');
coding = { system, version, code };
} else {
throw new Issue('error', 'invalid', null, null,
- 'Must provide sourceCode (with system), sourceCoding, or sourceCodeableConcept', null, 400);
+ 'Must provide sourceCode+(source)system, sourceCoding, or sourceCodeableConcept, or targetCode+targetSystem), targetCoding, or targetCodeableConcept', null, 400);
}
// Get the concept map
@@ -173,21 +196,33 @@ class TranslateWorker extends TerminologyWorker {
if (params.has('targetScope')) {
targetScope = params.get('targetScope');
}
- if (params.has('targetSystem')) {
- targetSystem = params.get('targetSystem');
+ if (reverse) {
+ if (params.has('sourceSystem')) {
+ targetSystem = params.get('sourceSystem');
+ }
+ } else {
+ if (params.has('targetSystem')) {
+ targetSystem = params.get('targetSystem');
+ }
}
-
+ let explicit = true;
// If no explicit concept map, we need to find one based on source/target
if (conceptMaps.length == 0) {
- await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
- await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem, coding.code);
+ explicit = false;
+ if (reverse) {
+ await this.findConceptMapsInAdditionalResources(conceptMaps,targetSystem, targetScope, sourceScope, coding.system);
+ await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, targetSystem, targetScope, sourceScope, coding.system, coding.code);
+ } else {
+ await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
+ await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem, coding.code);
+ }
if (conceptMaps.length == 0) {
throw new Issue('error', 'not-found', null, null, 'No suitable ConceptMaps found for the specified source and target', null, 404);
}
}
// Perform the translation
- const result = await this.doTranslate(conceptMaps, coding, targetScope, targetSystem, txp);
+ const result = await this.doTranslate(conceptMaps, coding, targetScope, targetSystem, txp, reverse, explicit);
return res.status(200).json(result);
}
@@ -287,7 +322,7 @@ class TranslateWorker extends TerminologyWorker {
return result;
}
- translateUsingGroups(cm, coding, targetScope, targetSystem, params, output) {
+ translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, output, explicit) {
let result = false;
const matches = cm.listTranslations(coding, targetScope, targetSystem);
if (matches.length > 0) {
@@ -295,7 +330,13 @@ class TranslateWorker extends TerminologyWorker {
const g = match.group;
const em = match.match;
for (const map of em.target || []) {
- if (['null', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower', 'specializes', 'inexact'].includes(map.relationship)) {
+ let ok = false;
+ if (map.equivalence) { // R4 mode
+ ok = ['null', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower', 'specializes', 'inexact'].includes(map.equivalence);
+ } else {
+ ok = ['null', 'related-to', 'equivalent', 'source-is-narrower-than-target', 'source-is-broader-than-target'].includes(map.relationship);
+ }
+ if (ok) {
result = true;
const outcome = {
@@ -303,22 +344,119 @@ class TranslateWorker extends TerminologyWorker {
code: map.code
};
+ if (!this.hasMatch(output, outcome)) {
+ const matchParts = [];
+ matchParts.push({
+ name: 'concept',
+ valueCoding: outcome
+ });
+ matchParts.push({
+ name: 'relationship',
+ valueCode: map.relationship
+ });
+ // equivalence vs relationship will be sorted out in the version transform for parameters
+ if (map.equivalence) {
+ matchParts.push({
+ name: 'equivalence',
+ valueCode: map.equivalence
+ });
+ }
+ if (map.comment) {
+ matchParts.push({
+ name: 'message',
+ valueString: map.comment
+ });
+ }
+ for (const prod of map.product || []) {
+ const productParts = [];
+ productParts.push({
+ name: 'element',
+ valueString: prod.property
+ });
+ productParts.push({
+ name: 'concept',
+ valueCoding: {
+ system: prod.system,
+ code: prod.value
+ }
+ });
+ matchParts.push({
+ name: 'product',
+ part: productParts
+ });
+ }
+ if (!explicit) {
+ matchParts.push({
+ name: 'sourceMap',
+ valueCanonical: cm.vurl
+ });
+ }
+ output.push({
+ name: 'match',
+ part: matchParts
+ });
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, output, explicit) {
+ let result = false;
+ const matches = cm.listTranslationsReverse(coding, targetScope, targetSystem);
+ if (matches.length > 0) {
+ for (let match of matches) {
+ const g = match.group;
+ const em = match.match;
+ const map = match.target;
+ let ok = false;
+ if (map.equivalence) { // R4 mode
+ ok = ['null', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower', 'specializes', 'inexact'].includes(map.equivalence);
+ } else {
+ ok = ['null', 'related-to', 'equivalent', 'source-is-narrower-than-target', 'source-is-broader-than-target'].includes(map.relationship);
+ }
+ if (ok) {
+ result = true;
+
+ const outcome = {
+ system: g.source,
+ code: em.code
+ };
+ const t = {
+ system: g.target,
+ code: coding.code
+ };
+
+ if (!this.hasMatch(output, outcome)) {
const matchParts = [];
matchParts.push({
- name: 'concept',
+ name: 'source',
valueCoding: outcome
});
+ matchParts.push({
+ name: 'concept',
+ valueCoding: t
+ });
matchParts.push({
name: 'relationship',
valueCode: map.relationship
});
- if (map.comments) {
+ // equivalence vs relationship will be sorted out in the version transform for parameters
+ if (map.equivalence) {
+ matchParts.push({
+ name: 'equivalence',
+ valueCode: map.equivalence
+ });
+ }
+ if (map.comment) {
matchParts.push({
name: 'message',
- valueString: map.comments
+ valueString: map.comment
});
}
- for (const prod of map.products || []) {
+ for (const prod of map.product || []) {
const productParts = [];
productParts.push({
name: 'element',
@@ -336,6 +474,12 @@ class TranslateWorker extends TerminologyWorker {
part: productParts
});
}
+ if (!explicit) {
+ matchParts.push({
+ name: 'sourceMap',
+ valueCanonical: cm.vurl
+ });
+ }
output.push({
name: 'match',
part: matchParts
@@ -347,7 +491,7 @@ class TranslateWorker extends TerminologyWorker {
return result;
}
- async translateUsingCodeSystem(cm, coding, target, params, output) {
+ async translateUsingCodeSystem(cm, coding, target, params, output, reverse, explicit) {
let result = false;
const factory = cm.jsonObj.internalSource;
let prov = await factory.build(this.opContext, []);
@@ -357,7 +501,7 @@ class TranslateWorker extends TerminologyWorker {
valueUri: prov.system() + '|' + prov.version()
});
- let translations = await prov.getTranslations(coding, target);
+ let translations = await prov.getTranslations(cm, coding, target, reverse);
if (translations.length > 0) {
result = true;
@@ -371,7 +515,7 @@ class TranslateWorker extends TerminologyWorker {
}
const outcome = {
- system: t.uri,
+ system: t.system,
code: t.code,
version: t.version,
display: t.display
@@ -392,6 +536,12 @@ class TranslateWorker extends TerminologyWorker {
valueString: t.message
});
}
+ if (!explicit) {
+ matchParts.push({
+ name: 'sourceMap',
+ valueCanonical: cm.vurl
+ });
+ }
output.push({
name: 'match',
part: matchParts
@@ -408,9 +558,11 @@ class TranslateWorker extends TerminologyWorker {
* @param {string} targetScope - Target value set scope (optional)
* @param {string} targetSystem - Target code system (optional)
* @param {Parameters} params - Full parameters object
+ * @param {boolean} reverse - Full parameters object*
+ * @param {boolean} explicit - If the concept map was named explicitly
* @returns {Object} Parameters resource with translate result
*/
- async doTranslate(conceptMaps, coding, targetScope, targetSystem, params) {
+ async doTranslate(conceptMaps, coding, targetScope, targetSystem, params, reverse, explicit) {
this.deadCheck('doTranslate');
const result = [];
@@ -419,9 +571,11 @@ class TranslateWorker extends TerminologyWorker {
let added = false;
for (const cm of conceptMaps) {
if (cm.jsonObj.internalSource) {
- added = await this.translateUsingCodeSystem(cm, coding, targetSystem, params, result) || added;
- } else {
- added = this.translateUsingGroups(cm, coding, targetScope, targetSystem, params, result) || added;
+ added = await this.translateUsingCodeSystem(cm, coding, targetSystem, params, result, reverse, explicit) || added;
+ } else if (reverse) {
+ added = this.translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, result, reverse, explicit) || added;
+ } else{
+ added = this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result, reverse, explicit) || added;
}
}
result.push({
@@ -517,6 +671,16 @@ class TranslateWorker extends TerminologyWorker {
}
}
}
+
+ hasMatch(output, outcome) {
+ for (let o of output) {
+ let c = o.part.find(x => x.name === 'concept');
+ if (c.valueCoding.code === outcome.code && c.valueCoding.system === outcome.system) {
+ return true;
+ }
+ }
+ return false;
+ }
}
module.exports = TranslateWorker;
\ No newline at end of file
diff --git a/tx/workers/validate.js b/tx/workers/validate.js
index fd2122d6..74b4818b 100644
--- a/tx/workers/validate.js
+++ b/tx/workers/validate.js
@@ -316,21 +316,21 @@ class ValueSetChecker {
}
Extensions.checkNoImplicitRules(this.valueSet, 'ValueSetChecker.prepare', 'ValueSet');
- Extensions.checkNoModifiers(this.valueSet, 'ValueSetChecker.prepare', 'ValueSet');
+ Extensions.checkNoModifiers(this.valueSet, 'ValueSetChecker.prepare', 'ValueSet', this.valueSet.vurl);
this.allValueSet = this.valueSet.url === 'http://hl7.org/fhir/ValueSet/@all';
if (this.valueSet.jsonObj.compose) {
- Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose');
- this.worker.checkNoLockedDate(this.valueSet.url, this.valueSet.jsonObj.compose)
+ Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose', this.valueSet.vurl);
+ this.worker.checkNoLockedDate(this.valueSet.vurl, this.valueSet.jsonObj.compose)
let i = 0;
for (let cc of this.valueSet.jsonObj.compose.include || []) {
- await this.prepareConceptSet('include['+i+']', cc);
+ await this.prepareConceptSet('include['+i+']', cc, this.valueSet);
i++;
}
i = 0;
for (let cc of this.valueSet.jsonObj.compose.exclude || []) {
- await this.prepareConceptSet('exclude['+i+']', cc);
+ await this.prepareConceptSet('exclude['+i+']', cc, this.valueSet);
i++;
}
}
@@ -342,9 +342,9 @@ class ValueSetChecker {
}
}
- async prepareConceptSet(desc, cc) {
+ async prepareConceptSet(desc, cc, vs) {
this.worker.deadCheck('prepareConceptSet');
- Extensions.checkNoModifiers(cc, 'ValueSetChecker.prepare', desc);
+ Extensions.checkNoModifiers(cc, 'ValueSetChecker.prepare', desc, vs.vurl);
this.worker.opContext.addNote(this.valueSet, 'Prepare ' + desc + ': "' + this.worker.renderer.displayValueSetInclude(cc) + '"', this.indentCount);
if (cc.valueSet) {
for (let u of cc.valueSet) {
@@ -374,7 +374,7 @@ class ValueSetChecker {
let i = 0;
for (let ccf of cc.filter || []) {
this.worker.deadCheck('prepareConceptSet#2');
- Extensions.checkNoModifiers(ccf, 'ValueSetChecker.prepare', desc + '.filter');
+ Extensions.checkNoModifiers(ccf, 'ValueSetChecker.prepare', desc + '.filter', this.valueSet.vurl);
if (!ccf.value) {
throw new Issue('error', 'invalid', "ValueSet.compose."+desc+".filter["+i+"]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE',
this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.HTTPLanguages, [cs.system(), ccf.property, ccf.op]), "vs-invalid").handleAsOO(400);
@@ -677,7 +677,7 @@ class ValueSetChecker {
throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.i18n.translatePlural(unused.size, 'VALUESET_SUPPLEMENT_MISSING', this.params.HTTPLanguages, [[...unused].join(',')]), 'not-found').handleAsOO(422);
}
- if (Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose')) {
+ if (Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose', this.valueSet.vurl)) {
result = false;
let determinedVersion = undefined;
if (!version) {
@@ -807,7 +807,7 @@ class ValueSetChecker {
}
}
}
- } else if (Extensions.checkNoModifiers(this.valueSet.jsonObj.expansion, 'ValueSetChecker.prepare', 'ValueSet.expansion')) {
+ } else if (Extensions.checkNoModifiers(this.valueSet.jsonObj.expansion, 'ValueSetChecker.prepare', 'ValueSet.expansion', this.valueSet.vurl)) {
let ccc = this.valueSet.findContains(system, version, code);
if (ccc === null) {
result = false;
diff --git a/tx/xversion/xv-parameters.js b/tx/xversion/xv-parameters.js
index 660bdc26..08e79684 100644
--- a/tx/xversion/xv-parameters.js
+++ b/tx/xversion/xv-parameters.js
@@ -10,7 +10,11 @@ const {VersionUtilities} = require("../../library/version-utilities");
function parametersToR5(jsonObj, sourceVersion) {
if (VersionUtilities.isR5Ver(sourceVersion)) {
- return jsonObj; // No conversion needed
+ if (jsonObj.parameter && jsonObj.parameter.find(p => p.name == 'match')) {
+ return convertResourceWithinR5(JSON.parse(JSON.stringify(jsonObj)));
+ } else {
+ return jsonObj; // No conversion needed
+ }
}
const {convertResourceFromR5} = require("./xv-resource");
@@ -59,8 +63,57 @@ function parametersR5ToR4(r5Obj) {
if (p.resource) {
p.resource = convertResourceFromR5(p.resource, "R4");
}
+ if (p.name == 'match') {
+ fixMatchParameterfor4(p);
+ }
+ }
+ return r5Obj;
+}
+
+function convertResourceWithinR5(r5Obj) {
+ for (let p of r5Obj.parameter) {
+ if (p.name == 'match') {
+ fixMatchParameterfor5(p);
+ }
}
return r5Obj;
+
+}
+
+function fixMatchParameterfor5(p) {
+ if (p.part) {
+ p.part = p.part.filter(pp => pp.name !== 'equivalence');
+ }
+}
+
+function fixMatchParameterfor4(p) {
+ if (p.part) {
+ if (!p.part.find(pp => pp.name === 'equivalence')) {
+ let rel = p.part.find(pp => pp.name === 'relationship');
+ if (rel && rel.valueCode) {
+ let pp = {name: "equivalence"};
+ switch (rel.valueCode) {
+ case 'related-to':
+ pp.valueCode = 'relatedto';
+ break;
+ case 'equivalent':
+ pp.valueCode = 'equivalent';
+ break;
+ case 'source-is-narrower-than-target':
+ pp.valueCode = 'wider';
+ break;
+ case 'source-is-broader-than-target':
+ pp.valueCode = 'narrower';
+ break;
+ case 'not-related-to':
+ pp.valueCode = 'unmatched';
+ break;
+ }
+ p.part.push(pp);
+ }
+ }
+ p.part = p.part.filter(pp => pp.name !== 'relationship');
+ }
}
function convertParameterR5ToR3(p) {