fhirsmith 0.11.2 → 0.13.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 (68) hide show
  1. package/CHANGELOG.md +377 -2
  2. package/README.md +1 -1
  3. package/config-template.json +31 -1
  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 +208 -30
  10. package/publisher/validation.js +210 -0
  11. package/server.js +24 -15
  12. package/stats-db.js +127 -0
  13. package/stats.js +172 -6
  14. package/translations/Messages.properties +5 -1
  15. package/tx/README.md +21 -0
  16. package/tx/cs/cs-api.js +30 -1
  17. package/tx/cs/cs-api.md +1 -1
  18. package/tx/cs/cs-cs.js +1 -1
  19. package/tx/cs/cs-icd11.js +805 -0
  20. package/tx/cs/cs-lang.js +331 -85
  21. package/tx/cs/cs-loinc.js +45 -4
  22. package/tx/cs/cs-mimetypes.js +492 -21
  23. package/tx/cs/cs-snomed.js +141 -58
  24. package/tx/cs/cs-ucum.js +33 -3
  25. package/tx/cs/icd11-expressions.js +243 -0
  26. package/tx/importers/export-icd11-codesystem.js +823 -0
  27. package/tx/importers/icd11-schema.md +285 -0
  28. package/tx/importers/import-icd11.js +861 -0
  29. package/tx/importers/import-loinc.module.js +14 -7
  30. package/tx/importers/import-sct.module.js +10 -1
  31. package/tx/library/codesystem.js +21 -27
  32. package/tx/library/conceptmap.js +39 -0
  33. package/tx/library/nesting-limit.js +79 -0
  34. package/tx/library/operation-outcome.js +97 -3
  35. package/tx/library/parameter-check.js +139 -0
  36. package/tx/library/valueset.js +16 -20
  37. package/tx/library.js +23 -1
  38. package/tx/ocl/cache/cache-utils.cjs +5 -3
  39. package/tx/ocl/cs-ocl.cjs +73 -17
  40. package/tx/ocl/jobs/background-queue.cjs +6 -3
  41. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  42. package/tx/ocl/shared/patches.cjs +48 -1
  43. package/tx/ocl/vs-ocl.cjs +15 -13
  44. package/tx/operation-context.js +220 -10
  45. package/tx/params.js +226 -175
  46. package/tx/sct/expressions.js +677 -15
  47. package/tx/tx.fhir.org.yml +3 -2
  48. package/tx/tx.js +72 -92
  49. package/tx/workers/batch-validate.js +11 -18
  50. package/tx/workers/batch.js +16 -28
  51. package/tx/workers/cache-control.js +87 -17
  52. package/tx/workers/closure.js +3 -8
  53. package/tx/workers/compare.js +6 -34
  54. package/tx/workers/expand.js +108 -60
  55. package/tx/workers/lookup.js +35 -40
  56. package/tx/workers/read.js +6 -40
  57. package/tx/workers/search.js +2 -8
  58. package/tx/workers/subsumes.js +58 -41
  59. package/tx/workers/translate.js +472 -47
  60. package/tx/workers/validate.js +83 -78
  61. package/tx/workers/worker.js +92 -11
  62. package/tx/xml/xml-base.js +264 -77
  63. package/tx/xversion/xv-conceptmap.js +81 -0
  64. package/tx/xversion/xv-parameters.js +8 -0
  65. package/tx/xversion/xv-valueset.js +39 -1
  66. package/utilities/merge-audit.py +178 -0
  67. package/utilities/why-merge-audit.md +39 -0
  68. package/tx/data/snomed-subset.txt +0 -84
@@ -12,8 +12,9 @@ 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
+ const {checkAddedParameters} = require("../library/parameter-check");
17
18
 
18
19
  class LookupWorker extends TerminologyWorker {
19
20
  /**
@@ -48,16 +49,7 @@ class LookupWorker extends TerminologyWorker {
48
49
  this.log.error(error);
49
50
  debugLog(error);
50
51
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
51
- const statusCode = error.statusCode || 500;
52
- const issueCode = error.issueCode || 'exception';
53
- return res.status(statusCode).json({
54
- resourceType: 'OperationOutcome',
55
- issue: [{
56
- severity: 'error',
57
- code: issueCode,
58
- diagnostics: error.message
59
- }]
60
- });
52
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
61
53
  }
62
54
  }
63
55
 
@@ -75,15 +67,7 @@ class LookupWorker extends TerminologyWorker {
75
67
  this.log.error(error);
76
68
  debugLog(error);
77
69
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
78
- const issueCode = error.issueCode || 'exception';
79
- return res.status(400).json({
80
- resourceType: 'OperationOutcome',
81
- issue: [{
82
- severity: 'error',
83
- code: issueCode,
84
- diagnostics: error.message
85
- }]
86
- });
70
+ return res.status(400).json(outcomeFromError(error));
87
71
  }
88
72
  }
89
73
 
@@ -139,8 +123,11 @@ class LookupWorker extends TerminologyWorker {
139
123
  }
140
124
 
141
125
  if (!csProvider) {
142
- const systemUrl = params.system || params.coding?.system;
143
- 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;
144
131
  const msg = versionStr
145
132
  ? `CodeSystem not found: ${systemUrl} version ${versionStr}`
146
133
  : `CodeSystem not found: ${systemUrl}`;
@@ -168,8 +155,7 @@ class LookupWorker extends TerminologyWorker {
168
155
  oo.addIssue(error);
169
156
  return res.status(error.statusCode || 500).json(oo.jsonObj);
170
157
  } else {
171
- return res.status(error.statusCode || 500).json(this.operationOutcome(
172
- 'error', error.issueCode || 'exception', error.message));
158
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
173
159
  }
174
160
  }
175
161
  }
@@ -232,8 +218,7 @@ class LookupWorker extends TerminologyWorker {
232
218
  oo.addIssue(error);
233
219
  return res.status(error.statusCode || 500).json(oo.jsonObj);
234
220
  } else {
235
- return res.status(error.statusCode || 500).json(this.operationOutcome(
236
- 'error', error.issueCode || 'exception', error.message));
221
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
237
222
  }
238
223
  }
239
224
  }
@@ -270,7 +255,9 @@ class LookupWorker extends TerminologyWorker {
270
255
  if (locateResult?.message) {
271
256
  message += ' ('+locateResult.message+')';
272
257
  }
273
- 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);
274
261
  }
275
262
 
276
263
  const ctxt = locateResult.context;
@@ -393,8 +380,20 @@ class LookupWorker extends TerminologyWorker {
393
380
  }
394
381
  }
395
382
 
396
- // Let the provider add additional properties
383
+ // Let the provider add additional properties. Some providers pass content
384
+ // through from a remote API, where a value can be any JSON at all, so what
385
+ // comes back is checked before it goes out: a structure where a FHIR
386
+ // primitive belongs is a provider bug, and shipping it would produce a
387
+ // response that isn't valid FHIR (and, in XML, isn't even parseable).
388
+ const providerParamsFrom = responseParams.length;
397
389
  await csProvider.extendLookup(ctxt, params.properties || [], responseParams);
390
+ let providerName;
391
+ try {
392
+ providerName = csProvider.system();
393
+ } catch (e) {
394
+ providerName = 'unknown';
395
+ }
396
+ checkAddedParameters(responseParams, providerParamsFrom, providerName);
398
397
 
399
398
  if (reportedSupplements) {
400
399
  for (const supplement of reportedSupplements) {
@@ -428,21 +427,17 @@ class LookupWorker extends TerminologyWorker {
428
427
  }
429
428
 
430
429
  /**
431
- * 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.
432
433
  * @param {string} severity - error, warning, information
433
- * @param {string} code - Issue code
434
- * @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
435
437
  * @returns {Object} OperationOutcome resource
436
438
  */
437
- operationOutcome(severity, code, message) {
438
- return {
439
- resourceType: 'OperationOutcome',
440
- issue: [{
441
- severity,
442
- code,
443
- details: {text : message}
444
- }]
445
- };
439
+ operationOutcome(severity, code, message, txIssueType = null) {
440
+ return buildOperationOutcome(severity, code, message, txIssueType);
446
441
  }
447
442
  }
448
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