fhirsmith 0.13.4 → 0.14.0
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 +32 -1
- package/README.md +16 -3
- package/config-template.json +70 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/nginx.md +391 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher.js +146 -14
- package/server.js +51 -14
- package/stats.js +19 -13
- package/testing/readme.md +112 -0
- package/testing/render.js +482 -0
- package/testing/search.js +378 -0
- package/testing/store.js +266 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +486 -0
- package/translations/Messages.properties +32 -0
- package/tx/closure/closure-store.js +396 -0
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-rxnorm.js +35 -8
- package/tx/cs/cs-snomed.js +22 -3
- package/tx/html/codesystem-operations.liquid +177 -0
- package/tx/html/ecl-panel.liquid +93 -0
- package/tx/html/home-metrics.liquid +39 -76
- package/tx/html/tx-template.html +1 -0
- package/tx/library/conceptmap.js +22 -6
- package/tx/library/designations.js +12 -3
- package/tx/library/operation-outcome.js +3 -1
- package/tx/library/renderer.js +42 -5
- package/tx/operation-context.js +80 -17
- package/tx/provider.js +85 -0
- package/tx/sct/expressions.js +128 -44
- package/tx/tx-html.js +426 -44
- package/tx/tx.js +117 -9
- package/tx/workers/cache-control.js +2 -2
- package/tx/workers/closure.js +483 -13
- package/tx/workers/metadata.js +8 -3
- package/tx/workers/read.js +18 -3
- package/tx/workers/subsumes.js +18 -2
- package/tx/workers/translate.js +167 -20
- package/tx/workers/validate.js +61 -182
- package/tx/workers/worker.js +9 -0
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
package/tx/workers/translate.js
CHANGED
|
@@ -15,6 +15,7 @@ const {ConceptMap} = require("../library/conceptmap");
|
|
|
15
15
|
const {Extensions} = require("../library/extensions");
|
|
16
16
|
const {VersionUtilities} = require("../../library/version-utilities");
|
|
17
17
|
const {debugLog} = require("../operation-context");
|
|
18
|
+
const {ValueSetChecker, ValidationCheckMode} = require("./validate");
|
|
18
19
|
|
|
19
20
|
// ConceptMap.group.element.comment is R6; on an R5 ConceptMap it is carried by the
|
|
20
21
|
// cross-version extension. Read both, so a preadopted R5 resource and a native R6 one
|
|
@@ -260,6 +261,22 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
260
261
|
targetSystem = params.get('system');
|
|
261
262
|
}
|
|
262
263
|
}
|
|
264
|
+
// The sourceScope parameter is a precondition on the whole operation, not a way of picking
|
|
265
|
+
// maps: the client is saying which codes it is asking about. So it is evaluated once, before
|
|
266
|
+
// any map is considered, and if the code is not in it then no map applies under any
|
|
267
|
+
// condition - including a map the client nominated by url. Once the code has been shown to
|
|
268
|
+
// be in it, the parameter has done its work and plays no further part; each map is then
|
|
269
|
+
// judged by its own scope. Under reverse the two scopes have already swapped above, so
|
|
270
|
+
// sourceScope here is the scope of the concept that was named either way.
|
|
271
|
+
if (sourceScope) {
|
|
272
|
+
const inScope = await this.codeInScope(sourceScope, coding, txp,
|
|
273
|
+
`${reverse ? 'targetScope' : 'sourceScope'} parameter`);
|
|
274
|
+
if (!inScope) {
|
|
275
|
+
return res.status(200).json(this.noMapsResponse(coding, sourceScope, targetScope, targetSystem, reverse,
|
|
276
|
+
`'${coding.system}#${coding.code}' is not in the value set '${sourceScope}', so no ConceptMap can translate it`));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
263
280
|
// If no explicit concept map, we need to find one based on source/target
|
|
264
281
|
if (conceptMaps.length == 0) {
|
|
265
282
|
if (reverse) {
|
|
@@ -269,25 +286,19 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
269
286
|
await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
|
|
270
287
|
await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, sourceScope, targetScope, targetSystem, coding.code);
|
|
271
288
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
{
|
|
286
|
-
name: 'result',
|
|
287
|
-
valueBoolean: false
|
|
288
|
-
}]
|
|
289
|
-
});
|
|
290
|
-
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Whether the map was nominated or found, it only applies if the code is within its own
|
|
292
|
+
// declared scope, so the filter goes here rather than in the selection above - a nominated
|
|
293
|
+
// map never passes through selection at all.
|
|
294
|
+
const scoped = await this.filterMapsByScope(conceptMaps, coding, txp, reverse);
|
|
295
|
+
conceptMaps = scoped.maps;
|
|
296
|
+
|
|
297
|
+
if (conceptMaps.length == 0) {
|
|
298
|
+
// No map covers this code. That is an answer - there is no translation - not an error;
|
|
299
|
+
// a nominated map that cannot be *found* is the error case, and is reported when the url
|
|
300
|
+
// parameter is resolved above.
|
|
301
|
+
return res.status(200).json(this.noMapsResponse(coding, sourceScope, targetScope, targetSystem, reverse, null, scoped.anyScoped));
|
|
291
302
|
}
|
|
292
303
|
|
|
293
304
|
// Perform the translation
|
|
@@ -371,8 +382,15 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
371
382
|
let conceptMaps = [];
|
|
372
383
|
conceptMaps.push(conceptMap);
|
|
373
384
|
|
|
385
|
+
// Naming the map by id does not put an out-of-scope code into its scope, any more than
|
|
386
|
+
// naming it by url does: the map either covers this code or it does not.
|
|
387
|
+
const scoped = await this.filterMapsByScope(conceptMaps, coding, txp, false);
|
|
388
|
+
if (scoped.maps.length === 0) {
|
|
389
|
+
return res.status(200).json(this.noMapsResponse(coding, null, targetScope, targetSystem, false, null, scoped.anyScoped));
|
|
390
|
+
}
|
|
391
|
+
|
|
374
392
|
// Perform the translation
|
|
375
|
-
const result = await this.doTranslate(
|
|
393
|
+
const result = await this.doTranslate(scoped.maps, coding, targetScope, targetSystem, params);
|
|
376
394
|
return res.status(200).json(result);
|
|
377
395
|
}
|
|
378
396
|
|
|
@@ -906,6 +924,135 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
906
924
|
return result;
|
|
907
925
|
}
|
|
908
926
|
|
|
927
|
+
/**
|
|
928
|
+
* ConceptMap.sourceScope "limits the scope of the map to source codes that are members of
|
|
929
|
+
* this value set", so a code outside it means the map does not apply at all - not that the
|
|
930
|
+
* map has no element for it. The difference matters because group.unmapped answers the
|
|
931
|
+
* second question and must not answer the first: a map with unmapped mode=fixed would
|
|
932
|
+
* otherwise translate every code in the code system its group names, whatever its author
|
|
933
|
+
* declared, and sourceScope would have no effect on any map that has an unmapped.
|
|
934
|
+
*
|
|
935
|
+
* Resolving a scope costs a value set lookup and a $validate-code, and the same scope is
|
|
936
|
+
* usually shared by several of the candidate maps, so both are memoised for the life of the
|
|
937
|
+
* operation. They are deliberately not cached beyond it: tx-resource makes the resolution of
|
|
938
|
+
* a canonical a property of the request, not of the server.
|
|
939
|
+
*/
|
|
940
|
+
scopeVsCache = new Map(); // canonical -> ValueSet
|
|
941
|
+
scopeCodeCache = new Map(); // canonical + coding -> boolean
|
|
942
|
+
|
|
943
|
+
async resolveScopeValueSet(canonical, why) {
|
|
944
|
+
if (this.scopeVsCache.has(canonical)) {
|
|
945
|
+
return this.scopeVsCache.get(canonical);
|
|
946
|
+
}
|
|
947
|
+
const { url, version } = VersionUtilities.splitCanonical(canonical);
|
|
948
|
+
let vs = null;
|
|
949
|
+
try {
|
|
950
|
+
vs = await this.findValueSet(url, version);
|
|
951
|
+
} catch (e) {
|
|
952
|
+
if (e instanceof Issue) {
|
|
953
|
+
throw e;
|
|
954
|
+
}
|
|
955
|
+
vs = null;
|
|
956
|
+
}
|
|
957
|
+
if (!vs) {
|
|
958
|
+
// Fatal to the operation, deliberately. A scope that cannot be resolved is a scope that
|
|
959
|
+
// cannot be evaluated, and dropping the map silently would report "no translation" for
|
|
960
|
+
// what is really "cannot tell" - the same trap as answering not-subsumed when the real
|
|
961
|
+
// answer is unknown.
|
|
962
|
+
throw new Issue('error', 'not-found', null, null,
|
|
963
|
+
`The value set '${canonical}', which is the ${why}, could not be found, so the scope of the translation cannot be determined`, null, 404);
|
|
964
|
+
}
|
|
965
|
+
this.scopeVsCache.set(canonical, vs);
|
|
966
|
+
return vs;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async codeInScope(canonical, coding, params, why) {
|
|
970
|
+
const key = canonical + '\u0000' + coding.system + '|' + (coding.version || '') + '#' + coding.code;
|
|
971
|
+
if (this.scopeCodeCache.has(key)) {
|
|
972
|
+
return this.scopeCodeCache.get(key);
|
|
973
|
+
}
|
|
974
|
+
const vs = await this.resolveScopeValueSet(canonical, why);
|
|
975
|
+
let inScope;
|
|
976
|
+
try {
|
|
977
|
+
const checker = new ValueSetChecker(this, vs, params);
|
|
978
|
+
await checker.prepare();
|
|
979
|
+
// The coding is wrapped in a CodeableConcept so that this goes through
|
|
980
|
+
// checkCodeableConcept - the same path every $validate-code takes, and the only one the
|
|
981
|
+
// test suite exercises.
|
|
982
|
+
const outcome = await checker.checkCodeableConcept('Coding',
|
|
983
|
+
{ coding: [{ system: coding.system, version: coding.version, code: coding.code }] },
|
|
984
|
+
ValidationCheckMode.Coding);
|
|
985
|
+
inScope = outcome.has('result') && outcome.get('result') === true;
|
|
986
|
+
} catch (e) {
|
|
987
|
+
if (e instanceof Issue) {
|
|
988
|
+
throw e;
|
|
989
|
+
}
|
|
990
|
+
// The value set resolved; checking the code against it is what failed. Say so - the
|
|
991
|
+
// earlier wording claimed it could not be found, which sent the reader looking for a
|
|
992
|
+
// missing resource that was in fact right there.
|
|
993
|
+
throw new Issue('error', 'not-supported', null, null,
|
|
994
|
+
`The value set '${canonical}' was found, but '${coding.system}#${coding.code}' could not be checked against it (${e.message}), so the ${why} cannot be evaluated`, null, 422);
|
|
995
|
+
}
|
|
996
|
+
this.scopeCodeCache.set(key, inScope);
|
|
997
|
+
return inScope;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* The answer when nothing can translate this code: a successful operation with a negative
|
|
1002
|
+
* result, not an error. Where any candidate map declared a scope, the message names the code,
|
|
1003
|
+
* because the code is then the reason - saying only that no map goes from this system to that
|
|
1004
|
+
* one would be wrong, since such maps exist and simply do not cover this code.
|
|
1005
|
+
*/
|
|
1006
|
+
noMapsResponse(coding, sourceScope, targetScope, targetSystem, reverse, message, anyScoped) {
|
|
1007
|
+
let text = message;
|
|
1008
|
+
if (!text) {
|
|
1009
|
+
const from = reverse ? (targetSystem || sourceScope) : coding.system;
|
|
1010
|
+
const to = reverse ? coding.system : (targetSystem || targetScope);
|
|
1011
|
+
text = anyScoped
|
|
1012
|
+
? `No ConceptMap that translates from '${from || '(unspecified)'}' to '${to || '(unspecified)'}' has '${coding.system}#${coding.code}' in scope`
|
|
1013
|
+
: `No ConceptMap is available to translate from '${from || '(unspecified)'}' to '${to || '(unspecified)'}'`;
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
resourceType: 'Parameters',
|
|
1017
|
+
parameter: [{
|
|
1018
|
+
name: 'message',
|
|
1019
|
+
valueString: text
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: 'result',
|
|
1023
|
+
valueBoolean: false
|
|
1024
|
+
}]
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Drop the candidate maps that do not apply to this code. A map that declares no scope on the
|
|
1030
|
+
* relevant side is not filtered - it applies on the strength of its group source/target
|
|
1031
|
+
* systems alone. Under reverse the concept named is the target concept, so it is
|
|
1032
|
+
* ConceptMap.targetScope that governs.
|
|
1033
|
+
* @returns {Promise<{maps: Object[], anyScoped: boolean}>} anyScoped says whether any candidate
|
|
1034
|
+
* declared a scope, which decides whether the "no map" message names the code
|
|
1035
|
+
*/
|
|
1036
|
+
async filterMapsByScope(conceptMaps, coding, params, reverse) {
|
|
1037
|
+
const maps = [];
|
|
1038
|
+
let anyScoped = false;
|
|
1039
|
+
const side = reverse ? 'target' : 'source';
|
|
1040
|
+
for (const cm of conceptMaps) {
|
|
1041
|
+
const canonical = reverse ? cm.targetScope : cm.sourceScope;
|
|
1042
|
+
if (!canonical) {
|
|
1043
|
+
maps.push(cm);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
anyScoped = true;
|
|
1047
|
+
if (await this.codeInScope(canonical, coding, params, `${side} scope of the ConceptMap '${cm.url || '(inline)'}'`)) {
|
|
1048
|
+
maps.push(cm);
|
|
1049
|
+
} else {
|
|
1050
|
+
this.opContext.addNote(null, `ConceptMap '${cm.url}' does not apply: '${coding.code}' is not in its ${side} scope '${canonical}'`, 0);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return { maps, anyScoped };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
909
1056
|
/**
|
|
910
1057
|
* Perform the actual translate operation
|
|
911
1058
|
* @param {Object} conceptMap - ConceptMap resource
|
package/tx/workers/validate.js
CHANGED
|
@@ -86,11 +86,13 @@ class ValueSetChecker {
|
|
|
86
86
|
op.addIssueIfNew(new Issue('information', 'business-rule', '', 'MSG_EXPERIMENTAL', this.worker.i18n.translate('MSG_EXPERIMENTAL', this.params.HTTPLanguages, [vurl, '', rtype]), 'status-check'), false);
|
|
87
87
|
} else if ((status === 'draft' || standardsStatus === 'draft') &&
|
|
88
88
|
!((source.status === 'draft') || (Extensions.readString(source, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status') === 'draft'))) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
// MSG_DRAFT_SRC_STATUS names the referring resource's status and type as well as its
|
|
90
|
+
// url, so the reader can see why the reference is reported at all: a resource that is
|
|
91
|
+
// not draft pointing at one that is. Matches ValueSetProcessBase.checkCanonical in the
|
|
92
|
+
// java implementation, so both terminology services report this the same way
|
|
93
|
+
op.addIssueIfNew(new Issue('information', 'business-rule', '', 'MSG_DRAFT_SRC_STATUS',
|
|
94
|
+
this.worker.i18n.translate('MSG_DRAFT_SRC_STATUS', this.params.HTTPLanguages,
|
|
95
|
+
[vurl, this.worker.makeVurl(source), rtype, source.status, source.resourceType]), 'status-check'), false);
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
}
|
|
@@ -886,101 +888,6 @@ class ValueSetChecker {
|
|
|
886
888
|
return result;
|
|
887
889
|
}
|
|
888
890
|
|
|
889
|
-
async checkCoding(issuePath, coding) {
|
|
890
|
-
let inactive = false;
|
|
891
|
-
let path = issuePath;
|
|
892
|
-
let unknownSystems = new Set();
|
|
893
|
-
let unkCodes = [];
|
|
894
|
-
let messages = [];
|
|
895
|
-
let result = new Parameters();
|
|
896
|
-
|
|
897
|
-
this.worker.opContext.clearContexts();
|
|
898
|
-
if (this.params.inferSystem) {
|
|
899
|
-
this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(coding) + '" and infer system', this.indentCount);
|
|
900
|
-
} else {
|
|
901
|
-
this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(coding) + '"', this.indentCount);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
let op = new OperationOutcome();
|
|
905
|
-
this.checkCanonicalStatus(path, op, this.valueSet, this.valueSet);
|
|
906
|
-
let list = new Designations(this.worker.languages);
|
|
907
|
-
let ver = {value: ''};
|
|
908
|
-
inactive = {value: false};
|
|
909
|
-
let normalForm = {value: ''};
|
|
910
|
-
let vstatus = {value: ''};
|
|
911
|
-
let cause = {value: null};
|
|
912
|
-
let contentMode = {value: null};
|
|
913
|
-
let impliedSystem = {value: ''};
|
|
914
|
-
let defLang = {value: null};
|
|
915
|
-
|
|
916
|
-
let ok = await this.check(path, coding.system, coding.version, coding.code, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, result, contentMode, impliedSystem, unkCodes, messages, defLang);
|
|
917
|
-
if (ok === true) {
|
|
918
|
-
result.AddParamBool('result', true);
|
|
919
|
-
if ((cause.value === 'not-found' && contentMode.value !== 'complete') || contentMode.value === 'example') {
|
|
920
|
-
result.addParamStr('message', 'The system "' + coding.system + ' was found but did not contain enough information to properly validate the code (mode = ' + contentMode.value + ')');
|
|
921
|
-
}
|
|
922
|
-
if (coding.display && !list.hasDisplay(this.params.workingLanguages(), defLang.value, coding.display, false, DisplayCheckingStyle.CASE_INSENSITIVE).found) {
|
|
923
|
-
let baseMsg = 'Display_Name_for__should_be_one_of__instead_of';
|
|
924
|
-
let dc = list.displayCount(this.params.workingLanguages(), null, true);
|
|
925
|
-
if (dc > 0) {
|
|
926
|
-
if (list.hasDisplay(this.params.workingLanguages(), defLang.value, coding.display, false, DisplayCheckingStyle.CASE_INSENSITIVE).difference === DisplayDifference.Normalised) {
|
|
927
|
-
baseMsg = 'Display_Name_WS_for__should_be_one_of__instead_of';
|
|
928
|
-
}
|
|
929
|
-
if (dc === 1) {
|
|
930
|
-
result.addParamStr('message', this.worker.i18n.translate(baseMsg + '_one', this.params.HTTPLanguages,
|
|
931
|
-
['', coding.system, coding.code, list.present(this.params.workingLanguages(), defLang.value, true), coding.display, this.params.langSummary()]));
|
|
932
|
-
} else {
|
|
933
|
-
result.addParamStr('message', this.worker.i18n.translate(baseMsg + '_other', this.params.HTTPLanguages, [dc.toString(), coding.system, coding.code, list.present(this.params.workingLanguages(), defLang.value, true), coding.display, this.params.langSummary()]));
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
let pd = list.preferredDisplay(this.params.workingLanguages());
|
|
938
|
-
if (pd) {
|
|
939
|
-
result.addParamStr('display', pd);
|
|
940
|
-
}
|
|
941
|
-
result.addParamUri('system', coding.system);
|
|
942
|
-
if (ver.value) {
|
|
943
|
-
result.addParamStr('version', ver.value);
|
|
944
|
-
}
|
|
945
|
-
if (cause.value !== 'null') {
|
|
946
|
-
result.AddParamCode('cause', cause.value);
|
|
947
|
-
}
|
|
948
|
-
if (inactive.value) {
|
|
949
|
-
result.AddParamBool('inactive', inactive.value);
|
|
950
|
-
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
951
|
-
result.addParamCode('status', vstatus.value);
|
|
952
|
-
}
|
|
953
|
-
let msg = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, coding.code]);
|
|
954
|
-
messages.push(msg);
|
|
955
|
-
op.addIssue(new Issue('warning', 'business-rule', path, 'INACTIVE_CONCEPT_FOUND', msg, 'code-comment'));
|
|
956
|
-
} else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
|
|
957
|
-
result.addParamCode('status', vstatus.value);
|
|
958
|
-
let msg = this.worker.i18n.translate('DEPRECATED_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, coding.code]);
|
|
959
|
-
messages.push(msg);
|
|
960
|
-
op.addIssue(new Issue('warning', 'business-rule', path, 'DEPRECATED_CONCEPT_FOUND', msg, 'code-comment'));
|
|
961
|
-
}
|
|
962
|
-
} else if (ok === null) {
|
|
963
|
-
result.AddParamBool('result', false);
|
|
964
|
-
result.addParamStr('message', 'The CodeSystem "' + coding.system + '" is unknown, so the code "' + coding.code + '" is not known to be in the ' + this.valueSet.name);
|
|
965
|
-
for (let us of unknownSystems) {
|
|
966
|
-
result.addParamCanonical('x-caused-by-unknown-system', us);
|
|
967
|
-
}
|
|
968
|
-
} else {
|
|
969
|
-
result.AddParamBool('result', false);
|
|
970
|
-
if (ver.value) {
|
|
971
|
-
result.addParamStr('version', ver.value);
|
|
972
|
-
}
|
|
973
|
-
result.addParamStr('message', 'The system/code "' + coding.system + '"/"' + coding.code + '" is not in the value set ' + this.valueSet.name);
|
|
974
|
-
if (cause.value !== 'null') {
|
|
975
|
-
result.AddParamCode('cause', cause.value);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
if (op.hasIssues) {
|
|
979
|
-
result.addParam('issues').resource = op.jsonObj;
|
|
980
|
-
}
|
|
981
|
-
return result;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
891
|
valueSetDependsOnCodeSystem(url, version) {
|
|
985
892
|
for (let inc of this.valueSet.include) {
|
|
986
893
|
this.worker.deadCheck('valueSetDependsOnCodeSystem');
|
|
@@ -1051,6 +958,7 @@ class ValueSetChecker {
|
|
|
1051
958
|
this.checkCanonicalStatus(issuePath, op, this.valueSet.jsonObj, this.valueSet.jsonObj);
|
|
1052
959
|
let list = new Designations(this.worker.languages);
|
|
1053
960
|
let ok = false;
|
|
961
|
+
let noCode = false;
|
|
1054
962
|
let matchedCodings = new Set();
|
|
1055
963
|
let codelist = '';
|
|
1056
964
|
mt = [];
|
|
@@ -1071,8 +979,22 @@ class ValueSetChecker {
|
|
|
1071
979
|
path = issuePath;
|
|
1072
980
|
}
|
|
1073
981
|
if (!c.code) {
|
|
1074
|
-
|
|
1075
|
-
|
|
982
|
+
// No code at all, so there is nothing to look up and the value set check below has
|
|
983
|
+
// nothing to report either (see noCode). A coding with neither a system nor a code gets
|
|
984
|
+
// ONE message about the pair of them rather than one about each: there is nothing there
|
|
985
|
+
// to validate, and saying so twice does not make it clearer
|
|
986
|
+
if (!c.system && !this.params.inferSystem) {
|
|
987
|
+
let m = this.worker.i18n.translate('Coding_has_no_system_or_code__cannot_validate', this.params.HTTPLanguages, []);
|
|
988
|
+
msg(m);
|
|
989
|
+
op.addIssue(new Issue('warning', 'invalid', path, 'Coding_has_no_system_or_code__cannot_validate', m, 'invalid-data'));
|
|
990
|
+
} else {
|
|
991
|
+
let m = c.system
|
|
992
|
+
? `Coding has no code for system ${c.system} and cannot be validated`
|
|
993
|
+
: `Coding has no code and cannot be validated`;
|
|
994
|
+
msg(m);
|
|
995
|
+
op.addIssue(new Issue('warning', 'invalid', path, null, m, 'invalid-data'));
|
|
996
|
+
}
|
|
997
|
+
noCode = true;
|
|
1076
998
|
break;
|
|
1077
999
|
}
|
|
1078
1000
|
list.clear();
|
|
@@ -1338,7 +1260,13 @@ class ValueSetChecker {
|
|
|
1338
1260
|
// for an internally defined value set (CodeSystem validation), the code-system-level
|
|
1339
1261
|
// message is preferred - but if nothing produced an error, this is the only
|
|
1340
1262
|
// explanation there will be, so it can't be suppressed
|
|
1341
|
-
|
|
1263
|
+
// "none of the provided codes are in the value set" is only meaningful if a code was provided:
|
|
1264
|
+
// for a Coding with no code, the issue above has already said the only thing there is to say.
|
|
1265
|
+
// A CodeableConcept still gets its own summary error, since the concept as a whole did not
|
|
1266
|
+
// validate - but not when the value set is the internal one built for a CodeSystem
|
|
1267
|
+
// $validate-code, where it would name a value set the caller never mentioned
|
|
1268
|
+
let noCodeSummary = noCode && (mode !== 'codeableConcept' || this.valueSet.jsonObj.internallyDefined);
|
|
1269
|
+
if (ok === false && !noCodeSummary && (!this.valueSet.jsonObj.internallyDefined || !op.hasErrors())) {
|
|
1342
1270
|
let mid, m, p;
|
|
1343
1271
|
if (mode === 'codeableConcept') {
|
|
1344
1272
|
mid = 'TX_GENERAL_CC_ERROR_MESSAGE';
|
|
@@ -1539,78 +1467,6 @@ class ValueSetChecker {
|
|
|
1539
1467
|
}
|
|
1540
1468
|
}
|
|
1541
1469
|
|
|
1542
|
-
async checkSystemCode(issuePath, system, version, code) {
|
|
1543
|
-
this.worker.opContext.clearContexts();
|
|
1544
|
-
if (this.params.inferSystem) {
|
|
1545
|
-
this.worker.opContext.addNote(this.valueSet, 'Validate "' + code + '" and infer system', this.indentCount);
|
|
1546
|
-
} else {
|
|
1547
|
-
this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(system, version, code) + '"', this.indentCount);
|
|
1548
|
-
}
|
|
1549
|
-
let unknownSystems = new Set();
|
|
1550
|
-
let unkCodes = [];
|
|
1551
|
-
let messages = [];
|
|
1552
|
-
let result = new Parameters();
|
|
1553
|
-
let op = new OperationOutcome();
|
|
1554
|
-
this.checkCanonicalStatus(issuePath, op, this.valueSet, this.valueSet);
|
|
1555
|
-
let list = new Designations(this.worker.languages);
|
|
1556
|
-
let ver = {value: ''};
|
|
1557
|
-
let inactive = {value: false};
|
|
1558
|
-
let normalForm = {value: ''};
|
|
1559
|
-
let vstatus = {value: ''};
|
|
1560
|
-
let cause = {value: null};
|
|
1561
|
-
let contentMode = {value: null};
|
|
1562
|
-
let impliedSystem = {value: ''};
|
|
1563
|
-
let defLang = {value: null};
|
|
1564
|
-
|
|
1565
|
-
let ok = await this.check(issuePath, system, version, code, true, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, result, contentMode, impliedSystem, unkCodes, messages, defLang);
|
|
1566
|
-
if (ok === true) {
|
|
1567
|
-
result.AddParamBool('result', true);
|
|
1568
|
-
let pd = list.preferredDisplay(this.params.workingLanguages());
|
|
1569
|
-
if (pd) {
|
|
1570
|
-
result.addParamStr('display', pd);
|
|
1571
|
-
}
|
|
1572
|
-
result.addParamUri('system', system);
|
|
1573
|
-
if ((cause.value === 'not-found' && contentMode.value !== 'complete') || contentMode.value === 'example') {
|
|
1574
|
-
result.addParamStr('message', 'The system "' + system + ' was found but did not contain enough information to properly validate the code (mode = ' + contentMode.value + ')');
|
|
1575
|
-
}
|
|
1576
|
-
if (cause.value) {
|
|
1577
|
-
result.addParamCode('cause', cause.value);
|
|
1578
|
-
}
|
|
1579
|
-
if (inactive.value) {
|
|
1580
|
-
result.addParamBool('inactive', inactive.value);
|
|
1581
|
-
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
1582
|
-
result.addParamCode('status', vstatus.value);
|
|
1583
|
-
}
|
|
1584
|
-
let msg = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, code]);
|
|
1585
|
-
messages.push(msg);
|
|
1586
|
-
op.addIssue(new Issue('warning', 'business-rule', 'code', 'INACTIVE_CONCEPT_FOUND', msg, 'code-comment'));
|
|
1587
|
-
} else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
|
|
1588
|
-
result.addParamCode('status', vstatus.value);
|
|
1589
|
-
let msg = this.worker.i18n.translate('DEPRECATED_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, code]);
|
|
1590
|
-
messages.push(msg);
|
|
1591
|
-
op.addIssue(new Issue('warning', 'business-rule', 'code', 'DEPRECATED_CONCEPT_FOUND', msg, 'code-comment'));
|
|
1592
|
-
}
|
|
1593
|
-
} else if (ok === null) {
|
|
1594
|
-
result.AddParamBool('result', false);
|
|
1595
|
-
result.addParamStr('message', 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name);
|
|
1596
|
-
op.addIssue(new Issue('error', cause.value, 'code', null, 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name, 'not-found'));
|
|
1597
|
-
for (let us of unknownSystems) {
|
|
1598
|
-
result.addParamCanonical('x-caused-by-unknown-system', us);
|
|
1599
|
-
}
|
|
1600
|
-
} else {
|
|
1601
|
-
result.AddParamBool('result', false);
|
|
1602
|
-
result.addParamStr('message', 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name);
|
|
1603
|
-
op.addIssue(new Issue('error', cause.value, 'code', null, 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name, 'not-in-vs'));
|
|
1604
|
-
if (cause.value) {
|
|
1605
|
-
result.AddParamCode('cause', cause.value);
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
if (op.hasIssues()) {
|
|
1609
|
-
result.addParam('issues').resource = op.jsonObj;
|
|
1610
|
-
}
|
|
1611
|
-
return result;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
1470
|
/**
|
|
1615
1471
|
* The code as submitted differs from the code as the code system renders it. Report why.
|
|
1616
1472
|
*
|
|
@@ -2046,7 +1902,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2046
1902
|
|
|
2047
1903
|
// Extract coded value
|
|
2048
1904
|
mode = {mode: null};
|
|
2049
|
-
coded = this.extractCodedValue(params, true, mode);
|
|
1905
|
+
coded = this.extractCodedValue(params, true, mode, txp);
|
|
2050
1906
|
if (!coded) {
|
|
2051
1907
|
throw new Issue('error', 'invalid', null, null, 'Unable to find code to validate (looked for coding | codeableConcept | code in parameters)', null, 400).handleAsOO(400);
|
|
2052
1908
|
}
|
|
@@ -2056,7 +1912,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2056
1912
|
if (!codeSystem) {
|
|
2057
1913
|
if (!coded?.coding?.[0].system) {
|
|
2058
1914
|
let msg = this.i18n.translate('Coding_has_no_system__cannot_validate', txp.HTTPLanguages, []);
|
|
2059
|
-
throw new Issue('warning', 'invalid', mode
|
|
1915
|
+
throw new Issue('warning', 'invalid', this.codingPath(mode), 'Coding_has_no_system__cannot_validate', msg, 'invalid-data');
|
|
2060
1916
|
} else {
|
|
2061
1917
|
throw new Issue('error', 'invalid', null, null, 'No CodeSystem specified - provide url parameter or codeSystem resource', null, 400);
|
|
2062
1918
|
}
|
|
@@ -2092,6 +1948,20 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2092
1948
|
}
|
|
2093
1949
|
}
|
|
2094
1950
|
|
|
1951
|
+
/**
|
|
1952
|
+
* The path to the coding itself, for an issue about the coding rather than one of its properties.
|
|
1953
|
+
* A CodeableConcept is validated one coding at a time, so an issue raised before that loop starts
|
|
1954
|
+
* still belongs to the first coding, not to the CodeableConcept - which is what the value set path
|
|
1955
|
+
* reports (see ValueSetChecker.checkCodeableConcept).
|
|
1956
|
+
*/
|
|
1957
|
+
codingPath(mode) {
|
|
1958
|
+
switch (mode.mode) {
|
|
1959
|
+
case 'code': return '';
|
|
1960
|
+
case 'coding': return 'Coding';
|
|
1961
|
+
default: return 'CodeableConcept.coding[0]';
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
2095
1965
|
/**
|
|
2096
1966
|
* Handle an instance-level CodeSystem $validate-code request
|
|
2097
1967
|
* GET/POST /CodeSystem/{id}/$validate-code
|
|
@@ -2119,7 +1989,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2119
1989
|
|
|
2120
1990
|
// Extract coded value
|
|
2121
1991
|
let mode = { mode : null }
|
|
2122
|
-
const coded = this.extractCodedValue(params, true, mode);
|
|
1992
|
+
const coded = this.extractCodedValue(params, true, mode, txp);
|
|
2123
1993
|
if (!coded) {
|
|
2124
1994
|
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
2125
1995
|
'Unable to find code to validate (looked for coding | codeableConcept | code in parameters =codingX:Coding)'));
|
|
@@ -2179,7 +2049,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2179
2049
|
// Extract coded value
|
|
2180
2050
|
|
|
2181
2051
|
let mode = { mode : null };
|
|
2182
|
-
const coded = this.extractCodedValue(params, false, mode);
|
|
2052
|
+
const coded = this.extractCodedValue(params, false, mode, txp);
|
|
2183
2053
|
if (!coded) {
|
|
2184
2054
|
throw new Issue("error", "invalid", null, null, 'Unable to find code to validate (looked for coding | codeableConcept | code+system | code+inferSystem in parameters', null, 422);
|
|
2185
2055
|
}
|
|
@@ -2218,7 +2088,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2218
2088
|
|
|
2219
2089
|
// Extract coded value
|
|
2220
2090
|
let mode = { mode : null };
|
|
2221
|
-
const coded = this.extractCodedValue(params, false, mode);
|
|
2091
|
+
const coded = this.extractCodedValue(params, false, mode, txp);
|
|
2222
2092
|
if (!coded) {
|
|
2223
2093
|
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
2224
2094
|
'Unable to find code to validate (looked for coding | codeableConcept | code in parameters =codingX:Coding)'));
|
|
@@ -2250,7 +2120,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2250
2120
|
if (csResource) {
|
|
2251
2121
|
return new FhirCodeSystemProvider(this.opContext, new CodeSystem(csResource), []); // todo: supplements
|
|
2252
2122
|
}
|
|
2253
|
-
let path = coded == null ? null : mode
|
|
2123
|
+
let path = coded == null ? null : this.systemPath(mode);
|
|
2254
2124
|
let fromCoded = false;
|
|
2255
2125
|
// Check for url parameter
|
|
2256
2126
|
let url = this.getStringParam(params, 'url');
|
|
@@ -2347,14 +2217,22 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2347
2217
|
* Extract the coded value to validate as a CodeableConcept
|
|
2348
2218
|
* @param {Object} params - Parameters resource
|
|
2349
2219
|
* @param {string} mode - 'cs' for CodeSystem, 'vs' for ValueSet
|
|
2220
|
+
* @param {Object} txp - the operation parameters. inferSystem is cleared on these unless the
|
|
2221
|
+
* code parameter is the one being validated - see below
|
|
2350
2222
|
* @returns {Object|null} CodeableConcept or null
|
|
2351
2223
|
*/
|
|
2352
|
-
extractCodedValue(params, isCs, mode) {
|
|
2224
|
+
extractCodedValue(params, isCs, mode, txp) {
|
|
2225
|
+
// inferSystem is defined for the code parameter only: it does not apply to a Coding, nor to
|
|
2226
|
+
// the codings of a CodeableConcept. Those carry their own system (or fail to), and a server
|
|
2227
|
+
// that inferred one here would be answering about a code the caller never named
|
|
2228
|
+
const noInfer = () => { if (txp) { txp.inferSystem = false; } };
|
|
2229
|
+
|
|
2353
2230
|
// Priority 1: codeableConcept parameter
|
|
2354
2231
|
const cc = this.getCodeableConceptParam(params, 'codeableConcept');
|
|
2355
2232
|
if (cc) {
|
|
2356
2233
|
mode.mode = 'codeableConcept';
|
|
2357
2234
|
mode.issuePath = "CodeableConcept";
|
|
2235
|
+
noInfer();
|
|
2358
2236
|
return cc;
|
|
2359
2237
|
}
|
|
2360
2238
|
|
|
@@ -2363,6 +2241,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2363
2241
|
if (coding) {
|
|
2364
2242
|
mode.mode = 'coding';
|
|
2365
2243
|
mode.issuePath = "Coding";
|
|
2244
|
+
noInfer();
|
|
2366
2245
|
return {coding: [coding]};
|
|
2367
2246
|
}
|
|
2368
2247
|
|
package/tx/workers/worker.js
CHANGED
|
@@ -33,6 +33,15 @@ class TerminologyWorker {
|
|
|
33
33
|
foundParameters = [];
|
|
34
34
|
renderer;
|
|
35
35
|
|
|
36
|
+
// ValueSetChecker reads both of these off whatever worker it is given, so they belong to
|
|
37
|
+
// every worker that can construct one - not just the validate and expand workers that
|
|
38
|
+
// happen to have declared them first. A worker without them fails inside the checker with
|
|
39
|
+
// "Cannot read properties of undefined (reading 'size')", a long way from the cause.
|
|
40
|
+
// ValidateWorker and ExpandWorker redeclare them; a subclass field initialiser overrides
|
|
41
|
+
// the base one, so their behaviour is unchanged.
|
|
42
|
+
requiredSupplements = new Set();
|
|
43
|
+
usedSupplements = new Set();
|
|
44
|
+
|
|
36
45
|
/**
|
|
37
46
|
* @param {OperationContext} opContext - Operation context
|
|
38
47
|
* @param {Logger} log - Provider for code systems and resources
|
|
@@ -263,16 +263,23 @@ function conceptMapR5ToR3(r5Obj) {
|
|
|
263
263
|
* @returns {string} R5 relationship value
|
|
264
264
|
* @private
|
|
265
265
|
*/
|
|
266
|
+
// The two vocabularies name the relationship from opposite ends. R3/R4 equivalence
|
|
267
|
+
// describes the TARGET ('wider' = the target is wider than the source); R5 relationship
|
|
268
|
+
// describes the SOURCE ('source-is-narrower-than-target'). So wider <-> narrower-than-
|
|
269
|
+
// target, and narrower <-> broader-than-target. These tables follow the Java convertors
|
|
270
|
+
// (ConceptMap30_50 / ConceptMap40_50), which are the reference.
|
|
266
271
|
function convertEquivalenceToRelationship(equivalence) {
|
|
267
272
|
const equivalenceToRelationship = {
|
|
268
273
|
'relatedto': 'related-to',
|
|
269
274
|
'equivalent': 'equivalent',
|
|
270
275
|
'equal': 'equivalent',
|
|
271
|
-
'wider': 'source-is-
|
|
272
|
-
'subsumes': 'source-is-
|
|
273
|
-
'narrower': 'source-is-
|
|
274
|
-
'specializes': 'source-is-
|
|
275
|
-
'inexact': '
|
|
276
|
+
'wider': 'source-is-narrower-than-target',
|
|
277
|
+
'subsumes': 'source-is-narrower-than-target',
|
|
278
|
+
'narrower': 'source-is-broader-than-target',
|
|
279
|
+
'specializes': 'source-is-broader-than-target',
|
|
280
|
+
'inexact': 'related-to',
|
|
281
|
+
// Java gives no relationship for 'unmatched' (a target-less 'unmatched' is lifted to
|
|
282
|
+
// element.noMap before this is reached); R5 requires one, so not-related-to it is
|
|
276
283
|
'unmatched': 'not-related-to',
|
|
277
284
|
'disjoint': 'not-related-to'
|
|
278
285
|
};
|
|
@@ -289,9 +296,11 @@ function convertRelationshipToEquivalence(relationship) {
|
|
|
289
296
|
const relationshipToEquivalence = {
|
|
290
297
|
'related-to': 'relatedto',
|
|
291
298
|
'equivalent': 'equivalent',
|
|
292
|
-
'source-is-
|
|
293
|
-
'source-is-
|
|
294
|
-
'not-related-to
|
|
299
|
+
'source-is-narrower-than-target': 'wider',
|
|
300
|
+
'source-is-broader-than-target': 'narrower',
|
|
301
|
+
// 'unmatched' is reserved for "no map" (see conceptMapToR5); a real not-related-to
|
|
302
|
+
// target is disjoint, as in the Java convertors
|
|
303
|
+
'not-related-to': 'disjoint'
|
|
295
304
|
};
|
|
296
305
|
return relationshipToEquivalence[relationship] || 'relatedto';
|
|
297
306
|
}
|