fhirsmith 0.14.0 → 0.14.2

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 (73) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +3 -2
  3. package/config-template.json +20 -0
  4. package/library/fhir-openapi-schema.js +441 -0
  5. package/library/html-server.js +9 -3
  6. package/library/languages.js +15 -0
  7. package/library/npm-audit.js +265 -0
  8. package/library/openapi-doc.js +557 -0
  9. package/library/same-origin.js +62 -0
  10. package/library/utilities.js +21 -0
  11. package/nginx.md +17 -9
  12. package/package.json +10 -4
  13. package/packages/openapi.js +13 -0
  14. package/packages/openapi.yaml +603 -0
  15. package/packages/packages-template.html +4 -2
  16. package/packages/packages.js +286 -81
  17. package/packages/readme.md +120 -0
  18. package/publisher/publisher-template.html +1 -0
  19. package/publisher/publisher.js +95 -42
  20. package/registry/api.js +117 -41
  21. package/registry/crawler.js +24 -0
  22. package/registry/fhirsmith-releases.js +170 -0
  23. package/registry/model.js +10 -0
  24. package/registry/openapi.js +13 -0
  25. package/registry/openapi.yaml +311 -0
  26. package/registry/readme.md +107 -208
  27. package/registry/registry-template.html +4 -1
  28. package/registry/registry.js +183 -20
  29. package/server.js +25 -6
  30. package/testing/openapi-schemas.config.js +58 -0
  31. package/testing/openapi-schemas.json +1920 -0
  32. package/testing/openapi.js +15 -0
  33. package/testing/openapi.yaml +273 -0
  34. package/testing/readme.md +75 -1
  35. package/testing/render.js +270 -29
  36. package/testing/search.js +1 -1
  37. package/testing/store.js +152 -8
  38. package/testing/testing-template.html +2 -0
  39. package/testing/testing.js +307 -9
  40. package/translations/Messages.properties +1 -0
  41. package/tx/README.md +58 -0
  42. package/tx/cm/cm-database.js +10 -1
  43. package/tx/cm/cm-package.js +8 -1
  44. package/tx/cs/cs-rxnorm.js +23 -6
  45. package/tx/cs/cs-snomed.js +3 -72
  46. package/tx/html/tx-template.html +2 -0
  47. package/tx/library/canonical-resource.js +50 -1
  48. package/tx/library/codesystem.js +1 -0
  49. package/tx/library/conceptmap.js +1 -0
  50. package/tx/library/valueset.js +16 -0
  51. package/tx/library.js +17 -2
  52. package/tx/openapi-operations.js +363 -0
  53. package/tx/openapi-schemas.config.js +118 -0
  54. package/tx/openapi-schemas.json +7903 -0
  55. package/tx/openapi.js +289 -0
  56. package/tx/openapi.yaml +151 -0
  57. package/tx/operation-context.js +15 -0
  58. package/tx/params.js +3 -12
  59. package/tx/provider.js +10 -1
  60. package/tx/sct/editions.js +70 -0
  61. package/tx/tx-html.js +6 -0
  62. package/tx/tx.js +55 -0
  63. package/tx/vs/vs-database.js +14 -5
  64. package/tx/vs/vs-package.js +10 -0
  65. package/tx/workers/expand.js +37 -18
  66. package/tx/workers/metadata.js +0 -13
  67. package/tx/workers/translate.js +25 -1
  68. package/tx/workers/validate.js +74 -24
  69. package/tx/workers/worker.js +32 -43
  70. package/utilities/extract-schema-json.js +167 -0
  71. package/utilities/generate-openapi-schemas.js +49 -0
  72. package/utilities/submit-test-report.js +1 -1
  73. package/xig/xig.js +2 -4
package/tx/tx.js CHANGED
@@ -54,6 +54,7 @@ const {BundleXML} = require("./xml/bundle-xml");
54
54
  const ConceptUsageTracker = require("./usage-tracker");
55
55
  const ProblemFinder = require("./problems");
56
56
  const { buildOperationOutcome, outcomeFromError } = require('./library/operation-outcome');
57
+ const txOpenApi = require('./openapi');
57
58
  // const {writeFileSync} = require("fs");
58
59
 
59
60
  class TXModule {
@@ -521,6 +522,11 @@ class TXModule {
521
522
 
522
523
  app.use(express.urlencoded({ extended: true }));
523
524
 
525
+ // The OpenAPI description, on the endpoints it describes (R5)
526
+ if (txOpenApi.describes(fhirVersion)) {
527
+ this.setupOpenApi(router, endpointInfo);
528
+ }
529
+
524
530
  // Set up routes
525
531
  this.setupRoutes(router, endpointInfo.path);
526
532
 
@@ -540,6 +546,55 @@ class TXModule {
540
546
  this.log.info(`Endpoint ${endpointPath} registered`);
541
547
  }
542
548
 
549
+ /**
550
+ * The OpenAPI description of an endpoint: /openapi.json, /openapi.yaml, and /openapi (an
551
+ * HTML reference for browsers, the JSON otherwise), and an RFC 8631 Link header on every
552
+ * response that points to them. These are sent directly, not through the res.json the
553
+ * endpoint wraps for FHIR resources.
554
+ *
555
+ * @param {express.Router} router
556
+ * @param {Object} endpointInfo
557
+ */
558
+ setupOpenApi(router, endpointInfo) {
559
+ const doc = txOpenApi.forEndpoint(endpointInfo.path);
560
+ const sendJson = (res) => res.type('application/json').send(doc.getJson());
561
+ router.use((req, res, next) => {
562
+ res.setHeader('Link', `<${req.baseUrl}/openapi.json>; rel="service-desc", <${req.baseUrl}/openapi>; rel="service-doc"`);
563
+ next();
564
+ });
565
+ const count = (start) => this.countRequest(endpointInfo.path, 'openapi', Date.now() - start);
566
+ router.get('/openapi.json', (req, res) => {
567
+ const start = Date.now();
568
+ try {
569
+ sendJson(res);
570
+ } finally {
571
+ count(start);
572
+ }
573
+ });
574
+ router.get('/openapi.yaml', (req, res) => {
575
+ const start = Date.now();
576
+ try {
577
+ res.type('application/yaml').send(doc.getYaml());
578
+ } finally {
579
+ count(start);
580
+ }
581
+ });
582
+ router.get('/openapi', async (req, res) => {
583
+ const start = Date.now();
584
+ try {
585
+ if (!txHtml.acceptsHtml(req)) {
586
+ return sendJson(res);
587
+ }
588
+ const txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, endpointInfo.path);
589
+ const html = await txhtml.renderPage('API', doc.renderHtml(), endpointInfo, req.txStartTime);
590
+ res.setHeader('Content-Type', 'text/html');
591
+ res.send(html);
592
+ } finally {
593
+ count(start);
594
+ }
595
+ });
596
+ }
597
+
543
598
  /**
544
599
  * Set up routes for an endpoint
545
600
  * @param {express.Router} router - Express router
@@ -743,7 +743,17 @@ class ValueSetDatabase {
743
743
  const valueSetMap = new Map();
744
744
 
745
745
  for (const row of rows) {
746
- const valueSet = new ValueSet(JSON.parse(row.content));
746
+ let valueSet;
747
+ try {
748
+ valueSet = new ValueSet(JSON.parse(row.content));
749
+ } catch (e) {
750
+ // one unacceptable value set (e.g. one containing a CodeSystem, which the
751
+ // server doesn't support) must not take the whole package's value sets with it
752
+ require('../../library/logger').getInstance().child({ module: 'tx' })
753
+ .warn(`${source || 'ValueSet database'}: skipping ValueSet ${row.url || row.id}${row.version ? '|' + row.version : ''}: ${e.message}`);
754
+ this.vsCount--;
755
+ continue;
756
+ }
747
757
  valueSet.sourcePackage = source;
748
758
  // Attach the stored content hash so callers can detect changes
749
759
  // without recomputing over the full JSON.
@@ -839,10 +849,9 @@ class ValueSetDatabase {
839
849
  });
840
850
  } else {
841
851
  // Fall back to parsing JSON
842
- results = rows.map(row => {
843
- const vs = map.get(row.id);
844
- return vs;
845
- });
852
+ // a value set the map doesn't have was rejected when the map was loaded
853
+ // (see loadAllValueSets), so it isn't a result
854
+ results = rows.map(row => map.get(row.id)).filter(vs => vs);
846
855
  }
847
856
 
848
857
  resolve(results);
@@ -3,6 +3,7 @@ const fs = require('fs').promises;
3
3
  const { AbstractValueSetProvider } = require('./vs-api');
4
4
  const { PackageContentLoader } = require('../../library/package-manager');
5
5
  const { ValueSetDatabase } = require('./vs-database');
6
+ const { checkContained } = require('../library/canonical-resource');
6
7
  const { VersionUtilities } = require('../../library/version-utilities');
7
8
  const {validateParameter} = require("../../library/utilities");
8
9
 
@@ -126,6 +127,15 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
126
127
  for (const entry of valueSetEntries) {
127
128
  const valueSet = await this.packageLoader.loadFile(entry);
128
129
  if (valueSet.url) {
130
+ try {
131
+ // don't store a value set the server can't use (e.g. one that contains a
132
+ // CodeSystem): it would be listed by search, and fail when read
133
+ checkContained(valueSet);
134
+ } catch (e) {
135
+ require('../../library/logger').getInstance().child({ module: 'tx' })
136
+ .warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ValueSet ${valueSet.url}: ${e.message}`);
137
+ continue;
138
+ }
129
139
  valueSets.push(valueSet);
130
140
  }
131
141
  }
@@ -620,19 +620,23 @@ class ValueSetExpander {
620
620
  return count;
621
621
  }
622
622
 
623
+ /**
624
+ * Exclude the codes of an (expanded) value set. Excludes are processed before includes,
625
+ * so the codes are recorded as excluded - as excludeCode does - and the includes then
626
+ * skip them. (This used to remove them from what had been included so far, which at
627
+ * that point is nothing, so excluding a value set excluded nothing.)
628
+ */
623
629
  excludeValueSet(vs, expansion, imports, offset) {
624
- for (const c of vs.expansion.contains) {
625
- this.worker.deadCheck('excludeValueSet');
626
- const s = this.keyC(c);
627
- if (this.passesImports(imports, c.system, c.code, offset) && this.map.has(s)) {
628
- const idx = this.fullList.indexOf(this.map.get(s));
629
- if (idx >= 0) {
630
- this.fullList.splice(idx, 1);
630
+ const walk = (list) => {
631
+ for (const c of list || []) {
632
+ this.worker.deadCheck('excludeValueSet');
633
+ if (c.code && this.passesImports(imports, c.system, c.code, offset)) {
634
+ this.excluded.add((this.doingVersion && !this.params.versionsMatch ? c.system + '|' + c.version : c.system) + '#' + c.code);
631
635
  }
632
- this.map.delete(s);
633
- this.decTotal();
636
+ walk(c.contains);
634
637
  }
635
- }
638
+ };
639
+ walk(vs.expansion.contains);
636
640
  }
637
641
 
638
642
  async checkSource(cset, exp, filter, srcURL, ts, vsInfo , source) {
@@ -1012,8 +1016,10 @@ class ValueSetExpander {
1012
1016
  let vs = await this.worker.findValueSet(s, '', vsSrc);
1013
1017
  const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
1014
1018
  this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
1015
- if (!vs.isContained && ivs.valueSet.vurl) {
1016
- this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl);
1019
+ // ivs.valueSet is the expansion (plain JSON), so it has no vurl - as for the
1020
+ // includes, build it
1021
+ if (!vs.isContained && this.worker.makeVurl(ivs.valueSet)) {
1022
+ this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
1017
1023
  }
1018
1024
  valueSets.push(ivs);
1019
1025
  }
@@ -1314,13 +1320,22 @@ class ValueSetExpander {
1314
1320
  }
1315
1321
 
1316
1322
  async expand(source, filter, noCacheThisOne) {
1317
- this.noCacheThisOne = noCacheThisOne;
1318
- this.totalStatus = 'uninitialised';
1319
- this.total = 0;
1320
-
1321
1323
  Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
1322
1324
  Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
1325
+ // circular reference detection: this value set is in the chain being processed until
1326
+ // its expansion is done
1323
1327
  this.worker.seeValueSet(source, this.params);
1328
+ try {
1329
+ return await this.expandSeen(source, filter, noCacheThisOne);
1330
+ } finally {
1331
+ this.worker.unseeValueSet(source);
1332
+ }
1333
+ }
1334
+
1335
+ async expandSeen(source, filter, noCacheThisOne) {
1336
+ this.noCacheThisOne = noCacheThisOne;
1337
+ this.totalStatus = 'uninitialised';
1338
+ this.total = 0;
1324
1339
  this.valueSet = source;
1325
1340
 
1326
1341
  const result = structuredClone(source.jsonObj);
@@ -1348,6 +1363,10 @@ class ValueSetExpander {
1348
1363
  if (result.expansion) {
1349
1364
  return result; // just return the expansion
1350
1365
  }
1366
+ if (!source.jsonObj.compose) {
1367
+ throw new Issue('error', 'invalid', null, 'VALUESET_NO_COMPOSE',
1368
+ this.worker.i18n.translate('VALUESET_NO_COMPOSE', this.params.httpLanguages, [source.contextKey || source.vurlOrMsg]), 'vs-invalid', 422);
1369
+ }
1351
1370
 
1352
1371
  if (this.params.generateNarrative && !this.noDetails) {
1353
1372
  div_ = div();
@@ -1385,9 +1404,9 @@ class ValueSetExpander {
1385
1404
  }
1386
1405
 
1387
1406
  if (this.params.DisplayLanguages) {
1388
- this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.asString(true));
1407
+ this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.asParameter());
1389
1408
  } else if (this.params.HTTPLanguages) {
1390
- this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.asString(true));
1409
+ this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.asParameter());
1391
1410
  }
1392
1411
  if (this.params.designations) {
1393
1412
  for (const s of this.params.designations) {
@@ -437,7 +437,6 @@ class MetadataHandler {
437
437
  // Parameters the $expand handler reads - see handleTypeLevelExpand in
438
438
  // tx/workers/expand.js and TxParameters.seeParameter in tx/params.js.
439
439
  // Keep this list in step with those when adding a parameter.
440
- { name: '_incomplete' },
441
440
  { name: 'abstract' },
442
441
  { name: 'activeOnly' },
443
442
  { name: 'check-system-version' },
@@ -453,20 +452,12 @@ class MetadataHandler {
453
452
  { name: 'filter' },
454
453
  { name: 'force-system-version' },
455
454
  { name: 'force-valueset-version' },
456
- { name: 'inactive' },
457
- { name: 'includeAlternateCodes' },
458
455
  { name: 'includeDefinition' },
459
456
  { name: 'includeDesignations' },
460
- { name: 'incomplete-ok' },
461
457
  {
462
458
  name: 'limit',
463
459
  documentation: 'Upper bound on the size of the expansion; the operation fails as too-costly beyond it'
464
460
  },
465
- { name: 'limitedExpansion' },
466
- {
467
- name: 'mode',
468
- documentation: '=lenient-display-validation'
469
- },
470
461
  { name: 'no-cache' },
471
462
  { name: 'offset' },
472
463
  { name: 'profile' },
@@ -479,10 +470,6 @@ class MetadataHandler {
479
470
  { name: 'url' },
480
471
  { name: 'useSupplement' },
481
472
  { name: 'valueSet' },
482
- {
483
- name: 'valueSetMode',
484
- documentation: '= CHECK_MEMBERSHIP_ONLY | NO_MEMBERSHIP_CHECK'
485
- },
486
473
  { name: 'valueSetVersion' }
487
474
  ]
488
475
  };
@@ -1085,9 +1085,14 @@ class TranslateWorker extends TerminologyWorker {
1085
1085
  added = await this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result) || added;
1086
1086
  }
1087
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));
1088
1093
  result.push({
1089
1094
  name: 'result',
1090
- valueBoolean: added
1095
+ valueBoolean: translated
1091
1096
  });
1092
1097
  if (!added) {
1093
1098
  result.push({
@@ -1120,6 +1125,25 @@ class TranslateWorker extends TerminologyWorker {
1120
1125
  };
1121
1126
  }
1122
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
+
1123
1147
  // eslint-disable-next-line no-unused-vars
1124
1148
  isOkTarget(cm, vs) {
1125
1149
  // if cm.target != null then
@@ -285,7 +285,10 @@ class ValueSetChecker {
285
285
  }
286
286
 
287
287
  seeValueSet() {
288
- this.worker.opContext.seeContext(this.valueSet.vurl);
288
+ const key = this.valueSet.contextKey !== undefined ? this.valueSet.contextKey : this.valueSet.vurl;
289
+ if (key) {
290
+ this.worker.opContext.seeContext(key);
291
+ }
289
292
  if (this.valueSet.jsonObj.compose && this.valueSet.jsonObj.compose.extension) {
290
293
  for (let ext of this.valueSet.jsonObj.compose.extension) {
291
294
  if (ext.url === 'http://hl7.org/fhir/StructureDefinition/valueset-expansion-parameter' || ext.url === 'http://hl7.org/fhir/tools/StructureDefinition/valueset-expansion-parameter') {
@@ -305,8 +308,22 @@ class ValueSetChecker {
305
308
  async prepare() {
306
309
  if (this.valueSet === null) {
307
310
  throw new Issue('error', 'not-found', null, null, 'Error Error: vs = nil', null, 422);
308
- } else {
309
- this.seeValueSet();
311
+ }
312
+ // circular reference detection: this value set is in the chain being processed until
313
+ // it's prepared (which prepares everything it imports)
314
+ this.seeValueSet();
315
+ try {
316
+ await this.prepareSeen();
317
+ } finally {
318
+ const key = this.valueSet.contextKey !== undefined ? this.valueSet.contextKey : this.valueSet.vurl;
319
+ if (key) {
320
+ this.worker.opContext.unseeContext(key);
321
+ }
322
+ }
323
+ }
324
+
325
+ async prepareSeen() {
326
+ {
310
327
  this.worker.opContext.addNote(this.valueSet, 'Analysing ' + this.valueSet.vurl + ' for validation purposes', this.indentCount);
311
328
  if (this.indentCount === 0) {
312
329
  this.worker.opContext.addNote(this.valueSet, 'Parameters: ' + this.params.summary(), this.indentCount);
@@ -569,9 +586,14 @@ class ValueSetChecker {
569
586
  if (msg) {
570
587
  op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note'));
571
588
  }
572
- inactive.value = await cs.isInactive(ctxt.context);
573
- inactive.path = path;
574
- vstatus.value = await cs.getStatus(ctxt.context);
589
+ // in a CodeableConcept a later coding must not clear the inactive status an earlier
590
+ // coding found, or the warning silently disappears when the inactive coding comes first
591
+ if (!inactive.value) {
592
+ inactive.value = await cs.isInactive(ctxt.context);
593
+ inactive.path = path;
594
+ inactive.code = code;
595
+ vstatus.value = await cs.getStatus(ctxt.context);
596
+ }
575
597
  }
576
598
  if (displays !== null) {
577
599
  await this.worker.listDisplaysFromCodeSystem(displays, cs, ctxt.context);
@@ -925,6 +947,7 @@ class ValueSetChecker {
925
947
  let pdisp;
926
948
  let psys;
927
949
  let pcode;
950
+ let ppath; // the path of the coding that is returned
928
951
  let pver;
929
952
 
930
953
 
@@ -1063,6 +1086,7 @@ class ValueSetChecker {
1063
1086
  if (pcode === undefined) {
1064
1087
  psys = c.system;
1065
1088
  pcode = c.code;
1089
+ ppath = path;
1066
1090
  if (ver.value) {
1067
1091
  pver = ver.value;
1068
1092
  }
@@ -1088,7 +1112,11 @@ class ValueSetChecker {
1088
1112
  mt.push(m);
1089
1113
  op.addIssue(new Issue('error', 'invalid', p, 'Terminology_TX_System_Relative', m, 'invalid-data'));
1090
1114
  }
1091
- let prov = await this.worker.findCodeSystem(ws, c.version, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
1115
+ // the code isn't in the value set, but it's still looked up in the code system to report on
1116
+ // it - and that has to be the version the value set selects when the coding doesn't say,
1117
+ // not the server default (tx-ecosystem sct validate-code-pc-none)
1118
+ let lookupVersion = c.version || vsImpliedVersion;
1119
+ let prov = await this.worker.findCodeSystem(ws, lookupVersion, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
1092
1120
  if (prov === null) {
1093
1121
  let vss = await this.worker.findValueSet(ws, '', null);
1094
1122
  if (vss !== null) {
@@ -1098,7 +1126,7 @@ class ValueSetChecker {
1098
1126
  op.addIssue(new Issue('error', 'invalid', addToPath(path, 'system'), 'Terminology_TX_System_ValueSet2', m, 'invalid-data'));
1099
1127
  cause.value = 'invalid';
1100
1128
  } else {
1101
- let provS = await this.worker.findCodeSystem(ws, c.version, this.params, ['supplement'], op,true, true, false, this.worker.requiredSupplements);
1129
+ let provS = await this.worker.findCodeSystem(ws, lookupVersion, this.params, ['supplement'], op,true, true, false, this.worker.requiredSupplements);
1102
1130
  if (provS !== null) {
1103
1131
  vss = null;
1104
1132
  let m = this.worker.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', this.params.HTTPLanguages, [provS.vurl()]);
@@ -1109,7 +1137,7 @@ class ValueSetChecker {
1109
1137
  let prov2 = await this.worker.findCodeSystem(ws, '', this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
1110
1138
  let bAdd = true;
1111
1139
  let m, mid, vn;
1112
- if (prov2 === null && !c.version) {
1140
+ if (prov2 === null && !lookupVersion) {
1113
1141
  mid = 'UNKNOWN_CODESYSTEM';
1114
1142
  m = this.worker.i18n.translate('UNKNOWN_CODESYSTEM', this.params.HTTPLanguages, [ws]);
1115
1143
  bAdd = !unknownSystems.has(ws);
@@ -1123,9 +1151,9 @@ class ValueSetChecker {
1123
1151
  vn = ws;
1124
1152
  } else {
1125
1153
  mid = 'UNKNOWN_CODESYSTEM_VERSION';
1126
- vn = ws + '|' + c.version;
1154
+ vn = ws + '|' + lookupVersion;
1127
1155
  }
1128
- m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws, c.version, this.worker.presentVersionList(vl)]);
1156
+ m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws, lookupVersion, this.worker.presentVersionList(vl)]);
1129
1157
  bAdd = !unknownSystems.has(vn);
1130
1158
  if (bAdd) {
1131
1159
  unknownSystems.add(vn);
@@ -1181,6 +1209,7 @@ class ValueSetChecker {
1181
1209
  if (!this.params.membershipOnly && !inactive.value && await prov.isInactive(ctxt.context)) {
1182
1210
  inactive.value = true;
1183
1211
  inactive.path = path;
1212
+ inactive.code = c.code;
1184
1213
  const st = await prov.getStatus(ctxt.context);
1185
1214
  if (st) {
1186
1215
  vstatus.value = st;
@@ -1336,9 +1365,14 @@ class ValueSetChecker {
1336
1365
  }
1337
1366
 
1338
1367
  if (inactive.value) {
1339
- result.addParamBool('inactive', inactive.value);
1340
- if (vstatus.value && vstatus.value !== 'inactive') {
1341
- result.addParamCode('status', vstatus.value);
1368
+ // the inactive and status parameters describe the code being returned. In a CodeableConcept
1369
+ // the inactive coding may be a different one (or none was returned at all): it still gets
1370
+ // its warning below, but the parameters are not claimed for the returned code
1371
+ if (mode !== 'codeableConcept' || (pcode && inactive.path === ppath)) {
1372
+ result.addParamBool('inactive', inactive.value);
1373
+ if (vstatus.value && vstatus.value !== 'inactive') {
1374
+ result.addParamCode('status', vstatus.value);
1375
+ }
1342
1376
  }
1343
1377
  let mpath = inactive.path;
1344
1378
  if (!mpath) {
@@ -1350,7 +1384,9 @@ class ValueSetChecker {
1350
1384
  mid1 = 'INACTIVE_CONCEPT_FOUND_ADD';
1351
1385
  mm1 = 'inactive';
1352
1386
  }
1353
- let m = this.worker.i18n.translate(mid1, this.params.HTTPLanguages, [vstatus.value, tcode, mm1]);
1387
+ // report the code that is actually inactive - in a CodeableConcept that isn't necessarily
1388
+ // the first coding (tcode), which is what this message used to name
1389
+ let m = this.worker.i18n.translate(mid1, this.params.HTTPLanguages, [vstatus.value, inactive.code || tcode, mm1]);
1354
1390
  msg(m);
1355
1391
  op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
1356
1392
  } else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
@@ -1544,6 +1580,7 @@ class ValueSetChecker {
1544
1580
  if (!this.params.membershipOnly && role !== 'not in') {
1545
1581
  inactive.value = true;
1546
1582
  inactive.path = path;
1583
+ inactive.code = code;
1547
1584
  if (inactive.value) {
1548
1585
  vstatus.value = await cs.getStatus(loc.context);
1549
1586
  }
@@ -1554,6 +1591,7 @@ class ValueSetChecker {
1554
1591
  if (role !== 'not in') {
1555
1592
  inactive.value = true;
1556
1593
  inactive.path = path;
1594
+ inactive.code = code;
1557
1595
  vstatus.value = await cs.getStatus(loc.context);
1558
1596
  let msg = this.worker.i18n.translate('STATUS_CODE_WARNING_CODE', this.params.HTTPLanguages, ['not active', code]);
1559
1597
  messages.push(msg);
@@ -1562,9 +1600,12 @@ class ValueSetChecker {
1562
1600
  } else {
1563
1601
  result = true;
1564
1602
  if (role !== 'not in') {
1565
- inactive.value = await cs.isInactive(loc.context);
1566
- inactive.path = path;
1567
- vstatus.value = await cs.getStatus(loc.context);
1603
+ if (!inactive.value) {
1604
+ inactive.value = await cs.isInactive(loc.context);
1605
+ inactive.path = path;
1606
+ inactive.code = code;
1607
+ vstatus.value = await cs.getStatus(loc.context);
1608
+ }
1568
1609
  if (vcc !== null) {
1569
1610
  if (!vcc.coding) {
1570
1611
  vcc.coding = [];
@@ -1601,6 +1642,7 @@ class ValueSetChecker {
1601
1642
  if (!this.params.membershipOnly) {
1602
1643
  inactive.value = true;
1603
1644
  inactive.path = path;
1645
+ inactive.code = code;
1604
1646
  vstatus.value = await cs.getStatus(loc);
1605
1647
  }
1606
1648
  } else {
@@ -1614,9 +1656,12 @@ class ValueSetChecker {
1614
1656
  } else if (Extensions.has(cc,'http://hl7.org/fhir/StructureDefinition/valueset-deprecated')) {
1615
1657
  op.addIssue(new Issue('warning', 'business-rule', addToPath(path, 'code'), 'CONCEPT_DEPRECATED_IN_VALUESET', this.worker.i18n.translate('CONCEPT_DEPRECATED_IN_VALUESET', this.params.HTTPLanguages, [cs.system(), code, 'deprecated', vs.vurl]), 'code-comment'));
1616
1658
  }
1617
- inactive.value = await cs.isInactive(loc);
1618
- inactive.path = path;
1619
- vstatus.value = await cs.getStatus(loc);
1659
+ if (!inactive.value) {
1660
+ inactive.value = await cs.isInactive(loc);
1661
+ inactive.path = path;
1662
+ inactive.code = code;
1663
+ vstatus.value = await cs.getStatus(loc);
1664
+ }
1620
1665
  result = true;
1621
1666
  return result;
1622
1667
  }
@@ -1665,6 +1710,7 @@ class ValueSetChecker {
1665
1710
  if (!this.params.membershipOnly) {
1666
1711
  inactive.value = true;
1667
1712
  inactive.path = path;
1713
+ inactive.code = code;
1668
1714
  vstatus.value = await cs.getStatus(loc);
1669
1715
  }
1670
1716
  } else {
@@ -1677,6 +1723,7 @@ class ValueSetChecker {
1677
1723
  if (await cs.isInactive(loc)) {
1678
1724
  inactive.value = true;
1679
1725
  inactive.path = path;
1726
+ inactive.code = code;
1680
1727
  // only replace a status we already have with a real one - a filter context
1681
1728
  // doesn't always carry the status (SNOMED, LOINC), and the assembly reads
1682
1729
  // vstatus for the message wording as well as for the status parameter
@@ -1714,9 +1761,12 @@ class ValueSetChecker {
1714
1761
  }
1715
1762
  } else {
1716
1763
  result = true;
1717
- inactive.value = await cs.isInactive(loc.context);
1718
- inactive.path = path;
1719
- vstatus.value = await cs.getStatus(loc.context);
1764
+ if (!inactive.value) {
1765
+ inactive.value = await cs.isInactive(loc.context);
1766
+ inactive.path = path;
1767
+ inactive.code = code;
1768
+ vstatus.value = await cs.getStatus(loc.context);
1769
+ }
1720
1770
  await this.worker.listDisplaysFromCodeSystem(displays, cs, loc.context);
1721
1771
  return result;
1722
1772
  }
@@ -7,6 +7,7 @@ const {Issue} = require("../library/operation-outcome");
7
7
  const {Languages} = require("../../library/languages");
8
8
  const {ConceptMap} = require("../library/conceptmap");
9
9
  const {Renderer} = require("../library/renderer");
10
+ const {editionName} = require("../sct/editions");
10
11
 
11
12
  // The cache-id travels as an HTTP header (not an operation parameter) so proxies /
12
13
  // load-balancers can act on it and the server can reject it before parsing the body.
@@ -378,14 +379,22 @@ class TerminologyWorker {
378
379
  return null;
379
380
  }
380
381
  if (url.startsWith("#")) {
382
+ // A local reference: per FHIR, #id is resolved against the contained resources of
383
+ // the resource being processed - which, when that is itself a contained value set,
384
+ // means its container's (contained resources are all siblings in one list)
381
385
  if (source) {
382
- if (source.jsonObj) {
383
- source = source.jsonObj;
384
- }
385
- for (const contained of source.contained || []) {
386
- if (contained.id === url.substring(1)) {
386
+ const container = source.container || source;
387
+ const json = container.jsonObj || container;
388
+ for (const contained of json.contained || []) {
389
+ if (contained && contained.id === url.substring(1)) {
390
+ if (contained.resourceType !== 'ValueSet') {
391
+ // the resource wrappers already reject this; this is the second line of defence
392
+ throw new Issue('error', 'not-supported', null, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
393
+ `The reference '${url}' is to a contained ${contained.resourceType}, not a ValueSet`, 'not-supported', 400);
394
+ }
387
395
  const ret = this.wrapRawResource(contained);
388
396
  ret.isContained = true;
397
+ ret.container = container.jsonObj ? container : this.wrapRawResource(container);
389
398
  return ret;
390
399
  }
391
400
  }
@@ -476,11 +485,24 @@ class TerminologyWorker {
476
485
  * Note: this is the caller's parameters, not this.params - the worker's own
477
486
  * params is null when expanding an imported ValueSet
478
487
  */
488
+ /** The circular reference key seeValueSet tracks for a value set */
489
+ valueSetContextKey(vs) {
490
+ return vs.contextKey !== undefined ? vs.contextKey : (vs.url ? (vs.url + (vs.version ? '|' + vs.version : '')) : null);
491
+ }
492
+
493
+ /** Finished processing a value set: see OperationContext.unseeContext */
494
+ unseeValueSet(vs) {
495
+ const key = this.valueSetContextKey(vs);
496
+ if (key) {
497
+ this.opContext.unseeContext(key);
498
+ }
499
+ }
500
+
479
501
  seeValueSet(vs, params) {
480
502
  // Build canonical URL from url and version
481
- const vurl = vs.url ? (vs.url + (vs.version ? '|' + vs.version : '')) : null;
482
- if (vurl) {
483
- this.opContext.seeContext(vurl);
503
+ const key = this.valueSetContextKey(vs);
504
+ if (key) {
505
+ this.opContext.seeContext(key);
484
506
  }
485
507
  // Check for expansion parameter extensions on compose
486
508
  if (vs.jsonObj.compose && vs.jsonObj.compose.extension) {
@@ -1100,42 +1122,9 @@ function Unknown_Code_in_VersionSCT(url, version) {
1100
1122
  function SCTVersion(url, ver) {
1101
1123
  if (url !== 'http://snomed.info/sct' || !ver) {
1102
1124
  return '';
1103
- } else {
1104
- let result = 'unknown';
1105
- let s = ver.split('/');
1106
- if (s.length >= 5) {
1107
- if (s[4] === '900000000000207008') result = 'International Edition';
1108
- else if (s[4] === '449081005') result = 'International Spanish Edition';
1109
- else if (s[4] === '11000221109') result = 'Argentinian Edition';
1110
- else if (s[4] === '32506021000036107') result = 'Australian Edition (with drug extension)';
1111
- else if (s[4] === '11000234105') result = 'Austrian Edition';
1112
- else if (s[4] === '11000172109') result = 'Belgian Edition';
1113
- else if (s[4] === '20621000087109') result = 'Canadian English Edition';
1114
- else if (s[4] === '20611000087101') result = 'Canadian Canadian French Edition';
1115
- else if (s[4] === '11000279109') result = 'Czech Edition';
1116
- else if (s[4] === '554471000005108') result = 'Danish Edition';
1117
- else if (s[4] === '11000181102') result = 'Estonian Edition';
1118
- else if (s[4] === '11000229106') result = 'Finnish Edition';
1119
- else if (s[4] === '11000274103') result = 'German Edition';
1120
- else if (s[4] === '1121000189102') result = 'Indian Edition';
1121
- else if (s[4] === '827022005') result = 'IPS Terminology';
1122
- else if (s[4] === '11000220105') result = 'Irish Edition';
1123
- else if (s[4] === '11000146104') result = 'Netherlands Edition';
1124
- else if (s[4] === '21000210109') result = 'New Zealand Edition';
1125
- else if (s[4] === '51000202101') result = 'Norwegian Edition';
1126
- else if (s[4] === '11000267109') result = 'Republic of Korea Edition (South Korea)';
1127
- else if (s[4] === '900000001000122104') result = 'Spanish National Edition';
1128
- else if (s[4] === '45991000052106') result = 'Swedish Edition';
1129
- else if (s[4] === '2011000195101') result = 'Swiss Edition';
1130
- else if (s[4] === '83821000000107') result = 'UK Edition';
1131
- else if (s[4] === '999000021000000109') result = 'UK Clinical Edition';
1132
- else if (s[4] === '5631000179106') result = 'Uruguay Edition';
1133
- else if (s[4] === '21000325107') result = 'Chilean Edition';
1134
- else if (s[4] === '731000124108') result = 'US Edition';
1135
- else if (s[4] === '5991000124107') result = 'US Edition (with ICD-10-CM maps)';
1136
- }
1137
- return result;
1138
1125
  }
1126
+ const s = ver.split('/');
1127
+ return (s.length >= 5 && editionName(s[4])) || 'unknown';
1139
1128
  }
1140
1129
 
1141
1130
  module.exports = {