fhirsmith 0.13.4 → 0.14.1
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 +61 -1
- package/README.md +16 -3
- package/config-template.json +81 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/library/languages.js +15 -0
- package/library/same-origin.js +62 -0
- package/nginx.md +399 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +241 -56
- package/registry/registry.js +3 -1
- package/server.js +57 -15
- package/stats.js +19 -13
- package/testing/readme.md +144 -0
- package/testing/render.js +723 -0
- package/testing/search.js +378 -0
- package/testing/store.js +410 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +760 -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 +19 -69
- 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/editions.js +70 -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/expand.js +2 -2
- 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 +192 -21
- package/tx/workers/validate.js +70 -187
- package/tx/workers/worker.js +12 -35
- 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
|
|
@@ -938,9 +1085,14 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
938
1085
|
added = await this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result) || added;
|
|
939
1086
|
}
|
|
940
1087
|
}
|
|
1088
|
+
// A map can say, explicitly, that a concept is NOT related to a target (not-related-to,
|
|
1089
|
+
// or R4's unmatched/disjoint). That is still reported as a match - it is what the map
|
|
1090
|
+
// says - but it is not a translation: "The value can only be true if at least one
|
|
1091
|
+
// returned match has a relationship other than 'not-related-to'" (tx-ecosystem translate-2b)
|
|
1092
|
+
const translated = added && result.some(p => p.name === 'match' && this.isPositiveMatch(p));
|
|
941
1093
|
result.push({
|
|
942
1094
|
name: 'result',
|
|
943
|
-
valueBoolean:
|
|
1095
|
+
valueBoolean: translated
|
|
944
1096
|
});
|
|
945
1097
|
if (!added) {
|
|
946
1098
|
result.push({
|
|
@@ -973,6 +1125,25 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
973
1125
|
};
|
|
974
1126
|
}
|
|
975
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* Whether a match counts towards result=true. A noMap match says there is no mapping, and
|
|
1130
|
+
* an explicit negative relationship says the target is not related, so neither is a
|
|
1131
|
+
* translation. A match with no relationship otherwise (an unmapped fallback without one,
|
|
1132
|
+
* a code system supplied translation) still counts.
|
|
1133
|
+
*/
|
|
1134
|
+
isPositiveMatch(match) {
|
|
1135
|
+
const part = (name) => (match.part || []).find(p => p.name === name);
|
|
1136
|
+
if (part('noMap')?.valueBoolean === true) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const rel = part('relationship')?.valueCode;
|
|
1140
|
+
const eq = part('equivalence')?.valueCode;
|
|
1141
|
+
if (eq === 'unmatched' || eq === 'disjoint') {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
return rel !== 'not-related-to';
|
|
1145
|
+
}
|
|
1146
|
+
|
|
976
1147
|
// eslint-disable-next-line no-unused-vars
|
|
977
1148
|
isOkTarget(cm, vs) {
|
|
978
1149
|
// if cm.target != null then
|
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();
|
|
@@ -1166,7 +1088,11 @@ class ValueSetChecker {
|
|
|
1166
1088
|
mt.push(m);
|
|
1167
1089
|
op.addIssue(new Issue('error', 'invalid', p, 'Terminology_TX_System_Relative', m, 'invalid-data'));
|
|
1168
1090
|
}
|
|
1169
|
-
|
|
1091
|
+
// the code isn't in the value set, but it's still looked up in the code system to report on
|
|
1092
|
+
// it - and that has to be the version the value set selects when the coding doesn't say,
|
|
1093
|
+
// not the server default (tx-ecosystem sct validate-code-pc-none)
|
|
1094
|
+
let lookupVersion = c.version || vsImpliedVersion;
|
|
1095
|
+
let prov = await this.worker.findCodeSystem(ws, lookupVersion, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
|
|
1170
1096
|
if (prov === null) {
|
|
1171
1097
|
let vss = await this.worker.findValueSet(ws, '', null);
|
|
1172
1098
|
if (vss !== null) {
|
|
@@ -1176,7 +1102,7 @@ class ValueSetChecker {
|
|
|
1176
1102
|
op.addIssue(new Issue('error', 'invalid', addToPath(path, 'system'), 'Terminology_TX_System_ValueSet2', m, 'invalid-data'));
|
|
1177
1103
|
cause.value = 'invalid';
|
|
1178
1104
|
} else {
|
|
1179
|
-
let provS = await this.worker.findCodeSystem(ws,
|
|
1105
|
+
let provS = await this.worker.findCodeSystem(ws, lookupVersion, this.params, ['supplement'], op,true, true, false, this.worker.requiredSupplements);
|
|
1180
1106
|
if (provS !== null) {
|
|
1181
1107
|
vss = null;
|
|
1182
1108
|
let m = this.worker.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', this.params.HTTPLanguages, [provS.vurl()]);
|
|
@@ -1187,7 +1113,7 @@ class ValueSetChecker {
|
|
|
1187
1113
|
let prov2 = await this.worker.findCodeSystem(ws, '', this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
|
|
1188
1114
|
let bAdd = true;
|
|
1189
1115
|
let m, mid, vn;
|
|
1190
|
-
if (prov2 === null && !
|
|
1116
|
+
if (prov2 === null && !lookupVersion) {
|
|
1191
1117
|
mid = 'UNKNOWN_CODESYSTEM';
|
|
1192
1118
|
m = this.worker.i18n.translate('UNKNOWN_CODESYSTEM', this.params.HTTPLanguages, [ws]);
|
|
1193
1119
|
bAdd = !unknownSystems.has(ws);
|
|
@@ -1201,9 +1127,9 @@ class ValueSetChecker {
|
|
|
1201
1127
|
vn = ws;
|
|
1202
1128
|
} else {
|
|
1203
1129
|
mid = 'UNKNOWN_CODESYSTEM_VERSION';
|
|
1204
|
-
vn = ws + '|' +
|
|
1130
|
+
vn = ws + '|' + lookupVersion;
|
|
1205
1131
|
}
|
|
1206
|
-
m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws,
|
|
1132
|
+
m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws, lookupVersion, this.worker.presentVersionList(vl)]);
|
|
1207
1133
|
bAdd = !unknownSystems.has(vn);
|
|
1208
1134
|
if (bAdd) {
|
|
1209
1135
|
unknownSystems.add(vn);
|
|
@@ -1338,7 +1264,13 @@ class ValueSetChecker {
|
|
|
1338
1264
|
// for an internally defined value set (CodeSystem validation), the code-system-level
|
|
1339
1265
|
// message is preferred - but if nothing produced an error, this is the only
|
|
1340
1266
|
// explanation there will be, so it can't be suppressed
|
|
1341
|
-
|
|
1267
|
+
// "none of the provided codes are in the value set" is only meaningful if a code was provided:
|
|
1268
|
+
// for a Coding with no code, the issue above has already said the only thing there is to say.
|
|
1269
|
+
// A CodeableConcept still gets its own summary error, since the concept as a whole did not
|
|
1270
|
+
// validate - but not when the value set is the internal one built for a CodeSystem
|
|
1271
|
+
// $validate-code, where it would name a value set the caller never mentioned
|
|
1272
|
+
let noCodeSummary = noCode && (mode !== 'codeableConcept' || this.valueSet.jsonObj.internallyDefined);
|
|
1273
|
+
if (ok === false && !noCodeSummary && (!this.valueSet.jsonObj.internallyDefined || !op.hasErrors())) {
|
|
1342
1274
|
let mid, m, p;
|
|
1343
1275
|
if (mode === 'codeableConcept') {
|
|
1344
1276
|
mid = 'TX_GENERAL_CC_ERROR_MESSAGE';
|
|
@@ -1539,78 +1471,6 @@ class ValueSetChecker {
|
|
|
1539
1471
|
}
|
|
1540
1472
|
}
|
|
1541
1473
|
|
|
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
1474
|
/**
|
|
1615
1475
|
* The code as submitted differs from the code as the code system renders it. Report why.
|
|
1616
1476
|
*
|
|
@@ -2046,7 +1906,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2046
1906
|
|
|
2047
1907
|
// Extract coded value
|
|
2048
1908
|
mode = {mode: null};
|
|
2049
|
-
coded = this.extractCodedValue(params, true, mode);
|
|
1909
|
+
coded = this.extractCodedValue(params, true, mode, txp);
|
|
2050
1910
|
if (!coded) {
|
|
2051
1911
|
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
1912
|
}
|
|
@@ -2056,7 +1916,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2056
1916
|
if (!codeSystem) {
|
|
2057
1917
|
if (!coded?.coding?.[0].system) {
|
|
2058
1918
|
let msg = this.i18n.translate('Coding_has_no_system__cannot_validate', txp.HTTPLanguages, []);
|
|
2059
|
-
throw new Issue('warning', 'invalid', mode
|
|
1919
|
+
throw new Issue('warning', 'invalid', this.codingPath(mode), 'Coding_has_no_system__cannot_validate', msg, 'invalid-data');
|
|
2060
1920
|
} else {
|
|
2061
1921
|
throw new Issue('error', 'invalid', null, null, 'No CodeSystem specified - provide url parameter or codeSystem resource', null, 400);
|
|
2062
1922
|
}
|
|
@@ -2092,6 +1952,20 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2092
1952
|
}
|
|
2093
1953
|
}
|
|
2094
1954
|
|
|
1955
|
+
/**
|
|
1956
|
+
* The path to the coding itself, for an issue about the coding rather than one of its properties.
|
|
1957
|
+
* A CodeableConcept is validated one coding at a time, so an issue raised before that loop starts
|
|
1958
|
+
* still belongs to the first coding, not to the CodeableConcept - which is what the value set path
|
|
1959
|
+
* reports (see ValueSetChecker.checkCodeableConcept).
|
|
1960
|
+
*/
|
|
1961
|
+
codingPath(mode) {
|
|
1962
|
+
switch (mode.mode) {
|
|
1963
|
+
case 'code': return '';
|
|
1964
|
+
case 'coding': return 'Coding';
|
|
1965
|
+
default: return 'CodeableConcept.coding[0]';
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
2095
1969
|
/**
|
|
2096
1970
|
* Handle an instance-level CodeSystem $validate-code request
|
|
2097
1971
|
* GET/POST /CodeSystem/{id}/$validate-code
|
|
@@ -2119,7 +1993,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2119
1993
|
|
|
2120
1994
|
// Extract coded value
|
|
2121
1995
|
let mode = { mode : null }
|
|
2122
|
-
const coded = this.extractCodedValue(params, true, mode);
|
|
1996
|
+
const coded = this.extractCodedValue(params, true, mode, txp);
|
|
2123
1997
|
if (!coded) {
|
|
2124
1998
|
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
2125
1999
|
'Unable to find code to validate (looked for coding | codeableConcept | code in parameters =codingX:Coding)'));
|
|
@@ -2179,7 +2053,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2179
2053
|
// Extract coded value
|
|
2180
2054
|
|
|
2181
2055
|
let mode = { mode : null };
|
|
2182
|
-
const coded = this.extractCodedValue(params, false, mode);
|
|
2056
|
+
const coded = this.extractCodedValue(params, false, mode, txp);
|
|
2183
2057
|
if (!coded) {
|
|
2184
2058
|
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
2059
|
}
|
|
@@ -2218,7 +2092,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2218
2092
|
|
|
2219
2093
|
// Extract coded value
|
|
2220
2094
|
let mode = { mode : null };
|
|
2221
|
-
const coded = this.extractCodedValue(params, false, mode);
|
|
2095
|
+
const coded = this.extractCodedValue(params, false, mode, txp);
|
|
2222
2096
|
if (!coded) {
|
|
2223
2097
|
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
2224
2098
|
'Unable to find code to validate (looked for coding | codeableConcept | code in parameters =codingX:Coding)'));
|
|
@@ -2250,7 +2124,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2250
2124
|
if (csResource) {
|
|
2251
2125
|
return new FhirCodeSystemProvider(this.opContext, new CodeSystem(csResource), []); // todo: supplements
|
|
2252
2126
|
}
|
|
2253
|
-
let path = coded == null ? null : mode
|
|
2127
|
+
let path = coded == null ? null : this.systemPath(mode);
|
|
2254
2128
|
let fromCoded = false;
|
|
2255
2129
|
// Check for url parameter
|
|
2256
2130
|
let url = this.getStringParam(params, 'url');
|
|
@@ -2347,14 +2221,22 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2347
2221
|
* Extract the coded value to validate as a CodeableConcept
|
|
2348
2222
|
* @param {Object} params - Parameters resource
|
|
2349
2223
|
* @param {string} mode - 'cs' for CodeSystem, 'vs' for ValueSet
|
|
2224
|
+
* @param {Object} txp - the operation parameters. inferSystem is cleared on these unless the
|
|
2225
|
+
* code parameter is the one being validated - see below
|
|
2350
2226
|
* @returns {Object|null} CodeableConcept or null
|
|
2351
2227
|
*/
|
|
2352
|
-
extractCodedValue(params, isCs, mode) {
|
|
2228
|
+
extractCodedValue(params, isCs, mode, txp) {
|
|
2229
|
+
// inferSystem is defined for the code parameter only: it does not apply to a Coding, nor to
|
|
2230
|
+
// the codings of a CodeableConcept. Those carry their own system (or fail to), and a server
|
|
2231
|
+
// that inferred one here would be answering about a code the caller never named
|
|
2232
|
+
const noInfer = () => { if (txp) { txp.inferSystem = false; } };
|
|
2233
|
+
|
|
2353
2234
|
// Priority 1: codeableConcept parameter
|
|
2354
2235
|
const cc = this.getCodeableConceptParam(params, 'codeableConcept');
|
|
2355
2236
|
if (cc) {
|
|
2356
2237
|
mode.mode = 'codeableConcept';
|
|
2357
2238
|
mode.issuePath = "CodeableConcept";
|
|
2239
|
+
noInfer();
|
|
2358
2240
|
return cc;
|
|
2359
2241
|
}
|
|
2360
2242
|
|
|
@@ -2363,6 +2245,7 @@ class ValidateWorker extends TerminologyWorker {
|
|
|
2363
2245
|
if (coding) {
|
|
2364
2246
|
mode.mode = 'coding';
|
|
2365
2247
|
mode.issuePath = "Coding";
|
|
2248
|
+
noInfer();
|
|
2366
2249
|
return {coding: [coding]};
|
|
2367
2250
|
}
|
|
2368
2251
|
|