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.
@@ -1,25 +1,18 @@
1
- <div class="operations" style="margin-top: 20px;">
2
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOperations('{{ opsId }}')">Show Operations</button>
3
- <div id="{{ opsId }}" style="display: none; margin-top: 10px;">
1
+ <div class="operation-form" style="margin-bottom: 15px;">
2
+ <strong>Lookup</strong>
3
+ <form method="get" action="$lookup" style="display: inline-block; margin-left: 10px;">
4
+ <input type="hidden" name="system" value="{{ url }}"/>
5
+ <input type="text" name="code" placeholder="Code" required size="20"/>
6
+ <button type="submit" class="btn btn-sm btn-primary">Lookup</button>
7
+ </form>
8
+ </div>
4
9
 
5
- <div class="operation-form" style="margin-bottom: 15px;">
6
- <strong>Lookup</strong>
7
- <form method="get" action="$lookup" style="display: inline-block; margin-left: 10px;">
8
- <input type="hidden" name="system" value="{{ url }}"/>
9
- <input type="text" name="code" placeholder="Code" required size="20"/>
10
- <button type="submit" class="btn btn-sm btn-primary">Lookup</button>
11
- </form>
12
- </div>
13
-
14
- <div class="operation-form" style="margin-bottom: 15px;">
15
- <strong>Subsumes</strong>
16
- <form method="get" action="$subsumes" style="display: inline-block; margin-left: 10px;">
17
- <input type="hidden" name="system" value="{{ url }}"/>
18
- <input type="text" name="codeA" placeholder="Code A" required size="15"/>
19
- <input type="text" name="codeB" placeholder="Code B" required size="15"/>
20
- <button type="submit" class="btn btn-sm btn-primary">Subsumes</button>
21
- </form>
22
- </div>
23
-
24
- </div>
25
- </div>
10
+ <div class="operation-form" style="margin-bottom: 15px;">
11
+ <strong>Subsumes</strong>
12
+ <form method="get" action="$subsumes" style="display: inline-block; margin-left: 10px;">
13
+ <input type="hidden" name="system" value="{{ url }}"/>
14
+ <input type="text" name="codeA" placeholder="Code A" required size="15"/>
15
+ <input type="text" name="codeB" placeholder="Code B" required size="15"/>
16
+ <button type="submit" class="btn btn-sm btn-primary">Subsumes</button>
17
+ </form>
18
+ </div>
@@ -1,54 +1,48 @@
1
- <div class="operations" style="margin-top: 20px;">
2
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOperations('{{ opsId }}')">Show Operations</button>
3
- <div id="{{ opsId }}" style="display: none; margin-top: 10px;">
4
1
 
5
- <div class="operation-form" style="margin-bottom: 15px;">
6
- <strong>Expand</strong>
7
- <form method="get" action="$expand" style="margin-left: 10px; margin-top: 5px;">
8
- <input type="hidden" name="url" value="{{ url }}"/>
9
- <table class="grid" cellpadding="0" cellspacing="0">
10
- <tr>
11
- <td>Filter: <input type="text" name="filter" size="20"/></td>
12
- <td>Language: <input type="text" name="displayLanguage" size="10"/></td>
13
- </tr>
14
- <tr>
15
- <td colspan="2">
16
- <input type="checkbox" name="includeDesignations" id="expand_desig" value="true"/>
17
- <label for="expand_desig">Include Designations</label>
18
- <input type="checkbox" name="activeOnly" id="expand_active" value="true"/>
19
- <label for="expand_active">Active Only</label>
20
- </td>
21
- </tr>
22
- </table>
23
- <button type="submit" class="btn btn-sm btn-primary">Expand</button>
24
- </form>
25
- </div>
2
+ <div class="operation-form" style="margin-bottom: 15px;">
3
+ <strong>Expand</strong>
4
+ <form method="get" action="$expand" style="margin-left: 10px; margin-top: 5px;">
5
+ <input type="hidden" name="url" value="{{ url }}"/>
6
+ <table class="grid" cellpadding="0" cellspacing="0">
7
+ <tr>
8
+ <td>Filter: <input type="text" name="filter" size="20"/></td>
9
+ <td>Language: <input type="text" name="displayLanguage" size="10"/></td>
10
+ </tr>
11
+ <tr>
12
+ <td colspan="2">
13
+ <input type="checkbox" name="includeDesignations" id="expand_desig" value="true"/>
14
+ <label for="expand_desig">Include Designations</label>
15
+ <input type="checkbox" name="activeOnly" id="expand_active" value="true"/>
16
+ <label for="expand_active">Active Only</label>
17
+ </td>
18
+ </tr>
19
+ </table>
20
+ <button type="submit" class="btn btn-sm btn-primary">Expand</button>
21
+ </form>
22
+ </div>
26
23
 
27
- <div class="operation-form" style="margin-bottom: 15px;">
28
- <strong>Validate Code (ValueSet)</strong>
29
- <form method="get" action="$validate-code" style="margin-left: 10px; margin-top: 5px;">
30
- <input type="hidden" name="url" value="{{ url }}"/>
31
- <input type="hidden" name="inferSystem" id="{{ inferSystemId }}" value="true"/>
32
- <table class="grid" cellpadding="0" cellspacing="0">
33
- <tr>
34
- <td>System: <input type="text" name="system" id="{{ vcSystemId }}" size="30" onchange="updateInferSystem('{{ vcSystemId }}', '{{ inferSystemId }}')"/></td>
35
- <td>Version: <input type="text" name="version" size="10"/></td>
36
- </tr>
37
- <tr>
38
- <td>Code: <input type="text" name="code" size="20" required/></td>
39
- <td>Display: <input type="text" name="display" size="20"/></td>
40
- </tr>
41
- <tr>
42
- <td>Language: <input type="text" name="displayLanguage" size="10"/></td>
43
- <td>
44
- <input type="checkbox" name="abstract" value="true"/>
45
- <label>Abstract</label>
46
- </td>
47
- </tr>
48
- </table>
49
- <button type="submit" class="btn btn-sm btn-primary">Validate Code</button>
50
- </form>
51
- </div>
52
-
53
- </div>
54
- </div>
24
+ <div class="operation-form" style="margin-bottom: 15px;">
25
+ <strong>Validate Code (ValueSet)</strong>
26
+ <form method="get" action="$validate-code" style="margin-left: 10px; margin-top: 5px;">
27
+ <input type="hidden" name="url" value="{{ url }}"/>
28
+ <input type="hidden" name="inferSystem" id="{{ inferSystemId }}" value="true"/>
29
+ <table class="grid" cellpadding="0" cellspacing="0">
30
+ <tr>
31
+ <td>System: <input type="text" name="system" id="{{ vcSystemId }}" size="30" onchange="updateInferSystem('{{ vcSystemId }}', '{{ inferSystemId }}')"/></td>
32
+ <td>Version: <input type="text" name="version" size="10"/></td>
33
+ </tr>
34
+ <tr>
35
+ <td>Code: <input type="text" name="code" size="20" required/></td>
36
+ <td>Display: <input type="text" name="display" size="20"/></td>
37
+ </tr>
38
+ <tr>
39
+ <td>Language: <input type="text" name="displayLanguage" size="10"/></td>
40
+ <td>
41
+ <input type="checkbox" name="abstract" value="true"/>
42
+ <label>Abstract</label>
43
+ </td>
44
+ </tr>
45
+ </table>
46
+ <button type="submit" class="btn btn-sm btn-primary">Validate Code</button>
47
+ </form>
48
+ </div>
@@ -33,7 +33,12 @@ class CodeSystem extends CanonicalResource {
33
33
  // Convert to R5 format internally (modifies input for performance)
34
34
  this.jsonObj = codeSystemToR5(this.jsonObj, fhirVersion);
35
35
  if (!noMaps) {
36
- this.validate();
36
+ try {
37
+ this.validate();
38
+ } catch (e) {
39
+ const id = this.jsonObj?.url ? `${this.jsonObj.url}|${this.jsonObj.version || ''}` : this.jsonObj?.name || 'unknown';
40
+ throw new Error(`${e.message} (in ${id})`);
41
+ }
37
42
  this.buildMaps();
38
43
  }
39
44
  }
@@ -147,6 +147,7 @@ class Renderer {
147
147
  this.renderProperty(tbl, 'GENERAL_TITLE', res.title);
148
148
  this.renderProperty(tbl, 'GENERAL_STATUS', res.status);
149
149
  this.renderPropertyMD(tbl, 'GENERAL_DEFINITION', res.description);
150
+ this.renderPropertyMD(tbl, 'GENERAL_PURPOSE', res.purpose);
150
151
  this.renderProperty(tbl, 'CANON_REND_PUBLISHER', res.publisher);
151
152
  this.renderProperty(tbl, 'CANON_REND_COMMITTEE', Extensions.readString(res, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg'));
152
153
  this.renderProperty(tbl, 'GENERAL_COPYRIGHT', res.copyright);
@@ -339,7 +340,7 @@ class Renderer {
339
340
  }
340
341
  if (vs.expansion) {
341
342
  div_.h2().tx("Expansion");
342
- await this.renderExpansion(div_.table("grid"), vs, tbl);
343
+ await this.renderExpansion(div_, vs, tbl);
343
344
  }
344
345
 
345
346
  return div_.toString();
@@ -419,7 +420,7 @@ class Renderer {
419
420
  li.tx(this.translate('VALUE_SET_ALL_CODES_DEF')+" ");
420
421
  await this.renderLink(li,inc.system+(inc.version ? "|"+inc.version : ""));
421
422
  } else if (inc.concept) {
422
- li.tx(this.translate('VALUE_SET_THESE_CODES_DEF'));
423
+ li.tx(this.translate('VALUE_SET_THESE_CODES_DEF')+" ");
423
424
  await this.renderLink(li,inc.system+(inc.version ? "|"+inc.version : ""));
424
425
  li.tx(":");
425
426
  const ul = li.ul();
@@ -898,18 +899,45 @@ class Renderer {
898
899
  return count;
899
900
  }
900
901
 
902
+ async renderVSExpansion(vs, showProps) {
903
+ let div_ = div();
904
+ let tbl;
905
+ if (showProps) {
906
+ div_.h2().tx("Expansion Properties");
907
+ tbl = div_.table("grid");
908
+ } else {
909
+ tbl = div(); // dummy
910
+ }
911
+ await this.renderExpansion(div_.table("grid"), vs, tbl);
912
+ return div_.toString();
913
+ }
914
+
901
915
  async renderExpansion(x, vs, tbl) {
902
916
  this.renderProperty(tbl, 'Expansion Identifier', vs.expansion.identifier);
903
917
  this.renderProperty(tbl, 'Expansion Timestamp', vs.expansion.timestamp);
904
918
  this.renderProperty(tbl, 'Expansion Total', vs.expansion.total);
905
919
  this.renderProperty(tbl, 'Expansion Offset', vs.expansion.offset);
920
+ const warnings = [];
921
+ const warningNames = new Set(['deprecated', 'withdrawn', 'retired', 'experimental', 'draft']);
922
+ const useds = [];
923
+ const usedNames = new Set(['codesystem', 'valueset', 'supplement']);
906
924
  for (let p of vs.expansion.parameter || []) {
907
- if( getValueName(p) === 'valueUri' || getValueName(p) === 'valueCanonical') {
925
+ if (p.name.startsWith('warning-') && warningNames.has(p.name.substring(8))) {
926
+ warnings.push(p);
927
+ } else if (p.name.startsWith('used-') && usedNames.has(p.name.substring(5))) {
928
+ useds.push(p);
929
+ } else if( getValueName(p) === 'valueUri' || getValueName(p) === 'valueCanonical') {
908
930
  await this.renderPropertyLink(tbl, "Parameter: " + p.name, getValuePrimitive(p));
909
931
  } else {
910
932
  this.renderProperty(tbl, "Parameter: " + p.name, getValuePrimitive(p));
911
933
  }
912
934
  }
935
+ if (useds.length > 0) {
936
+ await this.renderUsed(x, useds);
937
+ }
938
+ if (warnings.length > 0) {
939
+ await this.renderWarnings(x, warnings);
940
+ }
913
941
 
914
942
  if (!vs.expansion.contains || vs.expansion.contains.length === 0) {
915
943
  x.para().i().tx('No concepts in expansion');
@@ -935,10 +963,10 @@ class Renderer {
935
963
  }
936
964
  headerRow.th().tx(this.translate('TX_DISPLAY'));
937
965
  if (columnInfo.hasAbstract) {
938
- headerRow.th().tx('Abstract');
966
+ headerRow.th().tx(this.translate('Abstract'));
939
967
  }
940
968
  if (columnInfo.hasInactive) {
941
- headerRow.th().tx('Inactive');
969
+ headerRow.th().tx(this.translate('VALUE_SET_INACTIVE'));
942
970
  }
943
971
 
944
972
  // Property columns (from expansion.property definitions)
@@ -1117,12 +1145,12 @@ class Renderer {
1117
1145
 
1118
1146
  // Abstract column
1119
1147
  if (columnInfo.hasAbstract) {
1120
- tr.td().tx(contains.abstract === true ? 'true' : '');
1148
+ tr.td().tx(contains.abstract === true ? 'abstract' : '');
1121
1149
  }
1122
1150
 
1123
1151
  // Inactive column
1124
1152
  if (columnInfo.hasInactive) {
1125
- tr.td().tx(contains.inactive === true ? 'true' : '');
1153
+ tr.td().tx(contains.inactive === true ? this.translate('VALUE_SET_INACT') : '');
1126
1154
  }
1127
1155
 
1128
1156
  // Property columns
@@ -1566,6 +1594,52 @@ class Renderer {
1566
1594
 
1567
1595
  return div_.toString();
1568
1596
  }
1597
+
1598
+ async renderWarnings(x, warnings) {
1599
+ await this.renderWarningsForStatus(x, 'deprecated', warnings);
1600
+ await this.renderWarningsForStatus(x, 'withdrawn', warnings);
1601
+ await this.renderWarningsForStatus(x, 'retired', warnings);
1602
+ await this.renderWarningsForStatus(x, 'experimental', warnings);
1603
+ await this.renderWarningsForStatus(x, 'draft', warnings);
1604
+ }
1605
+
1606
+ async renderWarningsForStatus(x, name, warnings) {
1607
+ const wl = warnings.filter(item => item.name == 'warning-'+name);
1608
+ if (wl && wl.length > 0) {
1609
+ x.para().tx(`This ValueSet depends on the following ${name} ValueSets: `);
1610
+ let ul = x.ul();
1611
+ for (const w of wl) {
1612
+ const linkinfo = await this.linkResolver.resolveURL(this.opContext, getValuePrimitive(w));
1613
+ if (linkinfo) {
1614
+ ul.li().ah(linkinfo.link).tx(linkinfo.description);
1615
+ } else {
1616
+ ul.li().code().tx(getValuePrimitive(w));
1617
+ }
1618
+ }
1619
+ }
1620
+ }
1621
+
1622
+ async renderUsed(x, list) {
1623
+ x.para().tx(`This ValueSet depends on the following items:`);
1624
+ let ul = x.ul();
1625
+ await this.renderUsedForType(ul, 'codesystem', 'CodeSystem', list);
1626
+ await this.renderUsedForType(ul, 'valueset', 'ValueSet', list);
1627
+ await this.renderUsedForType(ul, 'supplement', 'Supplement', list);
1628
+ }
1629
+
1630
+ async renderUsedForType(ul, name, title, list) {
1631
+ const wl = list.filter(item => item.name == 'used-' + name);
1632
+ for (const w of wl) {
1633
+ const li = ul.li();
1634
+ li.tx(title+": ");
1635
+ const linkinfo = await this.linkResolver.resolveURL(this.opContext, getValuePrimitive(w));
1636
+ if (linkinfo) {
1637
+ li.ah(linkinfo.link).tx(linkinfo.description);
1638
+ } else {
1639
+ li.code().tx(getValuePrimitive(w));
1640
+ }
1641
+ }
1642
+ }
1569
1643
  }
1570
1644
 
1571
1645
  module.exports = { Renderer };
package/tx/library.js CHANGED
@@ -158,7 +158,12 @@ class Library {
158
158
  this.log.info('Fetching Data from '+this.baseUrl);
159
159
 
160
160
  for (const source of config.sources) {
161
- await this.processSource(source, this.packageManager, "fetch");
161
+ try {
162
+ await this.processSource(source, this.packageManager, "fetch");
163
+ } catch (error) {
164
+ console.error(`Failed to fetch source '${source}': ${error.message}`);
165
+ throw error;
166
+ }
162
167
  }
163
168
 
164
169
  this.log.info("Downloaded "+((this.totalDownloaded + this.packageManager.totalDownloaded)/ 1024)+" kB");
@@ -167,13 +172,23 @@ class Library {
167
172
  this.#logSystemHeader();
168
173
 
169
174
  for (const source of config.sources) {
170
- await this.processSource(source, this.packageManager, "cs");
175
+ try {
176
+ await this.processSource(source, this.packageManager, "cs");
177
+ } catch (error) {
178
+ console.error(`Failed to load code systems from '${source}': ${error.message}`);
179
+ throw error;
180
+ }
171
181
  }
172
182
  this.log.info('Loading Packages');
173
183
  this.#logPackagesHeader();
174
184
 
175
185
  for (const source of config.sources) {
176
- await this.processSource(source, this.packageManager, "npm");
186
+ try {
187
+ await this.processSource(source, this.packageManager, "npm");
188
+ } catch (error) {
189
+ console.error(`Failed to load package '${source}': ${error.message}`);
190
+ throw error;
191
+ }
177
192
  }
178
193
 
179
194
  const endMemory = process.memoryUsage();
package/tx/provider.js CHANGED
@@ -153,6 +153,7 @@ class Provider {
153
153
  }
154
154
 
155
155
  getCodeSystemById(opContext, id) {
156
+
156
157
  // Search through codeSystems map for matching id
157
158
  for (const cs of this.codeSystems.values()) {
158
159
  if (opContext) opContext.deadCheck('getCodeSystemById');
@@ -318,9 +319,10 @@ class Provider {
318
319
  factory = this.codeSystemFactories.get(vurlMM);
319
320
  }
320
321
  if (factory != null) {
322
+ let vdesc = version == null ? "" : factory.describeVersion(version);
321
323
  return {
322
- link: this.path+"/CodeSystem/"+factory.id(),
323
- description: factory.name()+(version ? " v"+version : "")
324
+ link: this.path+"/CodeSystem/x-"+factory.id(),
325
+ description: factory.nameBase()+' '+vdesc
324
326
  };
325
327
  }
326
328
  let cs = this.codeSystems.get(vurl);
@@ -1469,7 +1469,7 @@ class SnomedExpressionServices {
1469
1469
  /**
1470
1470
  * Validate concept reference
1471
1471
  */
1472
- checkConcept(concept, limit) {
1472
+ checkConcept(concept, limits) {
1473
1473
  if (concept.code) {
1474
1474
  const conceptId = BigInt(concept.code);
1475
1475
  const result = this.concepts.findConcept(conceptId);
@@ -1480,13 +1480,24 @@ class SnomedExpressionServices {
1480
1480
  throw new Error(`Concept ${concept.code} not found`);
1481
1481
  }
1482
1482
  }
1483
- if (limit && concept.reference) {
1484
- // if a limit is specified, then the concept has to be a specialization of that.
1485
- let parentRef = this.concepts.findConcept(limit);
1486
- let descendentsRef = this.concepts.getAllDesc(parentRef.index);
1487
- const descendants = this.refs.getReferences(descendentsRef);
1488
- if (descendants && !descendants.includes(concept.reference)) {
1489
- throw new Error(`Concept ${concept.code} is not valid in this context (must be a ${limit})`);
1483
+ if (limits && concept.reference) {
1484
+ // if a limit is specified, then the concept has to be a specialization of one of them.
1485
+ let ok = false;
1486
+ for (const limit of limits) {
1487
+ let parentRef = this.concepts.findConcept(limit);
1488
+ let descendentsRef = this.concepts.getAllDesc(parentRef.index);
1489
+ const descendants = this.refs.getReferences(descendentsRef);
1490
+ if (descendants && descendants.includes(concept.reference)) {
1491
+ ok = true;
1492
+ break;
1493
+ }
1494
+ }
1495
+ if (!ok) {
1496
+ if (limits.length == 1) {
1497
+ throw new Error(`Concept ${concept.code} is not valid in this context (must be a ${limits[0]})`);
1498
+ } else {
1499
+ throw new Error(`Concept ${concept.code} is not valid in this context (must be a descendent of one of ${limits})`);
1500
+ }
1490
1501
  }
1491
1502
  }
1492
1503
 
@@ -1615,7 +1626,7 @@ class SnomedExpressionServices {
1615
1626
  * Validate refinement
1616
1627
  */
1617
1628
  checkRefinement(refinement) {
1618
- this.checkConcept(refinement.name, '410662002');
1629
+ this.checkConcept(refinement.name, ['410662002', '106237007']);
1619
1630
  this.checkExpression(refinement.value);
1620
1631
  }
1621
1632