fhirsmith 0.8.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +9 -5
- package/package.json +1 -1
- package/packages/package-crawler.js +1 -1
- package/publisher/publisher.js +257 -218
- package/root-bare-template.html +0 -3
- package/security.md +3 -0
- package/server.js +29 -24
- package/shl/readme.md +27 -0
- package/shl/shl.js +2 -8
- package/stats.js +1 -1
- package/tx/cs/cs-api.js +8 -4
- package/tx/cs/cs-omop.js +5 -3
- package/tx/cs/cs-rxnorm.js +4 -6
- package/tx/cs/cs-snomed.js +113 -1
- package/tx/data/cpt-fragment.db +0 -0
- package/tx/data/cs-de.json +186 -0
- package/tx/data/cs-extensions.json +92 -0
- package/tx/data/cs-simple.json +130 -0
- package/tx/data/cs-supplement.json +78 -0
- package/tx/data/lang.dat +49180 -0
- package/tx/data/languages.csv +191 -0
- package/tx/data/loinc-subset.txt +75 -0
- package/tx/data/omop-fragment.db +0 -0
- package/tx/data/readme.md +43 -0
- package/tx/data/regions.csv +273 -0
- package/tx/data/rxnorm-subset.txt +22 -0
- package/tx/data/snomed-subset.txt +47 -0
- package/tx/data/ucum-essence.xml +2059 -0
- package/tx/library/conceptmap.js +26 -0
- package/tx/library/extensions.js +4 -3
- package/tx/params.js +2 -2
- package/tx/provider.js +4 -1
- package/tx/sct/structures.js +1 -1
- package/tx/workers/expand.js +10 -10
- package/tx/workers/related.js +2 -2
- package/tx/workers/translate.js +184 -20
- package/tx/workers/validate.js +10 -10
- package/tx/xversion/xv-parameters.js +54 -1
package/tx/library/conceptmap.js
CHANGED
|
@@ -148,6 +148,32 @@ class ConceptMap extends CanonicalResource {
|
|
|
148
148
|
}
|
|
149
149
|
return result;
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
listTranslationsReverse(coding, targetScope, sourceSystem) {
|
|
153
|
+
let result = [];
|
|
154
|
+
let vurl = VersionUtilities.vurl(coding.system, coding.version);
|
|
155
|
+
|
|
156
|
+
let all = this.canonicalMatches(targetScope, this.targetScope);
|
|
157
|
+
for (const g of this.jsonObj.group || []) {
|
|
158
|
+
const targetOk = this.canonicalMatches(vurl, g.target);
|
|
159
|
+
const sourceOk = !sourceSystem || this.canonicalMatches(sourceSystem, g.source);
|
|
160
|
+
if (all || (sourceOk && targetOk)) {
|
|
161
|
+
for (const em of g.element || []) {
|
|
162
|
+
for (const tm of em.target || []) {
|
|
163
|
+
if (tm.code === coding.code) {
|
|
164
|
+
let match = {
|
|
165
|
+
group: g,
|
|
166
|
+
match: em,
|
|
167
|
+
target: tm
|
|
168
|
+
};
|
|
169
|
+
result.push(match);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
151
177
|
/**
|
|
152
178
|
* Gets the source scope (R5) or source system (R3/R4)
|
|
153
179
|
* @returns {string|undefined} Source scope/system
|
package/tx/library/extensions.js
CHANGED
|
@@ -29,7 +29,7 @@ const Extensions = {
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
|
|
32
|
-
checkNoModifiers(element, place, name) {
|
|
32
|
+
checkNoModifiers(element, place, name, resource) {
|
|
33
33
|
if (!element) {
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
@@ -41,11 +41,12 @@ const Extensions = {
|
|
|
41
41
|
for (const extension of element.modifierExtension) {
|
|
42
42
|
urls.add(extension.url);
|
|
43
43
|
}
|
|
44
|
+
const resId = resource ? resource : "";
|
|
44
45
|
const urlList = [...urls].join('\', \'');
|
|
45
46
|
if (urls.size > 1) {
|
|
46
|
-
throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of modifier extensions '+urlList);
|
|
47
|
+
throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of modifier extensions '+urlList);
|
|
47
48
|
} else {
|
|
48
|
-
throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of the modifier extension '+urlList);
|
|
49
|
+
throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of the modifier extension '+urlList);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
return true;
|
package/tx/params.js
CHANGED
|
@@ -114,11 +114,11 @@ class TxParameters {
|
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
116
116
|
case 'force-valueset-version': {
|
|
117
|
-
this.seeVersionRule(
|
|
117
|
+
this.seeVersionRule(getValuePrimitive(p), true, 'override');
|
|
118
118
|
break;
|
|
119
119
|
}
|
|
120
120
|
case 'check-valueset-version': {
|
|
121
|
-
this.seeVersionRule(
|
|
121
|
+
this.seeVersionRule(getValuePrimitive(p), true, 'check');
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
124
124
|
|
package/tx/provider.js
CHANGED
|
@@ -228,7 +228,10 @@ class Provider {
|
|
|
228
228
|
for (let csp of this.codeSystemFactories.values()) {
|
|
229
229
|
if (!uris.has(csp.system())) {
|
|
230
230
|
uris.add(csp.system());
|
|
231
|
-
await csp.findImplicitConceptMap(url, version);
|
|
231
|
+
let cm = await csp.findImplicitConceptMap(url, version);
|
|
232
|
+
if (cm) {
|
|
233
|
+
return cm;
|
|
234
|
+
}
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
}
|
package/tx/sct/structures.js
CHANGED
package/tx/workers/expand.js
CHANGED
|
@@ -604,7 +604,7 @@ class ValueSetExpander {
|
|
|
604
604
|
|
|
605
605
|
async checkSource(cset, exp, filter, srcURL, ts, vsInfo) {
|
|
606
606
|
this.worker.deadCheck('checkSource');
|
|
607
|
-
Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set');
|
|
607
|
+
Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set', srcURL);
|
|
608
608
|
let imp = false;
|
|
609
609
|
for (const u of cset.valueSet || []) {
|
|
610
610
|
this.worker.deadCheck('checkSource');
|
|
@@ -682,7 +682,7 @@ class ValueSetExpander {
|
|
|
682
682
|
this.worker.deadCheck('processCodes#1');
|
|
683
683
|
const valueSets = [];
|
|
684
684
|
|
|
685
|
-
Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set');
|
|
685
|
+
Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set', vsSrc.vurl);
|
|
686
686
|
|
|
687
687
|
if (cset.valueSet || cset.concept || (cset.filter || []).length > 1) {
|
|
688
688
|
this.canBeHierarchy = false;
|
|
@@ -792,7 +792,7 @@ class ValueSetExpander {
|
|
|
792
792
|
for (const cc of cset.concept) {
|
|
793
793
|
this.worker.deadCheck('processCodes#3');
|
|
794
794
|
cds.clear();
|
|
795
|
-
Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference');
|
|
795
|
+
Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl);
|
|
796
796
|
const cctxt = await cs.locate(cc.code, this.allAltCodes);
|
|
797
797
|
if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt.context)) && await this.passesFilters(cs, cctxt.context, prep, filters, 0)) {
|
|
798
798
|
await this.listDisplaysFromProvider(cds, cs, cctxt.context);
|
|
@@ -834,7 +834,7 @@ class ValueSetExpander {
|
|
|
834
834
|
if (!fc.value) {
|
|
835
835
|
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);
|
|
836
836
|
}
|
|
837
|
-
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter');
|
|
837
|
+
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
838
838
|
await cs.filter(prep, fc.property, fc.op, fc.value);
|
|
839
839
|
}
|
|
840
840
|
|
|
@@ -891,7 +891,7 @@ class ValueSetExpander {
|
|
|
891
891
|
this.worker.deadCheck('processCodes#1');
|
|
892
892
|
const valueSets = [];
|
|
893
893
|
|
|
894
|
-
Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set');
|
|
894
|
+
Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set', vsSrc.vurl);
|
|
895
895
|
|
|
896
896
|
if (cset.valueSet || cset.concept || (cset.filter || []).length > 1) {
|
|
897
897
|
this.canBeHierarchy = false;
|
|
@@ -978,7 +978,7 @@ class ValueSetExpander {
|
|
|
978
978
|
for (const cc of cset.concept) {
|
|
979
979
|
this.worker.deadCheck('processCodes#3');
|
|
980
980
|
cds.clear();
|
|
981
|
-
Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference');
|
|
981
|
+
Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl);
|
|
982
982
|
const cctxt = await cs.locate(cc.code, this.allAltCodes);
|
|
983
983
|
if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt)) && await this.passesFilters(cs, cctxt, prep, filters, 0)) {
|
|
984
984
|
if (filter.passesDesignations(cds) || filter.passes(cc.code)) {
|
|
@@ -1007,7 +1007,7 @@ class ValueSetExpander {
|
|
|
1007
1007
|
|
|
1008
1008
|
for (let fc of cset.filter) {
|
|
1009
1009
|
this.worker.deadCheck('processCodes#4a');
|
|
1010
|
-
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter');
|
|
1010
|
+
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
1011
1011
|
await cs.filter(prep, fc.property, fc.op, fc.value);
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
@@ -1157,8 +1157,8 @@ class ValueSetExpander {
|
|
|
1157
1157
|
this.totalStatus = 'uninitialised';
|
|
1158
1158
|
this.total = 0;
|
|
1159
1159
|
|
|
1160
|
-
Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet');
|
|
1161
|
-
Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet');
|
|
1160
|
+
Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
|
|
1161
|
+
Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
|
|
1162
1162
|
this.worker.seeValueSet(source, this.params);
|
|
1163
1163
|
this.valueSet = source;
|
|
1164
1164
|
|
|
@@ -1272,7 +1272,7 @@ class ValueSetExpander {
|
|
|
1272
1272
|
|
|
1273
1273
|
let vsInfo = this.scanValueSet(source.jsonObj.compose);
|
|
1274
1274
|
try {
|
|
1275
|
-
if (source.jsonObj.compose && Extensions.checkNoModifiers(source.jsonObj.compose, 'ValueSetExpander.Expand', 'compose')
|
|
1275
|
+
if (source.jsonObj.compose && Extensions.checkNoModifiers(source.jsonObj.compose, 'ValueSetExpander.Expand', 'compose', source.vurl)
|
|
1276
1276
|
&& this.worker.checkNoLockedDate(source.url, source.jsonObj.compose)) {
|
|
1277
1277
|
await this.handleCompose(source, filter, exp, notClosed, vsInfo);
|
|
1278
1278
|
}
|
package/tx/workers/related.js
CHANGED
|
@@ -205,12 +205,12 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
205
205
|
if (!thisC) {
|
|
206
206
|
return this.makeOutcome("indeterminate", `The ValueSet ${thisVS.vurl} has no compose`);
|
|
207
207
|
}
|
|
208
|
-
Extensions.checkNoModifiers(thisC, 'RelatedWorker.doRelated', 'compose')
|
|
208
|
+
Extensions.checkNoModifiers(thisC, 'RelatedWorker.doRelated', 'compose', thisVS.vurl)
|
|
209
209
|
this.checkNoLockedDate(thisVS.vurl, thisC);
|
|
210
210
|
if (!otherC) {
|
|
211
211
|
return this.makeOutcome("indeterminate", `The ValueSet ${otherVS.vurl} has no compose`);
|
|
212
212
|
}
|
|
213
|
-
Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose')
|
|
213
|
+
Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose', otherVS.vurl)
|
|
214
214
|
this.checkNoLockedDate(otherVS.vurl, otherC);
|
|
215
215
|
|
|
216
216
|
let systems = new Map(); // tracks whether they are version dependent or not
|
package/tx/workers/translate.js
CHANGED
|
@@ -113,6 +113,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
113
113
|
let targetScope = null;
|
|
114
114
|
let sourceScope = null;
|
|
115
115
|
let targetSystem = null;
|
|
116
|
+
let reverse = false;
|
|
116
117
|
|
|
117
118
|
// Get the source coding
|
|
118
119
|
// Accept both R5 names (sourceCoding, sourceCodeableConcept, sourceCode/sourceSystem)
|
|
@@ -145,10 +146,32 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
145
146
|
'system parameter is required when using code/sourceCode', null, 400);
|
|
146
147
|
}
|
|
147
148
|
const version = params.has('sourceVersion') ? params.get('sourceVersion') : params.get('version');
|
|
149
|
+
coding = {system, version, code};
|
|
150
|
+
} else if (params.has('targetCoding')) {
|
|
151
|
+
reverse = true;
|
|
152
|
+
coding = params.get('targetCoding');
|
|
153
|
+
} else if (params.has('targetCodeableConcept')) {
|
|
154
|
+
reverse = true;
|
|
155
|
+
const cc = params.get('targetCodeableConcept');
|
|
156
|
+
if (cc.coding && cc.coding.length > 0) {
|
|
157
|
+
coding = cc.coding[0]; // Use first coding
|
|
158
|
+
} else {
|
|
159
|
+
throw new Issue('error', 'invalid', null, null,
|
|
160
|
+
'sourceCodeableConcept must contain at least one coding', null, 400);
|
|
161
|
+
}
|
|
162
|
+
} else if (params.has('targetCode')) {
|
|
163
|
+
reverse = true;
|
|
164
|
+
const code = params.get('targetCode');
|
|
165
|
+
const system = params.get('targetSystem');
|
|
166
|
+
if (!system) {
|
|
167
|
+
throw new Issue('error', 'invalid', null, null,
|
|
168
|
+
'targetSystem parameter is required when using targetCode', null, 400);
|
|
169
|
+
}
|
|
170
|
+
const version = params.get('targetVersion');
|
|
148
171
|
coding = { system, version, code };
|
|
149
172
|
} else {
|
|
150
173
|
throw new Issue('error', 'invalid', null, null,
|
|
151
|
-
'Must provide sourceCode
|
|
174
|
+
'Must provide sourceCode+(source)system, sourceCoding, or sourceCodeableConcept, or targetCode+targetSystem), targetCoding, or targetCodeableConcept', null, 400);
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
// Get the concept map
|
|
@@ -173,21 +196,33 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
173
196
|
if (params.has('targetScope')) {
|
|
174
197
|
targetScope = params.get('targetScope');
|
|
175
198
|
}
|
|
176
|
-
if (
|
|
177
|
-
|
|
199
|
+
if (reverse) {
|
|
200
|
+
if (params.has('sourceSystem')) {
|
|
201
|
+
targetSystem = params.get('sourceSystem');
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
if (params.has('targetSystem')) {
|
|
205
|
+
targetSystem = params.get('targetSystem');
|
|
206
|
+
}
|
|
178
207
|
}
|
|
179
|
-
|
|
208
|
+
let explicit = true;
|
|
180
209
|
// If no explicit concept map, we need to find one based on source/target
|
|
181
210
|
if (conceptMaps.length == 0) {
|
|
182
|
-
|
|
183
|
-
|
|
211
|
+
explicit = false;
|
|
212
|
+
if (reverse) {
|
|
213
|
+
await this.findConceptMapsInAdditionalResources(conceptMaps,targetSystem, targetScope, sourceScope, coding.system);
|
|
214
|
+
await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, targetSystem, targetScope, sourceScope, coding.system, coding.code);
|
|
215
|
+
} else {
|
|
216
|
+
await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
|
|
217
|
+
await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem, coding.code);
|
|
218
|
+
}
|
|
184
219
|
if (conceptMaps.length == 0) {
|
|
185
220
|
throw new Issue('error', 'not-found', null, null, 'No suitable ConceptMaps found for the specified source and target', null, 404);
|
|
186
221
|
}
|
|
187
222
|
}
|
|
188
223
|
|
|
189
224
|
// Perform the translation
|
|
190
|
-
const result = await this.doTranslate(conceptMaps, coding, targetScope, targetSystem, txp);
|
|
225
|
+
const result = await this.doTranslate(conceptMaps, coding, targetScope, targetSystem, txp, reverse, explicit);
|
|
191
226
|
return res.status(200).json(result);
|
|
192
227
|
}
|
|
193
228
|
|
|
@@ -287,7 +322,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
287
322
|
return result;
|
|
288
323
|
}
|
|
289
324
|
|
|
290
|
-
|
|
325
|
+
translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, output, explicit) {
|
|
291
326
|
let result = false;
|
|
292
327
|
const matches = cm.listTranslations(coding, targetScope, targetSystem);
|
|
293
328
|
if (matches.length > 0) {
|
|
@@ -295,7 +330,13 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
295
330
|
const g = match.group;
|
|
296
331
|
const em = match.match;
|
|
297
332
|
for (const map of em.target || []) {
|
|
298
|
-
|
|
333
|
+
let ok = false;
|
|
334
|
+
if (map.equivalence) { // R4 mode
|
|
335
|
+
ok = ['null', 'relatedto', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower', 'specializes', 'inexact'].includes(map.equivalence);
|
|
336
|
+
} else {
|
|
337
|
+
ok = ['null', 'related-to', 'equivalent', 'source-is-narrower-than-target', 'source-is-broader-than-target'].includes(map.relationship);
|
|
338
|
+
}
|
|
339
|
+
if (ok) {
|
|
299
340
|
result = true;
|
|
300
341
|
|
|
301
342
|
const outcome = {
|
|
@@ -303,22 +344,119 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
303
344
|
code: map.code
|
|
304
345
|
};
|
|
305
346
|
|
|
347
|
+
if (!this.hasMatch(output, outcome)) {
|
|
348
|
+
const matchParts = [];
|
|
349
|
+
matchParts.push({
|
|
350
|
+
name: 'concept',
|
|
351
|
+
valueCoding: outcome
|
|
352
|
+
});
|
|
353
|
+
matchParts.push({
|
|
354
|
+
name: 'relationship',
|
|
355
|
+
valueCode: map.relationship
|
|
356
|
+
});
|
|
357
|
+
// equivalence vs relationship will be sorted out in the version transform for parameters
|
|
358
|
+
if (map.equivalence) {
|
|
359
|
+
matchParts.push({
|
|
360
|
+
name: 'equivalence',
|
|
361
|
+
valueCode: map.equivalence
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (map.comment) {
|
|
365
|
+
matchParts.push({
|
|
366
|
+
name: 'message',
|
|
367
|
+
valueString: map.comment
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
for (const prod of map.product || []) {
|
|
371
|
+
const productParts = [];
|
|
372
|
+
productParts.push({
|
|
373
|
+
name: 'element',
|
|
374
|
+
valueString: prod.property
|
|
375
|
+
});
|
|
376
|
+
productParts.push({
|
|
377
|
+
name: 'concept',
|
|
378
|
+
valueCoding: {
|
|
379
|
+
system: prod.system,
|
|
380
|
+
code: prod.value
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
matchParts.push({
|
|
384
|
+
name: 'product',
|
|
385
|
+
part: productParts
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (!explicit) {
|
|
389
|
+
matchParts.push({
|
|
390
|
+
name: 'sourceMap',
|
|
391
|
+
valueCanonical: cm.vurl
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
output.push({
|
|
395
|
+
name: 'match',
|
|
396
|
+
part: matchParts
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, output, explicit) {
|
|
407
|
+
let result = false;
|
|
408
|
+
const matches = cm.listTranslationsReverse(coding, targetScope, targetSystem);
|
|
409
|
+
if (matches.length > 0) {
|
|
410
|
+
for (let match of matches) {
|
|
411
|
+
const g = match.group;
|
|
412
|
+
const em = match.match;
|
|
413
|
+
const map = match.target;
|
|
414
|
+
let ok = false;
|
|
415
|
+
if (map.equivalence) { // R4 mode
|
|
416
|
+
ok = ['null', 'relatedto', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower', 'specializes', 'inexact'].includes(map.equivalence);
|
|
417
|
+
} else {
|
|
418
|
+
ok = ['null', 'related-to', 'equivalent', 'source-is-narrower-than-target', 'source-is-broader-than-target'].includes(map.relationship);
|
|
419
|
+
}
|
|
420
|
+
if (ok) {
|
|
421
|
+
result = true;
|
|
422
|
+
|
|
423
|
+
const outcome = {
|
|
424
|
+
system: g.source,
|
|
425
|
+
code: em.code
|
|
426
|
+
};
|
|
427
|
+
const t = {
|
|
428
|
+
system: g.target,
|
|
429
|
+
code: coding.code
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
if (!this.hasMatch(output, outcome)) {
|
|
306
433
|
const matchParts = [];
|
|
307
434
|
matchParts.push({
|
|
308
|
-
name: '
|
|
435
|
+
name: 'source',
|
|
309
436
|
valueCoding: outcome
|
|
310
437
|
});
|
|
438
|
+
matchParts.push({
|
|
439
|
+
name: 'concept',
|
|
440
|
+
valueCoding: t
|
|
441
|
+
});
|
|
311
442
|
matchParts.push({
|
|
312
443
|
name: 'relationship',
|
|
313
444
|
valueCode: map.relationship
|
|
314
445
|
});
|
|
315
|
-
|
|
446
|
+
// equivalence vs relationship will be sorted out in the version transform for parameters
|
|
447
|
+
if (map.equivalence) {
|
|
448
|
+
matchParts.push({
|
|
449
|
+
name: 'equivalence',
|
|
450
|
+
valueCode: map.equivalence
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (map.comment) {
|
|
316
454
|
matchParts.push({
|
|
317
455
|
name: 'message',
|
|
318
|
-
valueString: map.
|
|
456
|
+
valueString: map.comment
|
|
319
457
|
});
|
|
320
458
|
}
|
|
321
|
-
for (const prod of map.
|
|
459
|
+
for (const prod of map.product || []) {
|
|
322
460
|
const productParts = [];
|
|
323
461
|
productParts.push({
|
|
324
462
|
name: 'element',
|
|
@@ -336,6 +474,12 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
336
474
|
part: productParts
|
|
337
475
|
});
|
|
338
476
|
}
|
|
477
|
+
if (!explicit) {
|
|
478
|
+
matchParts.push({
|
|
479
|
+
name: 'sourceMap',
|
|
480
|
+
valueCanonical: cm.vurl
|
|
481
|
+
});
|
|
482
|
+
}
|
|
339
483
|
output.push({
|
|
340
484
|
name: 'match',
|
|
341
485
|
part: matchParts
|
|
@@ -347,7 +491,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
347
491
|
return result;
|
|
348
492
|
}
|
|
349
493
|
|
|
350
|
-
async translateUsingCodeSystem(cm, coding, target, params, output) {
|
|
494
|
+
async translateUsingCodeSystem(cm, coding, target, params, output, reverse, explicit) {
|
|
351
495
|
let result = false;
|
|
352
496
|
const factory = cm.jsonObj.internalSource;
|
|
353
497
|
let prov = await factory.build(this.opContext, []);
|
|
@@ -357,7 +501,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
357
501
|
valueUri: prov.system() + '|' + prov.version()
|
|
358
502
|
});
|
|
359
503
|
|
|
360
|
-
let translations = await prov.getTranslations(coding, target);
|
|
504
|
+
let translations = await prov.getTranslations(cm, coding, target, reverse);
|
|
361
505
|
|
|
362
506
|
if (translations.length > 0) {
|
|
363
507
|
result = true;
|
|
@@ -371,7 +515,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
371
515
|
}
|
|
372
516
|
|
|
373
517
|
const outcome = {
|
|
374
|
-
system: t.
|
|
518
|
+
system: t.system,
|
|
375
519
|
code: t.code,
|
|
376
520
|
version: t.version,
|
|
377
521
|
display: t.display
|
|
@@ -392,6 +536,12 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
392
536
|
valueString: t.message
|
|
393
537
|
});
|
|
394
538
|
}
|
|
539
|
+
if (!explicit) {
|
|
540
|
+
matchParts.push({
|
|
541
|
+
name: 'sourceMap',
|
|
542
|
+
valueCanonical: cm.vurl
|
|
543
|
+
});
|
|
544
|
+
}
|
|
395
545
|
output.push({
|
|
396
546
|
name: 'match',
|
|
397
547
|
part: matchParts
|
|
@@ -408,9 +558,11 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
408
558
|
* @param {string} targetScope - Target value set scope (optional)
|
|
409
559
|
* @param {string} targetSystem - Target code system (optional)
|
|
410
560
|
* @param {Parameters} params - Full parameters object
|
|
561
|
+
* @param {boolean} reverse - Full parameters object*
|
|
562
|
+
* @param {boolean} explicit - If the concept map was named explicitly
|
|
411
563
|
* @returns {Object} Parameters resource with translate result
|
|
412
564
|
*/
|
|
413
|
-
async doTranslate(conceptMaps, coding, targetScope, targetSystem, params) {
|
|
565
|
+
async doTranslate(conceptMaps, coding, targetScope, targetSystem, params, reverse, explicit) {
|
|
414
566
|
this.deadCheck('doTranslate');
|
|
415
567
|
|
|
416
568
|
const result = [];
|
|
@@ -419,9 +571,11 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
419
571
|
let added = false;
|
|
420
572
|
for (const cm of conceptMaps) {
|
|
421
573
|
if (cm.jsonObj.internalSource) {
|
|
422
|
-
added = await this.translateUsingCodeSystem(cm, coding, targetSystem, params, result) || added;
|
|
423
|
-
} else {
|
|
424
|
-
added = this.
|
|
574
|
+
added = await this.translateUsingCodeSystem(cm, coding, targetSystem, params, result, reverse, explicit) || added;
|
|
575
|
+
} else if (reverse) {
|
|
576
|
+
added = this.translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, result, reverse, explicit) || added;
|
|
577
|
+
} else{
|
|
578
|
+
added = this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result, reverse, explicit) || added;
|
|
425
579
|
}
|
|
426
580
|
}
|
|
427
581
|
result.push({
|
|
@@ -517,6 +671,16 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
517
671
|
}
|
|
518
672
|
}
|
|
519
673
|
}
|
|
674
|
+
|
|
675
|
+
hasMatch(output, outcome) {
|
|
676
|
+
for (let o of output) {
|
|
677
|
+
let c = o.part.find(x => x.name === 'concept');
|
|
678
|
+
if (c.valueCoding.code === outcome.code && c.valueCoding.system === outcome.system) {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
520
684
|
}
|
|
521
685
|
|
|
522
686
|
module.exports = TranslateWorker;
|
package/tx/workers/validate.js
CHANGED
|
@@ -316,21 +316,21 @@ class ValueSetChecker {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
Extensions.checkNoImplicitRules(this.valueSet, 'ValueSetChecker.prepare', 'ValueSet');
|
|
319
|
-
Extensions.checkNoModifiers(this.valueSet, 'ValueSetChecker.prepare', 'ValueSet');
|
|
319
|
+
Extensions.checkNoModifiers(this.valueSet, 'ValueSetChecker.prepare', 'ValueSet', this.valueSet.vurl);
|
|
320
320
|
|
|
321
321
|
this.allValueSet = this.valueSet.url === 'http://hl7.org/fhir/ValueSet/@all';
|
|
322
322
|
|
|
323
323
|
if (this.valueSet.jsonObj.compose) {
|
|
324
|
-
Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose');
|
|
325
|
-
this.worker.checkNoLockedDate(this.valueSet.
|
|
324
|
+
Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose', this.valueSet.vurl);
|
|
325
|
+
this.worker.checkNoLockedDate(this.valueSet.vurl, this.valueSet.jsonObj.compose)
|
|
326
326
|
let i = 0;
|
|
327
327
|
for (let cc of this.valueSet.jsonObj.compose.include || []) {
|
|
328
|
-
await this.prepareConceptSet('include['+i+']', cc);
|
|
328
|
+
await this.prepareConceptSet('include['+i+']', cc, this.valueSet);
|
|
329
329
|
i++;
|
|
330
330
|
}
|
|
331
331
|
i = 0;
|
|
332
332
|
for (let cc of this.valueSet.jsonObj.compose.exclude || []) {
|
|
333
|
-
await this.prepareConceptSet('exclude['+i+']', cc);
|
|
333
|
+
await this.prepareConceptSet('exclude['+i+']', cc, this.valueSet);
|
|
334
334
|
i++;
|
|
335
335
|
}
|
|
336
336
|
}
|
|
@@ -342,9 +342,9 @@ class ValueSetChecker {
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
async prepareConceptSet(desc, cc) {
|
|
345
|
+
async prepareConceptSet(desc, cc, vs) {
|
|
346
346
|
this.worker.deadCheck('prepareConceptSet');
|
|
347
|
-
Extensions.checkNoModifiers(cc, 'ValueSetChecker.prepare', desc);
|
|
347
|
+
Extensions.checkNoModifiers(cc, 'ValueSetChecker.prepare', desc, vs.vurl);
|
|
348
348
|
this.worker.opContext.addNote(this.valueSet, 'Prepare ' + desc + ': "' + this.worker.renderer.displayValueSetInclude(cc) + '"', this.indentCount);
|
|
349
349
|
if (cc.valueSet) {
|
|
350
350
|
for (let u of cc.valueSet) {
|
|
@@ -374,7 +374,7 @@ class ValueSetChecker {
|
|
|
374
374
|
let i = 0;
|
|
375
375
|
for (let ccf of cc.filter || []) {
|
|
376
376
|
this.worker.deadCheck('prepareConceptSet#2');
|
|
377
|
-
Extensions.checkNoModifiers(ccf, 'ValueSetChecker.prepare', desc + '.filter');
|
|
377
|
+
Extensions.checkNoModifiers(ccf, 'ValueSetChecker.prepare', desc + '.filter', this.valueSet.vurl);
|
|
378
378
|
if (!ccf.value) {
|
|
379
379
|
throw new Issue('error', 'invalid', "ValueSet.compose."+desc+".filter["+i+"]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE',
|
|
380
380
|
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 {
|
|
|
677
677
|
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);
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
if (Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose')) {
|
|
680
|
+
if (Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose', this.valueSet.vurl)) {
|
|
681
681
|
result = false;
|
|
682
682
|
let determinedVersion = undefined;
|
|
683
683
|
if (!version) {
|
|
@@ -807,7 +807,7 @@ class ValueSetChecker {
|
|
|
807
807
|
}
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
|
-
} else if (Extensions.checkNoModifiers(this.valueSet.jsonObj.expansion, 'ValueSetChecker.prepare', 'ValueSet.expansion')) {
|
|
810
|
+
} else if (Extensions.checkNoModifiers(this.valueSet.jsonObj.expansion, 'ValueSetChecker.prepare', 'ValueSet.expansion', this.valueSet.vurl)) {
|
|
811
811
|
let ccc = this.valueSet.findContains(system, version, code);
|
|
812
812
|
if (ccc === null) {
|
|
813
813
|
result = false;
|
|
@@ -10,7 +10,11 @@ const {VersionUtilities} = require("../../library/version-utilities");
|
|
|
10
10
|
|
|
11
11
|
function parametersToR5(jsonObj, sourceVersion) {
|
|
12
12
|
if (VersionUtilities.isR5Ver(sourceVersion)) {
|
|
13
|
-
|
|
13
|
+
if (jsonObj.parameter && jsonObj.parameter.find(p => p.name == 'match')) {
|
|
14
|
+
return convertResourceWithinR5(JSON.parse(JSON.stringify(jsonObj)));
|
|
15
|
+
} else {
|
|
16
|
+
return jsonObj; // No conversion needed
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
const {convertResourceFromR5} = require("./xv-resource");
|
|
@@ -59,8 +63,57 @@ function parametersR5ToR4(r5Obj) {
|
|
|
59
63
|
if (p.resource) {
|
|
60
64
|
p.resource = convertResourceFromR5(p.resource, "R4");
|
|
61
65
|
}
|
|
66
|
+
if (p.name == 'match') {
|
|
67
|
+
fixMatchParameterfor4(p);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return r5Obj;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function convertResourceWithinR5(r5Obj) {
|
|
74
|
+
for (let p of r5Obj.parameter) {
|
|
75
|
+
if (p.name == 'match') {
|
|
76
|
+
fixMatchParameterfor5(p);
|
|
77
|
+
}
|
|
62
78
|
}
|
|
63
79
|
return r5Obj;
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fixMatchParameterfor5(p) {
|
|
84
|
+
if (p.part) {
|
|
85
|
+
p.part = p.part.filter(pp => pp.name !== 'equivalence');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fixMatchParameterfor4(p) {
|
|
90
|
+
if (p.part) {
|
|
91
|
+
if (!p.part.find(pp => pp.name === 'equivalence')) {
|
|
92
|
+
let rel = p.part.find(pp => pp.name === 'relationship');
|
|
93
|
+
if (rel && rel.valueCode) {
|
|
94
|
+
let pp = {name: "equivalence"};
|
|
95
|
+
switch (rel.valueCode) {
|
|
96
|
+
case 'related-to':
|
|
97
|
+
pp.valueCode = 'relatedto';
|
|
98
|
+
break;
|
|
99
|
+
case 'equivalent':
|
|
100
|
+
pp.valueCode = 'equivalent';
|
|
101
|
+
break;
|
|
102
|
+
case 'source-is-narrower-than-target':
|
|
103
|
+
pp.valueCode = 'wider';
|
|
104
|
+
break;
|
|
105
|
+
case 'source-is-broader-than-target':
|
|
106
|
+
pp.valueCode = 'narrower';
|
|
107
|
+
break;
|
|
108
|
+
case 'not-related-to':
|
|
109
|
+
pp.valueCode = 'unmatched';
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
p.part.push(pp);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
p.part = p.part.filter(pp => pp.name !== 'relationship');
|
|
116
|
+
}
|
|
64
117
|
}
|
|
65
118
|
|
|
66
119
|
function convertParameterR5ToR3(p) {
|