fhirsmith 0.5.6 → 0.6.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.
package/tx/tx-html.js CHANGED
@@ -9,6 +9,15 @@ const htmlServer = require('../library/html-server');
9
9
  const Logger = require('../library/logger');
10
10
  const packageJson = require("../package.json");
11
11
  const escape = require('escape-html');
12
+ const {ExpandWorker} = require("./workers/expand");
13
+ const ValueSet = require("./library/valueset");
14
+ const {CodeSystemXML} = require("./xml/codesystem-xml");
15
+ const {ValueSetXML} = require("./xml/valueset-xml");
16
+ const {BundleXML} = require("./xml/bundle-xml");
17
+ const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
18
+ const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
19
+ const {ParametersXML} = require("./xml/parameters-xml");
20
+ const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
12
21
 
13
22
  const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
14
23
 
@@ -57,10 +66,16 @@ function loadTemplate() {
57
66
  class TxHtmlRenderer {
58
67
  renderer;
59
68
  liquid;
69
+ languages;
70
+ i18n;
71
+ path;
60
72
 
61
- constructor(renderer, liquid) {
73
+ constructor(renderer, liquid, languages, i18n, path) {
62
74
  this.renderer = renderer;
63
75
  this.liquid = liquid;
76
+ this.languages = languages;
77
+ this.i18n = i18n;
78
+ this.path = path;
64
79
  }
65
80
 
66
81
  /**
@@ -85,7 +100,7 @@ class TxHtmlRenderer {
85
100
  if (_fmt && typeof _fmt !== 'string') {
86
101
  _fmt = null;
87
102
  }
88
- if (_fmt && _fmt == 'html') {
103
+ if (_fmt && (_fmt == 'html' || _fmt.startsWith('html/'))) {
89
104
  return true;
90
105
  }
91
106
  if (!_fmt) {
@@ -106,6 +121,14 @@ class TxHtmlRenderer {
106
121
  } else {
107
122
  const resourceType = json.resourceType || 'Response';
108
123
 
124
+ let pfx = resourceType;
125
+ if (req.path.includes('$')) {
126
+ let s = req.path.substring(req.path.indexOf('$') + 1).replace(/[^a-zA-Z].*$/, '');
127
+ switch (s) {
128
+ case 'expand': pfx = "Expansion for "+resourceType;
129
+ }
130
+ }
131
+
109
132
  if (resourceType === 'Bundle' && json.type === 'searchset') {
110
133
  // Extract the resource type being searched from self link or entries
111
134
  const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
@@ -124,11 +147,11 @@ class TxHtmlRenderer {
124
147
  }
125
148
 
126
149
  if (json.id) {
127
- return `${resourceType}/${json.id}`;
150
+ return `${pfx} ${json.id}`;
128
151
  }
129
152
 
130
153
  if (json.name) {
131
- return `${resourceType}: ${json.name}`;
154
+ return `${pfx} ${json.name}`;
132
155
  }
133
156
 
134
157
  return resourceType;
@@ -267,15 +290,27 @@ class TxHtmlRenderer {
267
290
  return await this.buildHomePage(req);
268
291
  } else {
269
292
  try {
293
+ const _fmt = req?.query?._format || req?.query?.format || req?.body?._format;
294
+ const op = req ? req.path.includes("$") : false;
270
295
  const resourceType = json.resourceType;
271
296
 
272
297
  switch (resourceType) {
273
298
  case 'Parameters':
274
299
  return await this.renderParameters(json);
275
300
  case 'CodeSystem':
276
- return await this.renderCodeSystem(json, inBundle);
277
- case 'ValueSet':
278
- return await this.renderValueSet(json, inBundle);
301
+ return await this.renderCodeSystem(json, inBundle, _fmt, op);
302
+ case 'ValueSet': {
303
+ let exp = undefined;
304
+ if (!inBundle && !op && (!_fmt || _fmt == 'html')) {
305
+ try {
306
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
307
+ exp = new ValueSet(await worker.handleInternalExpand(json, req));
308
+ } catch (error) {
309
+ exp = error;
310
+ }
311
+ }
312
+ return await this.renderValueSet(json, inBundle, _fmt, op, exp);
313
+ }
279
314
  case 'ConceptMap':
280
315
  return await this.renderConceptMap(json, inBundle);
281
316
  case 'CapabilityStatement':
@@ -575,35 +610,88 @@ class TxHtmlRenderer {
575
610
  /**
576
611
  * Render CodeSystem resource
577
612
  */
578
- async renderCodeSystem(json, inBundle) {
579
- let html = await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
613
+ async renderCodeSystem(json, inBundle, _fmt) {
614
+ if (inBundle) {
615
+ return await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
616
+ } else {
617
+ let html = `<ul class="nav nav-tabs">`;
618
+ html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
619
+ html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
620
+ html += this.tab(_fmt && _fmt == 'html/xml', 'XML', json.resourceType, 'html/xml', json.id);
621
+ html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
622
+ html += this.tab(_fmt && _fmt == 'html/ops', 'LookUp / Subsumes', json.resourceType, 'html/ops', json.id);
623
+ html += `</ul>`;
624
+
625
+ if (!_fmt || _fmt == 'html') {
626
+ html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
627
+ } else if (_fmt == "html/json") {
628
+ html += await this.renderResourceJson(json);
629
+ } else if (_fmt == "html/xml") {
630
+ html += await this.renderResourceXml(json);
631
+ } else if (_fmt == "html/narrative") {
632
+ html += await this.renderResourceWithNarrative(json, json.text?.div);
633
+ } else if (_fmt == "html/ops") {
634
+ html += await this.liquid.renderFile('codesystem-operations', {
635
+ opsId: this.generateResourceId(),
636
+ vcSystemId: this.generateResourceId(),
637
+ inferSystemId: this.generateResourceId(),
638
+ url: escape(json.url || '')
639
+ });
640
+ }
580
641
 
581
- if (!inBundle) {
582
- html += await this.liquid.renderFile('codesystem-operations', {
583
- opsId: this.generateResourceId(),
584
- url: escape(json.url || '')
585
- });
586
- }
587
642
 
588
- return html;
643
+ return html;
644
+ }
589
645
  }
590
646
 
647
+ tab(b, name, rtype, type, id) {
648
+ if (b) {
649
+ return `<li class="active"><a href="#">${name}</a></li>`;
650
+ } else {
651
+ return `<li><a href="${this.path}/${rtype}/${id}?_format=${type}">${name}</a></li>`;
652
+ }
653
+ }
591
654
  /**
592
655
  * Render ValueSet resource
593
656
  */
594
- async renderValueSet(json, inBundle) {
595
- let html = await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
596
-
597
- if (!inBundle) {
598
- html += await this.liquid.renderFile('valueset-operations', {
599
- opsId: this.generateResourceId(),
600
- vcSystemId: this.generateResourceId(),
601
- inferSystemId: this.generateResourceId(),
602
- url: escape(json.url || '')
603
- });
657
+ async renderValueSet(json, inBundle, _fmt, op, exp) {
658
+ if (inBundle || op) {
659
+ return await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
660
+ } else {
661
+ let html = `<ul class="nav nav-tabs">`;
662
+ html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
663
+ html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
664
+ html += this.tab(_fmt && _fmt == 'html/xml', 'XML', json.resourceType, 'html/xml', json.id);
665
+ html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
666
+ html += this.tab(_fmt && _fmt == 'html/ops', 'Expand / Validate', json.resourceType, 'html/ops', json.id);
667
+ html += `</ul>`;
668
+
669
+ if (!_fmt || _fmt == 'html') {
670
+ html += await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
671
+ if (exp) {
672
+ html += "<h2>Expansion</h2>";
673
+ if (exp instanceof ValueSet) {
674
+ html += await this.renderer.renderVSExpansion(exp.jsonObj, false)
675
+ } else {
676
+ html += `<p>Error: `+exp.message+`</p>`;
677
+ }
678
+ }
679
+ } else if (_fmt == "html/json") {
680
+ html += await this.renderResourceJson(json);
681
+ } else if (_fmt == "html/xml") {
682
+ html += await this.renderResourceXml(json);
683
+ } else if (_fmt == "html/narrative") {
684
+ html += await this.renderResourceWithNarrative(json, json.text?.div);
685
+ } else if (_fmt == "html/ops") {
686
+ html += await this.liquid.renderFile('valueset-operations', {
687
+ opsId: this.generateResourceId(),
688
+ vcSystemId: this.generateResourceId(),
689
+ inferSystemId: this.generateResourceId(),
690
+ url: escape(json.url || '')
691
+ });
692
+ }
693
+ return html;
604
694
  }
605
-
606
- return html;
607
695
  }
608
696
 
609
697
  /**
@@ -1101,9 +1189,7 @@ class TxHtmlRenderer {
1101
1189
  * Render resource with text/div narrative and collapsible JSON source
1102
1190
  */
1103
1191
  async renderResourceWithNarrative(json, rendered) {
1104
- const resourceId = this.generateResourceId();
1105
-
1106
- let html = "";
1192
+ let html = '';
1107
1193
 
1108
1194
  // Show text/div narrative if present
1109
1195
  if (rendered) {
@@ -1113,30 +1199,37 @@ class TxHtmlRenderer {
1113
1199
  } else {
1114
1200
  html += '<div class="narrative">(No Narrative)</div>';
1115
1201
  }
1116
- if (json.text && json.text.div) {
1117
- // Collapsible JSON source
1118
- html += '<div class="xhtml">';
1119
- html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
1120
- html += 'Show Original Narrative</button>';
1121
- html += `<div id="${resourceId}x" class="original-narrative" style="display: none; margin-top: 10px;">`;
1122
-
1123
- html += '<div class="narrative">';
1124
- html += json.text.div; // Already HTML, render as-is
1125
- html += '</div>';
1126
- }
1127
- html += '</div>';
1128
- html += '</div>';
1129
1202
 
1203
+ return html;
1204
+ }
1130
1205
 
1131
- // Collapsible JSON source
1132
- html += '<div class="json-source">';
1133
- html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleJsonSource('${resourceId}')">`;
1134
- html += 'Show JSON Source</button>';
1135
- html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
1206
+ async renderResourceJson(json) {
1207
+ let html = "";
1208
+ html += `<div class="json-content" style="margin-top: 10px;">`;
1136
1209
  html += `<pre>${escape(JSON.stringify(json, null, 2))}</pre>`;
1137
1210
  html += '</div>';
1138
- html += '</div>';
1211
+ return html;
1212
+ }
1213
+
1214
+ convertResourceToXml(res) {
1215
+ switch (res.resourceType) {
1216
+ case "CodeSystem" : return CodeSystemXML.toXml(res);
1217
+ case "ValueSet" : return ValueSetXML.toXml(res);
1218
+ case "Bundle" : return BundleXML.toXml(res, this.fhirVersion);
1219
+ case "CapabilityStatement" : return CapabilityStatementXML.toXml(res, "R5");
1220
+ case "TerminologyCapabilities" : return TerminologyCapabilitiesXML.toXml(res, "R5");
1221
+ case "Parameters": return ParametersXML.toXml(res, this.fhirVersion);
1222
+ case "OperationOutcome": return OperationOutcomeXML.toXml(res, this.fhirVersion);
1223
+ }
1224
+ throw new Error(`Resource type ${res.resourceType} not supported in XML`);
1225
+ }
1139
1226
 
1227
+ async renderResourceXml(json) {
1228
+ let xml = this.convertResourceToXml(json);
1229
+ let html = "";
1230
+ html += `<div class="xml-content" style="margin-top: 10px;">`;
1231
+ html += `<pre>${escape(xml)}</pre>`;
1232
+ html += '</div>';
1140
1233
  return html;
1141
1234
  }
1142
1235
 
package/tx/tx.js CHANGED
@@ -300,7 +300,7 @@ class TXModule {
300
300
  // Wrap res.json to intercept and convert to HTML if browser requests it, and log the request
301
301
  const originalJson = res.json.bind(res);
302
302
 
303
- let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid);
303
+ let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid, this.languages, this.i18n, endpointInfo.path);
304
304
  res.json = async (data) => {
305
305
  try {
306
306
  const duration = Date.now() - req.txStartTime;
@@ -897,7 +897,7 @@ class TXModule {
897
897
  router.get('/problems.html', async (req, res) => {
898
898
  const start = Date.now();
899
899
  try {
900
- let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid);
900
+ let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, req.txEndpoint.path);
901
901
  const problemFinder = new ProblemFinder();
902
902
  const content = await problemFinder.scanValueSets(req.txProvider);
903
903
  const html = await txhtml.renderPage('Problems', '<h3>ValueSet dependencies on unknown CodeSystem/Versions</h3>'+content, req.txEndpoint, req.txStartTime);
@@ -1388,17 +1388,28 @@ class ValueSetExpander {
1388
1388
  }
1389
1389
 
1390
1390
  checkCanonicalStatus(exp, vurl, status, standardsStatus, experimental, source) {
1391
+ let sourceStatus = source ? source.status : undefined;
1392
+ let sourceStandardsStatus= source ? Extensions.readString(source, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status') : undefined;
1391
1393
  if (standardsStatus == 'deprecated') {
1392
- this.addParamUri(exp, 'warning-deprecated', vurl);
1394
+ if (sourceStandardsStatus != 'deprecated') {
1395
+ this.addParamUri(exp, 'warning-deprecated', vurl);
1396
+ }
1393
1397
  } else if (standardsStatus == 'withdrawn') {
1394
- this.addParamUri(exp, 'warning-withdrawn', vurl);
1398
+ if (sourceStandardsStatus != 'withdrawn') {
1399
+ this.addParamUri(exp, 'warning-withdrawn', vurl);
1400
+ }
1395
1401
  } else if (status == 'retired') {
1396
- this.addParamUri(exp, 'warning-retired', vurl);
1397
- } else if (experimental && !source.experimental) {
1398
- this.addParamUri(exp, 'warning-experimental', vurl)
1399
- } else if (((status == 'draft') || (standardsStatus == 'draft')) &&
1400
- !((source.status == 'draft') || (Extensions.readString(source, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status') == 'draft'))) {
1401
- this.addParamUri(exp, 'warning-draft', vurl)
1402
+ if (sourceStatus != 'retired') {
1403
+ this.addParamUri(exp, 'warning-retired', vurl);
1404
+ }
1405
+ } else if (experimental) {
1406
+ if (!source.experimental) {
1407
+ this.addParamUri(exp, 'warning-experimental', vurl);
1408
+ }
1409
+ } else if (((status == 'draft') || (standardsStatus == 'draft'))) {
1410
+ if (!((source.status == 'draft') || (Extensions.readString(source, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status') == 'draft'))) {
1411
+ this.addParamUri(exp, 'warning-draft', vurl)
1412
+ }
1402
1413
  }
1403
1414
  }
1404
1415
 
@@ -1797,7 +1808,48 @@ class ExpandWorker extends TerminologyWorker {
1797
1808
  req.logInfo = this.usedSources.join("|")+txp.logInfo();
1798
1809
  return res.json(result);
1799
1810
  }
1800
-
1811
+
1812
+ /**
1813
+ * Handle type-level expand: /ValueSet/$expand
1814
+ * ValueSet identified by url, or provided directly in body
1815
+ */
1816
+ async handleInternalExpand(valueSet, req) {
1817
+ this.deadCheck('expand-internal');
1818
+
1819
+ if (!valueSet.jsonObj) {
1820
+ valueSet = new ValueSet(valueSet);
1821
+ }
1822
+ // Determine how the request is structured
1823
+ let params = null;
1824
+ this.seeSourceVS(valueSet);
1825
+
1826
+ if (req.method === 'POST' && req.body) {
1827
+ if (req.body.resourceType === 'ValueSet') {
1828
+ params = this.queryToParameters(req.query);
1829
+ } else if (req.body.resourceType === 'Parameters') {
1830
+ // Body is a Parameters resource
1831
+ params = req.body;
1832
+ } else {
1833
+ // Assume form body - convert to Parameters
1834
+ params = this.formToParameters(req.body, req.query);
1835
+ }
1836
+ } else {
1837
+ // GET request - convert query to Parameters
1838
+ params = this.queryToParameters(req.query);
1839
+ }
1840
+ this.addHttpParams(req, params);
1841
+
1842
+ // Handle tx-resource and cache-id parameters
1843
+ this.setupAdditionalResources(params);
1844
+ const logExtraOutput = this.findParameter(params, 'logExtraOutput');
1845
+
1846
+ let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false);
1847
+ txp.readParams(params);
1848
+
1849
+ // Perform the expansion
1850
+ return await this.doExpand(valueSet, txp, logExtraOutput);
1851
+ }
1852
+
1801
1853
  /**
1802
1854
  * Handle instance-level expand: /ValueSet/{id}/$expand
1803
1855
  * ValueSet identified by resource ID
@@ -137,7 +137,7 @@ class SearchWorker extends TerminologyWorker {
137
137
  const searchParams = {};
138
138
  for (const [key, value] of Object.entries(params)) {
139
139
  if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
140
- searchParams[key] = value.toLowerCase();
140
+ searchParams[key] = key == 'url' ? value : value.toLowerCase();
141
141
  }
142
142
  }
143
143
 
@@ -146,6 +146,9 @@ class SearchWorker extends TerminologyWorker {
146
146
 
147
147
  for (const [key, cs] of this.provider.codeSystems) {
148
148
  this.deadCheck('searchCodeSystems');
149
+ if (cs.url == 'http://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets') {
150
+ console.log("debug");
151
+ }
149
152
  if (key == cs.vurl) {
150
153
  const json = cs.jsonObj;
151
154
 
@@ -179,7 +182,7 @@ class SearchWorker extends TerminologyWorker {
179
182
  }
180
183
  } else if (param === 'url') { // exact match
181
184
  const propValue = json.url;
182
- if (propValue != searchValue) {
185
+ if (propValue !== searchValue) {
183
186
  isMatch = false;
184
187
  break;
185
188
  }
@@ -17,7 +17,7 @@ function terminologyCapabilitiesToR5(jsonObj, sourceVersion) {
17
17
  if (VersionUtilities.isR4Ver(sourceVersion)) {
18
18
  for (const cs of jsonObj.codeSystem || []) {
19
19
  if (cs.content) {
20
- let cnt = Extensions.readString("http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
20
+ let cnt = Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
21
21
  if (cnt) {
22
22
  delete cs.extensions;
23
23
  cs.content = cnt;