fhirsmith 0.12.0 → 0.13.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 (57) hide show
  1. package/CHANGELOG.md +323 -4
  2. package/README.md +1 -1
  3. package/config-template.json +13 -0
  4. package/library/languages.js +191 -34
  5. package/library/path-safety.js +51 -0
  6. package/library/request-token.js +56 -0
  7. package/package.json +2 -2
  8. package/packages/packages.js +17 -118
  9. package/publisher/publisher.js +60 -30
  10. package/publisher/validation.js +15 -3
  11. package/server.js +12 -3
  12. package/translations/Messages.properties +1 -0
  13. package/tx/README.md +21 -0
  14. package/tx/cs/cs-api.js +30 -1
  15. package/tx/cs/cs-cs.js +14 -1
  16. package/tx/cs/cs-icd11.js +844 -0
  17. package/tx/cs/cs-lang.js +331 -85
  18. package/tx/cs/cs-loinc.js +45 -4
  19. package/tx/cs/cs-mimetypes.js +492 -21
  20. package/tx/cs/cs-snomed.js +38 -4
  21. package/tx/cs/cs-ucum.js +33 -3
  22. package/tx/cs/icd11-expressions.js +243 -0
  23. package/tx/importers/export-icd11-codesystem.js +823 -0
  24. package/tx/importers/icd11-schema.md +285 -0
  25. package/tx/importers/import-icd11.js +861 -0
  26. package/tx/importers/import-loinc.module.js +14 -7
  27. package/tx/importers/import-sct.module.js +10 -1
  28. package/tx/library/codesystem.js +21 -27
  29. package/tx/library/nesting-limit.js +79 -0
  30. package/tx/library/operation-outcome.js +97 -3
  31. package/tx/library/valueset.js +16 -20
  32. package/tx/library.js +23 -1
  33. package/tx/ocl/cache/cache-utils.cjs +5 -3
  34. package/tx/ocl/cs-ocl.cjs +73 -17
  35. package/tx/ocl/jobs/background-queue.cjs +6 -3
  36. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  37. package/tx/ocl/shared/patches.cjs +48 -1
  38. package/tx/ocl/vs-ocl.cjs +15 -13
  39. package/tx/provider.js +1 -1
  40. package/tx/sct/expressions.js +194 -17
  41. package/tx/tx.fhir.org.yml +2 -1
  42. package/tx/tx.js +15 -42
  43. package/tx/workers/batch-validate.js +6 -16
  44. package/tx/workers/batch.js +16 -28
  45. package/tx/workers/cache-control.js +6 -12
  46. package/tx/workers/closure.js +3 -8
  47. package/tx/workers/compare.js +6 -34
  48. package/tx/workers/expand.js +36 -45
  49. package/tx/workers/lookup.js +21 -39
  50. package/tx/workers/read.js +6 -40
  51. package/tx/workers/search.js +2 -8
  52. package/tx/workers/subsumes.js +58 -41
  53. package/tx/workers/translate.js +11 -17
  54. package/tx/workers/validate.js +76 -77
  55. package/tx/workers/worker.js +70 -1
  56. package/tx/xml/xml-base.js +243 -76
  57. package/tx/xversion/xv-valueset.js +39 -1
@@ -11,7 +11,7 @@
11
11
  const { TerminologyWorker } = require('./worker');
12
12
  const {TxParameters} = require("../params");
13
13
  const {Extensions} = require("../library/extensions");
14
- const {Issue, OperationOutcome} = require("../library/operation-outcome");
14
+ const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
15
15
  const ValueSet = require("../library/valueset");
16
16
  const {ValueSetExpander} = require("./expand");
17
17
  const {SearchFilterText} = require("../library/designations");
@@ -59,18 +59,7 @@ class CompareWorker extends TerminologyWorker {
59
59
  oo.addIssue(error);
60
60
  return res.status(error.statusCode || 500).json(oo.jsonObj);
61
61
  } else {
62
- const issueCode = error.issueCode || 'exception';
63
- return res.status(statusCode).json({
64
- resourceType: 'OperationOutcome',
65
- issue: [{
66
- severity: 'error',
67
- code: issueCode,
68
- details: {
69
- text: error.message
70
- },
71
- diagnostics: error.message
72
- }]
73
- });
62
+ return res.status(statusCode).json(outcomeFromError(error));
74
63
  }
75
64
  }
76
65
  }
@@ -89,18 +78,7 @@ class CompareWorker extends TerminologyWorker {
89
78
  debugLog(error);
90
79
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
91
80
  const statusCode = error.statusCode || 500;
92
- const issueCode = error.issueCode || 'exception';
93
- return res.status(statusCode).json({
94
- resourceType: 'OperationOutcome',
95
- issue: [{
96
- severity: 'error',
97
- code: issueCode,
98
- details: {
99
- text : error.message
100
- },
101
- diagnostics: error.message
102
- }]
103
- });
81
+ return res.status(statusCode).json(outcomeFromError(error));
104
82
  }
105
83
  }
106
84
 
@@ -158,15 +136,9 @@ class CompareWorker extends TerminologyWorker {
158
136
  * @param {string} message - Diagnostic message
159
137
  * @returns {Object} OperationOutcome resource
160
138
  */
161
- operationOutcome(severity, code, message) {
162
- return {
163
- resourceType: 'OperationOutcome',
164
- issue: [{
165
- severity,
166
- code,
167
- diagnostics: message
168
- }]
169
- };
139
+ operationOutcome(severity, code, message, txIssueType = null) {
140
+ // the shared builder, so that every outcome has details.text and a tx-issue-type coding
141
+ return buildOperationOutcome(severity, code, message, txIssueType);
170
142
  }
171
143
 
172
144
  async readValueSet(res, prefix, params) {
@@ -14,7 +14,7 @@ const {Designations, SearchFilterText} = require("../library/designations");
14
14
  const {Extensions} = require("../library/extensions");
15
15
  const {getValuePrimitive, getValueName, validateParameter} = require("../../library/utilities");
16
16
  const {div} = require("../../library/html");
17
- const {Issue, OperationOutcome} = require("../library/operation-outcome");
17
+ const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
18
18
  const crypto = require('crypto');
19
19
  const ValueSet = require("../library/valueset");
20
20
  const {VersionUtilities} = require("../../library/version-utilities");
@@ -447,8 +447,21 @@ class ValueSetExpander {
447
447
  }
448
448
  }
449
449
 
450
+ // '*' asks for every property the code system defines for the concept (R6's
451
+ // ValueSet.compose.property allows it, and a request can name it too). It is handled
452
+ // here rather than by expanding it into a list of names, because which properties
453
+ // exist is a property of the concept, not of the request
454
+ if (this.params.properties.includes('*') && csProps != null && cs != null) {
455
+ for (const cp of csProps) {
456
+ const vn = getValueName(cp);
457
+ this.defineProperty(expansion, n, this.getPropUrl(cs, cp.code, cp), cp.code, vn, cp[vn], true);
458
+ }
459
+ }
460
+
450
461
  for (const pn of this.params.properties) {
451
- if (pn === 'definition') {
462
+ if (pn === '*') {
463
+ continue; // done above
464
+ } else if (pn === 'definition') {
452
465
  if (definition) {
453
466
  this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#definition', pn, "valueString", definition);
454
467
  }
@@ -1094,7 +1107,7 @@ class ValueSetExpander {
1094
1107
  this.worker.opContext.log('prep filters');
1095
1108
  const prep = await cs.getPrepContext(true);
1096
1109
  if (!filter.isNull) {
1097
- await cs.searchFilter(filter, prep, true);
1110
+ await cs.searchFilter(prep, filter, true);
1098
1111
  }
1099
1112
 
1100
1113
  if (cs.specialEnumeration()) {
@@ -1699,16 +1712,23 @@ class ValueSetExpander {
1699
1712
  if (!expansion.property) {
1700
1713
  expansion.property = [];
1701
1714
  }
1702
- let pd = expansion.property.find(t1 => t1.uri == url || t1.code == code);
1715
+ // match on the uri only when there is one. A code system may declare several properties with
1716
+ // no uri at all, and those are told apart by their code - matching on an undefined uri would
1717
+ // collapse all of them into whichever one was declared first
1718
+ let pd = url
1719
+ ? expansion.property.find(t1 => t1.uri == url || t1.code == code)
1720
+ : expansion.property.find(t1 => t1.code == code);
1703
1721
  if (!pd) {
1704
1722
  pd = {};
1705
1723
  expansion.property.push(pd);
1706
- pd.uri = url;
1707
1724
  pd.code = code;
1708
- } else if (!pd.uri) {
1725
+ if (url) {
1726
+ pd.uri = url;
1727
+ }
1728
+ } else if (url && !pd.uri) {
1709
1729
  pd.uri = url;
1710
1730
  }
1711
- if (pd.uri != url) {
1731
+ if (url && pd.uri != url) {
1712
1732
  throw new Error('URL mismatch on expansion: ' + pd.uri + ' vs ' + url + ' for code ' + code);
1713
1733
  }
1714
1734
  return pd.code;
@@ -1738,7 +1758,7 @@ class ValueSetExpander {
1738
1758
  }
1739
1759
  }
1740
1760
  for (const definition of definitions) {
1741
- if (definition.uri && used.has(definition.code)) {
1761
+ if (used.has(definition.code)) {
1742
1762
  this.declareProperty(expansion, definition.uri, definition.code);
1743
1763
  }
1744
1764
  }
@@ -1754,10 +1774,9 @@ class ValueSetExpander {
1754
1774
  if (value === undefined || value == null) {
1755
1775
  return;
1756
1776
  }
1757
- // we only define it if the code system has a definition
1758
- if (url) {
1759
- code = this.declareProperty(expansion, url, code);
1760
- }
1777
+ // every property that appears in contains.property has to be declared in expansion.property,
1778
+ // whether or not the code system gave it a uri
1779
+ code = this.declareProperty(expansion, url, code);
1761
1780
 
1762
1781
  if (!contains.property) {
1763
1782
  contains.property = [];
@@ -1950,18 +1969,7 @@ class ExpandWorker extends TerminologyWorker {
1950
1969
  oo.addIssue(error);
1951
1970
  return res.status(error.statusCode || 500).json(oo.jsonObj);
1952
1971
  } else {
1953
- const issueCode = error.issueCode || 'exception';
1954
- return res.status(statusCode).json({
1955
- resourceType: 'OperationOutcome',
1956
- issue: [{
1957
- severity: 'error',
1958
- code: issueCode,
1959
- details: {
1960
- text: error.message
1961
- },
1962
- diagnostics: error.message
1963
- }]
1964
- });
1972
+ return res.status(statusCode).json(outcomeFromError(error));
1965
1973
  }
1966
1974
  }
1967
1975
  }
@@ -1980,18 +1988,7 @@ class ExpandWorker extends TerminologyWorker {
1980
1988
  debugLog(error);
1981
1989
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
1982
1990
  const statusCode = error.statusCode || 500;
1983
- const issueCode = error.issueCode || 'exception';
1984
- return res.status(statusCode).json({
1985
- resourceType: 'OperationOutcome',
1986
- issue: [{
1987
- severity: 'error',
1988
- code: issueCode,
1989
- details: {
1990
- text : error.message
1991
- },
1992
- diagnostics: error.message
1993
- }]
1994
- });
1991
+ return res.status(statusCode).json(outcomeFromError(error));
1995
1992
  }
1996
1993
  }
1997
1994
 
@@ -2253,15 +2250,9 @@ class ExpandWorker extends TerminologyWorker {
2253
2250
  * @param {string} message - Diagnostic message
2254
2251
  * @returns {Object} OperationOutcome resource
2255
2252
  */
2256
- operationOutcome(severity, code, message) {
2257
- return {
2258
- resourceType: 'OperationOutcome',
2259
- issue: [{
2260
- severity,
2261
- code,
2262
- diagnostics: message
2263
- }]
2264
- };
2253
+ operationOutcome(severity, code, message, txIssueType = null) {
2254
+ // the shared builder, so that every outcome has details.text and a tx-issue-type coding
2255
+ return buildOperationOutcome(severity, code, message, txIssueType);
2265
2256
  }
2266
2257
 
2267
2258
  }
@@ -12,7 +12,7 @@ const { FhirCodeSystemProvider } = require('../cs/cs-cs');
12
12
  const { Designations} = require("../library/designations");
13
13
  const {TxParameters} = require("../params");
14
14
  const {Parameters} = require("../library/parameters");
15
- const {Issue, OperationOutcome} = require("../library/operation-outcome");
15
+ const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
16
16
  const {debugLog} = require("../operation-context");
17
17
  const {checkAddedParameters} = require("../library/parameter-check");
18
18
 
@@ -49,16 +49,7 @@ class LookupWorker extends TerminologyWorker {
49
49
  this.log.error(error);
50
50
  debugLog(error);
51
51
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
52
- const statusCode = error.statusCode || 500;
53
- const issueCode = error.issueCode || 'exception';
54
- return res.status(statusCode).json({
55
- resourceType: 'OperationOutcome',
56
- issue: [{
57
- severity: 'error',
58
- code: issueCode,
59
- diagnostics: error.message
60
- }]
61
- });
52
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
62
53
  }
63
54
  }
64
55
 
@@ -76,15 +67,7 @@ class LookupWorker extends TerminologyWorker {
76
67
  this.log.error(error);
77
68
  debugLog(error);
78
69
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
79
- const issueCode = error.issueCode || 'exception';
80
- return res.status(400).json({
81
- resourceType: 'OperationOutcome',
82
- issue: [{
83
- severity: 'error',
84
- code: issueCode,
85
- diagnostics: error.message
86
- }]
87
- });
70
+ return res.status(400).json(outcomeFromError(error));
88
71
  }
89
72
  }
90
73
 
@@ -140,8 +123,11 @@ class LookupWorker extends TerminologyWorker {
140
123
  }
141
124
 
142
125
  if (!csProvider) {
143
- const systemUrl = params.system || params.coding?.system;
144
- const versionStr = params.version || params.coding?.version;
126
+ // params is a Map-like: params.system is always undefined, so the message read
127
+ // "CodeSystem not found: undefined" for every unknown system or version.
128
+ const coding = params.has('coding') ? params.get('coding') : null;
129
+ const systemUrl = params.has('system') ? params.get('system') : coding?.system;
130
+ const versionStr = params.has('version') ? params.get('version') : coding?.version;
145
131
  const msg = versionStr
146
132
  ? `CodeSystem not found: ${systemUrl} version ${versionStr}`
147
133
  : `CodeSystem not found: ${systemUrl}`;
@@ -169,8 +155,7 @@ class LookupWorker extends TerminologyWorker {
169
155
  oo.addIssue(error);
170
156
  return res.status(error.statusCode || 500).json(oo.jsonObj);
171
157
  } else {
172
- return res.status(error.statusCode || 500).json(this.operationOutcome(
173
- 'error', error.issueCode || 'exception', error.message));
158
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
174
159
  }
175
160
  }
176
161
  }
@@ -233,8 +218,7 @@ class LookupWorker extends TerminologyWorker {
233
218
  oo.addIssue(error);
234
219
  return res.status(error.statusCode || 500).json(oo.jsonObj);
235
220
  } else {
236
- return res.status(error.statusCode || 500).json(this.operationOutcome(
237
- 'error', error.issueCode || 'exception', error.message));
221
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
238
222
  }
239
223
  }
240
224
  }
@@ -271,7 +255,9 @@ class LookupWorker extends TerminologyWorker {
271
255
  if (locateResult?.message) {
272
256
  message += ' ('+locateResult.message+')';
273
257
  }
274
- throw new Issue('error', 'not-found', null, null, message, null, 404);
258
+ // invalid-code, not not-found: the code system was found, the code in it was not.
259
+ // not-found is for a code system or value set that could not be resolved at all.
260
+ throw new Issue('error', 'not-found', null, null, message, 'invalid-code', 404);
275
261
  }
276
262
 
277
263
  const ctxt = locateResult.context;
@@ -441,21 +427,17 @@ class LookupWorker extends TerminologyWorker {
441
427
  }
442
428
 
443
429
  /**
444
- * Build an OperationOutcome
430
+ * Build an OperationOutcome. Delegates to the shared builder so that every outcome this
431
+ * server emits has details.text and a tx-issue-type coding: diagnostics is stripped by
432
+ * the test harness, so nothing a caller needs may live there.
445
433
  * @param {string} severity - error, warning, information
446
- * @param {string} code - Issue code
447
- * @param {string} message - Diagnostic message
434
+ * @param {string} code - FHIR issue type
435
+ * @param {string} message - the human readable account of the problem
436
+ * @param {string} [txIssueType] - tx-issue-type; defaulted from code when not given
448
437
  * @returns {Object} OperationOutcome resource
449
438
  */
450
- operationOutcome(severity, code, message) {
451
- return {
452
- resourceType: 'OperationOutcome',
453
- issue: [{
454
- severity,
455
- code,
456
- details: {text : message}
457
- }]
458
- };
439
+ operationOutcome(severity, code, message, txIssueType = null) {
440
+ return buildOperationOutcome(severity, code, message, txIssueType);
459
441
  }
460
442
  }
461
443
 
@@ -7,6 +7,7 @@
7
7
  const { TerminologyWorker } = require('./worker');
8
8
  const {debugLog} = require("../operation-context");
9
9
 
10
+ const { buildOperationOutcome } = require('../library/operation-outcome');
10
11
  class ReadWorker extends TerminologyWorker {
11
12
  /**
12
13
  * @param {OperationContext} opContext - Operation context
@@ -50,27 +51,13 @@ class ReadWorker extends TerminologyWorker {
50
51
  return await this.handleConceptMap(req, res, id);
51
52
 
52
53
  default:
53
- return res.status(404).json({
54
- resourceType: 'OperationOutcome',
55
- issue: [{
56
- severity: 'error',
57
- code: 'not-found',
58
- diagnostics: `Unknown resource type: ${resourceType}`
59
- }]
60
- });
54
+ return res.status(404).json(buildOperationOutcome('error', 'not-found', `Unknown resource type: ${resourceType}`));
61
55
  }
62
56
  } catch (error) {
63
57
  this.log.error(error);
64
58
  debugLog(error);
65
59
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
66
- return res.status(500).json({
67
- resourceType: 'OperationOutcome',
68
- issue: [{
69
- severity: 'error',
70
- code: 'exception',
71
- diagnostics: error.message
72
- }]
73
- });
60
+ return res.status(500).json(buildOperationOutcome('error', 'exception', error.message));
74
61
  }
75
62
  }
76
63
 
@@ -127,14 +114,7 @@ class ReadWorker extends TerminologyWorker {
127
114
  }
128
115
  }
129
116
 
130
- return res.status(404).json({
131
- resourceType: 'OperationOutcome',
132
- issue: [{
133
- severity: 'error',
134
- code: 'not-found',
135
- diagnostics: `CodeSystem/${id} not found`
136
- }]
137
- });
117
+ return res.status(404).json(buildOperationOutcome('error', 'not-found', `CodeSystem/${id} not found`));
138
118
  }
139
119
 
140
120
  /**
@@ -151,14 +131,7 @@ class ReadWorker extends TerminologyWorker {
151
131
  }
152
132
  }
153
133
 
154
- return res.status(404).json({
155
- resourceType: 'OperationOutcome',
156
- issue: [{
157
- severity: 'error',
158
- code: 'not-found',
159
- diagnostics: `ValueSet/${id} not found`
160
- }]
161
- });
134
+ return res.status(404).json(buildOperationOutcome('error', 'not-found', `ValueSet/${id} not found`));
162
135
  }
163
136
  /**
164
137
  * Handle ConceptMap read
@@ -174,14 +147,7 @@ class ReadWorker extends TerminologyWorker {
174
147
  }
175
148
  }
176
149
 
177
- return res.status(404).json({
178
- resourceType: 'OperationOutcome',
179
- issue: [{
180
- severity: 'error',
181
- code: 'not-found',
182
- diagnostics: `ConceptMap/${id} not found`
183
- }]
184
- });
150
+ return res.status(404).json(buildOperationOutcome('error', 'not-found', `ConceptMap/${id} not found`));
185
151
  }
186
152
  }
187
153
 
@@ -10,6 +10,7 @@ const {Utilities} = require("../../library/utilities");
10
10
  const {debugLog} = require("../operation-context");
11
11
  const {acceptsHtml} = require("../tx-html");
12
12
 
13
+ const { buildOperationOutcome } = require('../library/operation-outcome');
13
14
  class SearchWorker extends TerminologyWorker {
14
15
  /**
15
16
  * @param {OperationContext} opContext - Operation context
@@ -127,14 +128,7 @@ class SearchWorker extends TerminologyWorker {
127
128
  this.log.error(error);
128
129
  debugLog(error);
129
130
  req.logInfo = "error "+(error.msgId || error.className);
130
- return res.status(500).json({
131
- resourceType: 'OperationOutcome',
132
- issue: [{
133
- severity: 'error',
134
- code: 'exception',
135
- diagnostics: error.message
136
- }]
137
- });
131
+ return res.status(500).json(buildOperationOutcome('error', 'exception', error.message));
138
132
  }
139
133
  }
140
134
 
@@ -7,7 +7,7 @@
7
7
  // POST /CodeSystem/{id}/$subsumes
8
8
  //
9
9
 
10
- const { TerminologyWorker } = require('./worker');
10
+ const { TerminologyWorker, Unknown_Code_in_VersionSCT, SCTVersion } = require('./worker');
11
11
  const { FhirCodeSystemProvider } = require('../cs/cs-cs');
12
12
  const {TxParameters} = require("../params");
13
13
  const {Parameters} = require("../library/parameters");
@@ -51,8 +51,7 @@ class SubsumesWorker extends TerminologyWorker {
51
51
  oo.addIssue(error);
52
52
  return res.status(error.statusCode || 500).json(oo.jsonObj);
53
53
  } else {
54
- return res.status(error.statusCode || 500).json(this.operationOutcome(
55
- 'error', error.issueCode || 'exception', error.message));
54
+ return res.status(error.statusCode || 500).json(this.unexpectedErrorOutcome(error));
56
55
  }
57
56
  }
58
57
  }
@@ -74,8 +73,7 @@ class SubsumesWorker extends TerminologyWorker {
74
73
  oo.addIssue(error);
75
74
  return res.status(error.statusCode || 500).json(oo.jsonObj);
76
75
  } else {
77
- return res.status(error.statusCode || 500).json(this.operationOutcome(
78
- 'error', error.issueCode || 'exception', error.message));
76
+ return res.status(error.statusCode || 500).json(this.unexpectedErrorOutcome(error));
79
77
  }
80
78
  }
81
79
  }
@@ -87,13 +85,14 @@ class SubsumesWorker extends TerminologyWorker {
87
85
  async handleTypeLevelSubsumes(req, res) {
88
86
  this.deadCheck('subsumes-type-level');
89
87
 
90
- // Handle tx-resource and cache-id parameters from Parameters resource
91
- if (req.body && req.body.resourceType === 'Parameters') {
92
- this.setupAdditionalResources(req.body);
93
- }
94
-
95
- // Parse parameters from request
88
+ // Parse the parameters first, then hand them to setupAdditionalResources. It used to be
89
+ // handed req.body, which meant a GET (no body) never reached the cache-id handling: when a
90
+ // client front-loads a suite's resources into a server-side cache the request carries no
91
+ // tx-resource at all, only the X-Cache-Id header, and every code system supplied that way
92
+ // was invisible. On a GET parseParameters turns the query string into the same Parameters
93
+ // shape, and the cache-id comes off the operation context
96
94
  const params = new Parameters(this.parseParameters(req));
95
+ this.setupAdditionalResources(params.jsonObj);
97
96
  const txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n);
98
97
  txp.readParams(params.jsonObj);
99
98
 
@@ -101,7 +100,9 @@ class SubsumesWorker extends TerminologyWorker {
101
100
  let codingA, codingB;
102
101
  let csProvider;
103
102
 
103
+ let names = ['codeA', 'codeB'];
104
104
  if (params.has('codingA') && params.has('codingB')) {
105
+ names = ['codingA', 'codingB'];
105
106
  // Using codingA and codingB (only from Parameters resource)
106
107
  codingA = params.get('codingA');
107
108
  codingB = params.get('codingB');
@@ -138,7 +139,7 @@ class SubsumesWorker extends TerminologyWorker {
138
139
  }
139
140
 
140
141
  // Perform the subsumes check
141
- const result = await this.doSubsumes(csProvider, codingA, codingB);
142
+ const result = await this.doSubsumes(csProvider, codingA, codingB, txp, names);
142
143
  req.logInfo = this.usedSources.join("|")+txp.logInfo();
143
144
  return res.status(200).json(result);
144
145
  }
@@ -159,13 +160,9 @@ class SubsumesWorker extends TerminologyWorker {
159
160
  throw new Issue('error', 'not found', null, null, `CodeSystem/${id} not found`, null, 404);
160
161
  }
161
162
 
162
- // Handle tx-resource and cache-id parameters from Parameters resource
163
- if (req.body && req.body.resourceType === 'Parameters') {
164
- this.setupAdditionalResources(req.body);
165
- }
166
-
167
- // Parse parameters from request
163
+ // See handleTypeLevelSubsumes: parse first, so a GET reaches the cache-id handling too
168
164
  const params = new Parameters(this.parseParameters(req));
165
+ this.setupAdditionalResources(params.jsonObj);
169
166
  const txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n);
170
167
  txp.readParams(params.jsonObj);
171
168
 
@@ -178,7 +175,9 @@ class SubsumesWorker extends TerminologyWorker {
178
175
  // Get the codings
179
176
  let codingA, codingB;
180
177
 
178
+ let names = ['codeA', 'codeB'];
181
179
  if (params.has('codingA') && params.has('codingB')) {
180
+ names = ['codingA', 'codingB'];
182
181
  codingA = params.get('codingA');
183
182
  codingB = params.get('codingB');
184
183
  } else if (params.has('codeA') && params.has('codeB')) {
@@ -198,7 +197,7 @@ class SubsumesWorker extends TerminologyWorker {
198
197
  }
199
198
 
200
199
  // Perform the subsumes check
201
- const result = await this.doSubsumes(csProvider, codingA, codingB);
200
+ const result = await this.doSubsumes(csProvider, codingA, codingB, txp, names);
202
201
  req.logInfo = this.usedSources.join("|")+txp.logInfo();
203
202
  return res.json(result);
204
203
  }
@@ -263,9 +262,11 @@ class SubsumesWorker extends TerminologyWorker {
263
262
  * @param {CodeSystemProvider} csProvider - CodeSystem provider
264
263
  * @param {Object} codingA - First coding
265
264
  * @param {Object} codingB - Second coding
265
+ * @param {TxParameters} txp - parsed parameters (for languages)
266
+ * @param {Array<string>} names - the parameters the two codes came from, for `expression`
266
267
  * @returns {Object} Parameters resource with subsumes result
267
268
  */
268
- async doSubsumes(csProvider, codingA, codingB) {
269
+ async doSubsumes(csProvider, codingA, codingB, txp, names = ['codeA', 'codeB']) {
269
270
  this.deadCheck('doSubsumes');
270
271
 
271
272
  const csSystem = csProvider.system();
@@ -287,18 +288,12 @@ class SubsumesWorker extends TerminologyWorker {
287
288
  // Validate both codes exist
288
289
  const locateA = await csProvider.locate(codingA.code);
289
290
  if (!locateA || !locateA.context) {
290
- const error = new Error(`Invalid code: '${codingA.code}' not found in CodeSystem '${csSystem}'`);
291
- error.statusCode = 404;
292
- error.issueCode = 'not-found';
293
- throw error;
291
+ throw this.unknownCodeIssue(csProvider, codingA.code, names[0], locateA ? locateA.message : null, txp);
294
292
  }
295
293
 
296
294
  const locateB = await csProvider.locate(codingB.code);
297
295
  if (!locateB || !locateB.context) {
298
- const error = new Error(`Invalid code: '${codingB.code}' not found in CodeSystem '${csSystem}'`);
299
- error.statusCode = 404;
300
- error.issueCode = 'not-found';
301
- throw error;
296
+ throw this.unknownCodeIssue(csProvider, codingB.code, names[1], locateB ? locateB.message : null, txp);
302
297
  }
303
298
 
304
299
  let equal = false;
@@ -324,21 +319,43 @@ class SubsumesWorker extends TerminologyWorker {
324
319
  }
325
320
 
326
321
  /**
327
- * Build an OperationOutcome
328
- * @param {string} severity - error, warning, information
329
- * @param {string} code - Issue code
330
- * @param {string} message - Diagnostic message
322
+ * Build the Issue for a code that isn't in the code system. Uses the same message ids
323
+ * and tx-issue-type detail code as $validate-code, so clients get consistent errors
324
+ * @param {CodeSystemProvider} csProvider - CodeSystem provider
325
+ * @param {string} code - the code that wasn't found
326
+ * @param {string} path - the parameter the code came from ('codeA' / 'codeB')
327
+ * @param {string} message - any explanation the provider gave for not finding it
328
+ * @param {TxParameters} txp - parsed parameters (for languages)
329
+ * @returns {Issue}
330
+ */
331
+ unknownCodeIssue(csProvider, code, path, message, txp) {
332
+ const system = csProvider.system();
333
+ const version = csProvider.version();
334
+ const msgId = Unknown_Code_in_VersionSCT(system, version);
335
+ const langs = txp ? txp.HTTPLanguages : null;
336
+ const msg = this.i18n.translate(msgId, langs, [code, system, version, SCTVersion(system, version)]);
337
+ const issue = new Issue('error', 'code-invalid', path, msgId, msg, 'invalid-code', 404);
338
+ if (message) {
339
+ issue.withDiagnostics(message);
340
+ }
341
+ return issue;
342
+ }
343
+
344
+ /**
345
+ * Wrap an error we did not expect as an OperationOutcome. The message goes in details.text,
346
+ * not diagnostics: TxTester strips diagnostics from every issue it compares, so anything
347
+ * reported that way is invisible in a failing test - all you see is 'exception'
348
+ * @param {Error} error - the error that escaped
331
349
  * @returns {Object} OperationOutcome resource
332
350
  */
333
- operationOutcome(severity, code, message) {
334
- return {
335
- resourceType: 'OperationOutcome',
336
- issue: [{
337
- severity,
338
- code,
339
- diagnostics: message
340
- }]
341
- };
351
+ unexpectedErrorOutcome(error) {
352
+ // txIssueType, when the thrower set one, becomes the details.coding - that is where
353
+ // a client looks for the machine-readable reason (e.g. cannot-determine).
354
+ const issue = new Issue('error', error.issueCode || 'exception', null, null,
355
+ error.message || String(error), error.txIssueType || null, error.statusCode || 500);
356
+ const oo = new OperationOutcome();
357
+ oo.addIssue(issue);
358
+ return oo.jsonObj;
342
359
  }
343
360
  }
344
361