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.
Files changed (52) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +16 -3
  3. package/config-template.json +81 -1
  4. package/library/cgroup-memory.js +113 -0
  5. package/library/folder-setup.js +8 -1
  6. package/library/languages.js +15 -0
  7. package/library/same-origin.js +62 -0
  8. package/nginx.md +399 -0
  9. package/package.json +3 -2
  10. package/publisher/git-remote.js +141 -0
  11. package/publisher/github-release.js +195 -0
  12. package/publisher/publisher-template.html +1 -0
  13. package/publisher/publisher.js +241 -56
  14. package/registry/registry.js +3 -1
  15. package/server.js +57 -15
  16. package/stats.js +19 -13
  17. package/testing/readme.md +144 -0
  18. package/testing/render.js +723 -0
  19. package/testing/search.js +378 -0
  20. package/testing/store.js +410 -0
  21. package/testing/testing-template.html +126 -0
  22. package/testing/testing.js +760 -0
  23. package/translations/Messages.properties +32 -0
  24. package/tx/closure/closure-store.js +396 -0
  25. package/tx/cs/cs-api.js +6 -1
  26. package/tx/cs/cs-rxnorm.js +35 -8
  27. package/tx/cs/cs-snomed.js +19 -69
  28. package/tx/html/codesystem-operations.liquid +177 -0
  29. package/tx/html/ecl-panel.liquid +93 -0
  30. package/tx/html/home-metrics.liquid +39 -76
  31. package/tx/html/tx-template.html +1 -0
  32. package/tx/library/conceptmap.js +22 -6
  33. package/tx/library/designations.js +12 -3
  34. package/tx/library/operation-outcome.js +3 -1
  35. package/tx/library/renderer.js +42 -5
  36. package/tx/operation-context.js +80 -17
  37. package/tx/provider.js +85 -0
  38. package/tx/sct/editions.js +70 -0
  39. package/tx/sct/expressions.js +128 -44
  40. package/tx/tx-html.js +426 -44
  41. package/tx/tx.js +117 -9
  42. package/tx/workers/cache-control.js +2 -2
  43. package/tx/workers/closure.js +483 -13
  44. package/tx/workers/expand.js +2 -2
  45. package/tx/workers/metadata.js +8 -3
  46. package/tx/workers/read.js +18 -3
  47. package/tx/workers/subsumes.js +18 -2
  48. package/tx/workers/translate.js +192 -21
  49. package/tx/workers/validate.js +70 -187
  50. package/tx/workers/worker.js +12 -35
  51. package/tx/xversion/xv-conceptmap.js +17 -8
  52. package/utilities/submit-test-report.js +150 -0
@@ -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
- if (conceptMaps.length == 0) {
273
- // The client did not nominate a map, and this server knows none that covers this
274
- // source and target. That is an answer - there is no translation - not an error;
275
- // a nominated map that cannot be found is the error case, and is reported when
276
- // the url parameter is resolved above.
277
- const from = reverse ? (targetSystem || sourceScope) : coding.system;
278
- const to = reverse ? coding.system : (targetSystem || targetScope);
279
- return res.status(200).json({
280
- resourceType: 'Parameters',
281
- parameter: [{
282
- name: 'message',
283
- valueString: `No ConceptMap is available to translate from '${from || '(unspecified)'}' to '${to || '(unspecified)'}'`
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(conceptMaps, coding, targetScope, targetSystem, params);
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: added
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
@@ -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
- if (pid) {
90
- op.addIssueIfNew(new Issue('information', 'business-rule', '', 'MSG_DRAFT', this.worker.i18n.translate('MSG_DRAFT_SRC', this.params.HTTPLanguages, [vurl, pid, rtype]), 'status-check'), false);
91
- } else {
92
- op.addIssueIfNew(new Issue('information', 'business-rule', '', 'MSG_DRAFT', this.worker.i18n.translate('MSG_DRAFT', this.params.HTTPLanguages, [vurl, '', rtype]), 'status-check'), false);
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
- let msg = `Coding has no code for system ${c.system} and cannot be validated`;
1075
- op.addIssue(new Issue('error', 'invalid', path, null, msg, 'invalid-data'));
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
- let prov = await this.worker.findCodeSystem(ws, c.version, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
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, c.version, this.params, ['supplement'], op,true, true, false, this.worker.requiredSupplements);
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 && !c.version) {
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 + '|' + c.version;
1130
+ vn = ws + '|' + lookupVersion;
1205
1131
  }
1206
- m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws, c.version, this.worker.presentVersionList(vl)]);
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
- if (ok === false && (!this.valueSet.jsonObj.internallyDefined || !op.hasErrors())) {
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.issuePath, 'Coding_has_no_system__cannot_validate', msg, 'invalid-data');
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.issuePath+".system";
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