fhirsmith 0.13.3 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +48 -4
  3. package/config-template.json +70 -1
  4. package/library/cgroup-memory.js +113 -0
  5. package/library/folder-setup.js +8 -1
  6. package/nginx.md +391 -0
  7. package/package.json +3 -2
  8. package/publisher/git-remote.js +141 -0
  9. package/publisher/github-release.js +195 -0
  10. package/publisher/publisher.js +146 -14
  11. package/server.js +51 -14
  12. package/stats.js +19 -13
  13. package/testing/readme.md +112 -0
  14. package/testing/render.js +482 -0
  15. package/testing/search.js +378 -0
  16. package/testing/store.js +266 -0
  17. package/testing/testing-template.html +126 -0
  18. package/testing/testing.js +486 -0
  19. package/translations/Messages.properties +32 -0
  20. package/tx/closure/closure-store.js +396 -0
  21. package/tx/cs/cs-api.js +6 -1
  22. package/tx/cs/cs-rxnorm.js +35 -8
  23. package/tx/cs/cs-snomed.js +142 -27
  24. package/tx/html/codesystem-operations.liquid +177 -0
  25. package/tx/html/ecl-panel.liquid +93 -0
  26. package/tx/html/home-metrics.liquid +39 -76
  27. package/tx/html/tx-template.html +1 -0
  28. package/tx/library/conceptmap.js +22 -6
  29. package/tx/library/designations.js +12 -3
  30. package/tx/library/extensions.js +2 -2
  31. package/tx/library/operation-outcome.js +3 -1
  32. package/tx/library/renderer.js +42 -5
  33. package/tx/operation-context.js +111 -15
  34. package/tx/provider.js +85 -0
  35. package/tx/sct/expressions.js +128 -44
  36. package/tx/sct/text-index.js +419 -0
  37. package/tx/tx-html.js +426 -44
  38. package/tx/tx.js +117 -9
  39. package/tx/workers/cache-control.js +2 -2
  40. package/tx/workers/closure.js +483 -13
  41. package/tx/workers/expand.js +20 -0
  42. package/tx/workers/metadata.js +29 -4
  43. package/tx/workers/read.js +18 -3
  44. package/tx/workers/subsumes.js +18 -2
  45. package/tx/workers/translate.js +167 -20
  46. package/tx/workers/validate.js +61 -182
  47. package/tx/workers/worker.js +9 -0
  48. package/tx/xversion/xv-conceptmap.js +17 -8
  49. package/utilities/submit-test-report.js +150 -0
@@ -107,9 +107,16 @@ class SubsumesWorker extends TerminologyWorker {
107
107
  codingA = params.get('codingA');
108
108
  codingB = params.get('codingB');
109
109
 
110
+ // A Coding with no system has no defined meaning, so there is nothing to test. This is a
111
+ // problem with the request (400/invalid), not a code system the server can't find
112
+ for (const [name, coding] of [['codingA', codingA], ['codingB', codingB]]) {
113
+ if (!coding.system) {
114
+ throw this.parameterIssue(txp, 'SUBSUMPTION_NO_SYSTEM', [name], name);
115
+ }
116
+ }
110
117
  // Codings must have the same system
111
118
  if (codingA.system !== codingB.system) {
112
- throw new Issue('error', 'not-found', null, null, 'codingA and codingB must have the same system', null, 400);
119
+ throw this.parameterIssue(txp, 'SUBSUMPTION_SYSTEM_MISMATCH', [codingA.system, codingB.system], 'codingB');
113
120
  }
114
121
  // Get the code system provider from the coding's system
115
122
  csProvider = await this.findCodeSystem(codingA.system, codingA.version || '', txp, ['complete'], null, false);
@@ -117,7 +124,7 @@ class SubsumesWorker extends TerminologyWorker {
117
124
  } else if (params.has('codeA') && params.has('codeB')) {
118
125
  // Using codeA, codeB - system is required
119
126
  if (!params.has('system')) {
120
- throw new Issue('error', 'not-found', null, null, 'system parameter is required when using codeA and codeB', null, 404);
127
+ throw this.parameterIssue(txp, 'SUBSUMPTION_NO_SYSTEM', ['codeA'], 'codeA');
121
128
  }
122
129
 
123
130
  csProvider = await this.findCodeSystem(params.get('system'), params.get('version') || '', txp, ['complete'], null, false);
@@ -318,6 +325,15 @@ class SubsumesWorker extends TerminologyWorker {
318
325
  };
319
326
  }
320
327
 
328
+ /**
329
+ * Build the Issue for a request that is malformed - a missing or unusable parameter. These
330
+ * are 400 / invalid: the request can't be processed as given, whatever the server knows
331
+ */
332
+ parameterIssue(txp, msgId, args, path) {
333
+ const msg = this.i18n.translate(msgId, txp.HTTPLanguages, args);
334
+ return new Issue('error', 'invalid', path, msgId, msg, 'invalid-data', 400);
335
+ }
336
+
321
337
  /**
322
338
  * Build the Issue for a code that isn't in the code system. Uses the same message ids
323
339
  * and tx-issue-type detail code as $validate-code, so clients get consistent errors
@@ -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
@@ -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();
@@ -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
- if (ok === false && (!this.valueSet.jsonObj.internallyDefined || !op.hasErrors())) {
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.issuePath, 'Coding_has_no_system__cannot_validate', msg, 'invalid-data');
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.issuePath+".system";
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
 
@@ -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