fhirsmith 0.7.3 → 0.7.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to the Health Intersections Node Server will be documented i
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v0.7.4] - 2026-03-19
9
+
10
+ ### Changed
11
+
12
+ - XIG: show using resource package explicitly
13
+ - TX: Check conformance statement production at start up
14
+
15
+ ### Fixed
16
+ - TX: Load URI provider on tx.fhir.org
17
+ - TX: fix error getting SCT version for html format
18
+
19
+ ### Tx Conformance Statement
20
+
21
+ FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.9.0)
22
+
8
23
  ## [v0.7.3] - 2026-03-19
9
24
 
10
25
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
5
5
  "main": "server.js",
6
6
  "engines": {
@@ -1275,7 +1275,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
1275
1275
 
1276
1276
  id() {
1277
1277
  const match = this.version().match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
1278
- return "SCT-"+match[1]+"-"+match[2];
1278
+ return match && match[1] && match[2] ? "SCT-"+match[1]+"-"+match[2] : null;
1279
1279
  }
1280
1280
 
1281
1281
  describeVersion(version) {
package/tx/cs/cs-uri.js CHANGED
@@ -21,7 +21,7 @@ class UriServices extends CodeSystemProvider {
21
21
  }
22
22
 
23
23
  version() {
24
- return 'n/a';
24
+ return null;
25
25
  }
26
26
 
27
27
  description() {
@@ -182,7 +182,7 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
182
182
  }
183
183
 
184
184
  defaultVersion() {
185
- return 'n/a';
185
+ return null;
186
186
  }
187
187
 
188
188
  system() {
@@ -190,7 +190,7 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
190
190
  }
191
191
 
192
192
  version() {
193
- return 'n/a';
193
+ return null;
194
194
  }
195
195
 
196
196
  // eslint-disable-next-line no-unused-vars
package/tx/library.js CHANGED
@@ -34,6 +34,7 @@ const {VSACValueSetProvider} = require("./vs/vs-vsac");
34
34
  const { OCLCodeSystemProvider, OCLSourceCodeSystemFactory } = require('./ocl/cs-ocl');
35
35
  const { OCLValueSetProvider } = require('./ocl/vs-ocl');
36
36
  const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
37
+ const {UriServicesFactory} = require("./cs/cs-uri");
37
38
 
38
39
  /**
39
40
  * This class holds all the loaded content ready for processing
@@ -454,6 +455,12 @@ class Library {
454
455
  this.registerProvider('internal', hgvs);
455
456
  break;
456
457
  }
458
+ case "urls" : {
459
+ const urls = new UriServicesFactory(this.i18n);
460
+ await urls.load();
461
+ this.registerProvider('internal', urls);
462
+ break;
463
+ }
457
464
  case "vsac" : {
458
465
  if (!this.vsacCfg || !this.vsacCfg.apiKey) {
459
466
  throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
@@ -8,6 +8,7 @@ sources:
8
8
  - internal:areacode
9
9
  - internal:mimetypes
10
10
  - internal:usstates
11
+ - internal:urls
11
12
  - internal:hgvs
12
13
  - ucum:tx/data/ucum-essence.xml
13
14
  - loinc:loinc-2.77-a.db
package/tx/tx.js CHANGED
@@ -196,6 +196,9 @@ class TXModule {
196
196
  }
197
197
 
198
198
  this.log.info(`TX module initialized with ${config.endpoints.length} endpoint(s)`);
199
+
200
+ // Self-test: verify metadata generation works for each endpoint before accepting traffic
201
+ await this.selfTest();
199
202
  }
200
203
 
201
204
  /**
@@ -388,8 +391,8 @@ class TXModule {
388
391
  }
389
392
 
390
393
  if (contentType.includes('application/json') ||
391
- contentType.includes('application/fhir+json') ||
392
- contentType.includes('application/json+fhir')) {
394
+ contentType.includes('application/fhir+json') ||
395
+ contentType.includes('application/json+fhir')) {
393
396
 
394
397
  // If body is a Buffer, parse it
395
398
  if (Buffer.isBuffer(req.body)) {
@@ -731,11 +734,11 @@ class TXModule {
731
734
  router.get('/CodeSystem/:id/\\$validate-code', async (req, res) => {
732
735
  const start = Date.now();
733
736
  try {
734
- let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
737
+ let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
735
738
  await worker.handleCodeSystemInstance(req, res, this.log);
736
- } finally {
737
- this.countRequest('$validate', Date.now() - start);
738
- }
739
+ } finally {
740
+ this.countRequest('$validate', Date.now() - start);
741
+ }
739
742
  });
740
743
  router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
741
744
  const start = Date.now();
@@ -745,7 +748,7 @@ class TXModule {
745
748
  } finally {
746
749
  this.countRequest('$validate', Date.now() - start);
747
750
  }
748
-
751
+
749
752
  });
750
753
 
751
754
  // ValueSet/[id]/$validate-code
@@ -964,6 +967,79 @@ class TXModule {
964
967
  });
965
968
  }
966
969
 
970
+ /**
971
+ * Self-test: exercise CapabilityStatement and TerminologyCapabilities generation
972
+ * for each endpoint immediately after startup, throwing on any failure.
973
+ */
974
+ async selfTest() {
975
+ this.log.info('Running startup self-test for metadata endpoints...');
976
+
977
+ for (const endpointInfo of this.endpoints) {
978
+ const label = `${endpointInfo.path} (FHIR v${endpointInfo.fhirVersion})`;
979
+
980
+ // Build a minimal mock req/res that captures what metadataHandler.handle() produces
981
+ const makeMockReqRes = (mode) => {
982
+ const captured = { data: null, status: 200 };
983
+
984
+ const req = {
985
+ method: 'GET',
986
+ query: { mode },
987
+ headers: {},
988
+ // eslint-disable-next-line no-unused-vars
989
+ get: (name) => null,
990
+ txEndpoint: endpointInfo,
991
+ txProvider: endpointInfo.provider,
992
+ };
993
+
994
+ const res = {
995
+ statusCode: 200,
996
+ status(code) { captured.status = code; return this; },
997
+ setHeader() { return this; },
998
+ json(data) { captured.data = data; return this; },
999
+ send(data) { captured.data = data; return this; },
1000
+ };
1001
+
1002
+ return { req, res, captured };
1003
+ };
1004
+
1005
+ // Test 1: CapabilityStatement (/metadata with no mode, or mode=full)
1006
+ try {
1007
+ const { req, res, captured } = makeMockReqRes(undefined);
1008
+ await this.metadataHandler.handle(req, res);
1009
+ if (!captured.data) {
1010
+ throw new Error('No response data returned');
1011
+ }
1012
+ const rt = captured.data.resourceType;
1013
+ if (rt !== 'CapabilityStatement') {
1014
+ throw new Error(`Expected CapabilityStatement, got ${rt}`);
1015
+ }
1016
+ this.log.info(` [OK] CapabilityStatement for ${label}`);
1017
+ } catch (err) {
1018
+ this.log.error(` [FAIL] CapabilityStatement for ${label}: ${err.message}`);
1019
+ throw new Error(`Startup self-test failed (CapabilityStatement, ${label}): ${err.message}`);
1020
+ }
1021
+
1022
+ // Test 2: TerminologyCapabilities (/metadata?mode=terminology)
1023
+ try {
1024
+ const { req, res, captured } = makeMockReqRes('terminology');
1025
+ await this.metadataHandler.handle(req, res);
1026
+ if (!captured.data) {
1027
+ throw new Error('No response data returned');
1028
+ }
1029
+ const rt = captured.data.resourceType;
1030
+ if (rt !== 'TerminologyCapabilities') {
1031
+ throw new Error(`Expected TerminologyCapabilities, got ${rt}`);
1032
+ }
1033
+ this.log.info(` [OK] TerminologyCapabilities for ${label}`);
1034
+ } catch (err) {
1035
+ this.log.error(` [FAIL] TerminologyCapabilities for ${label}: ${err.message}`);
1036
+ throw new Error(`Startup self-test failed (TerminologyCapabilities, ${label}): ${err.message}`);
1037
+ }
1038
+ }
1039
+
1040
+ this.log.info('Startup self-test passed.');
1041
+ }
1042
+
967
1043
  /**
968
1044
  * Build an OperationOutcome for errors
969
1045
  */
@@ -1077,21 +1153,21 @@ class TXModule {
1077
1153
  ec = 0;
1078
1154
 
1079
1155
  checkProperJson() { // jsonStr) {
1080
- // const errors = [];
1081
- // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1082
- // if (jsonStr.includes('""')) errors.push('Found "" in json');
1083
- //
1084
- // if (errors.length > 0) {
1085
- // this.ec++;
1086
- // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1087
- // writeFileSync(filename, jsonStr);
1088
- // throw new Error(errors.join('; '));
1089
- // }
1156
+ // const errors = [];
1157
+ // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1158
+ // if (jsonStr.includes('""')) errors.push('Found "" in json');
1159
+ //
1160
+ // if (errors.length > 0) {
1161
+ // this.ec++;
1162
+ // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1163
+ // writeFileSync(filename, jsonStr);
1164
+ // throw new Error(errors.join('; '));
1165
+ // }
1090
1166
  }
1091
1167
 
1092
1168
  transformResourceForVersion(data, fhirVersion) {
1093
1169
  if (fhirVersion == "5.0" || !data.resourceType) {
1094
- return data;
1170
+ return data;
1095
1171
  }
1096
1172
  switch (data.resourceType) {
1097
1173
  case "CodeSystem": return codeSystemFromR5(data, fhirVersion);
package/xig/xig.js CHANGED
@@ -1241,13 +1241,14 @@ function buildAdditionalForm(queryParams) {
1241
1241
  }
1242
1242
  }
1243
1243
 
1244
+
1244
1245
  // Add text search field and package filter field
1245
1246
  html += `Text: <input type="text" name="text" value="${escape(text || '')}" class="" style="width: 200px;"/> `;
1246
1247
  html += `Package: <input type="text" name="pkg" value="${escape(pkg || '')}" placeholder="e.g. hl7.fhir.us" class="" style="width: 200px;"/> `;
1247
1248
 
1248
1249
  // Add submit button with 'only used' checkbox immediately before it
1249
1250
  const onlyUsedChecked = onlyUsed === 'true' ? ' checked' : '';
1250
- html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Used `;
1251
+ html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Show Used `;
1251
1252
  html += '<input type="submit" value="Search" style="color:rgb(89, 137, 241)"/>';
1252
1253
 
1253
1254
  html += '</form>';
@@ -2747,11 +2748,14 @@ function buildDependencyTable(dependencies) {
2747
2748
  }
2748
2749
  currentType = dep.ResourceType;
2749
2750
  html += '<table class="table table-bordered">';
2750
- html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${escape(currentType)}</strong></td></tr>`;
2751
+ html += `<tr style="background-color: #eeeeee"><td colspan="3"><strong>${escape(currentType)}</strong></td></tr>`;
2751
2752
  }
2752
2753
 
2753
2754
  html += '<tr>';
2754
2755
 
2756
+ // Package column
2757
+ html += `<td>${escape(dep.PID || '')}</td>`;
2758
+
2755
2759
  // Build the link to the resource detail page
2756
2760
  const packagePid = dep.PID.replace(/#/g, '|'); // Convert # to | for URL
2757
2761
  const resourceUrl = `/xig/resource/${encodeURIComponent(packagePid)}/${encodeURIComponent(dep.ResourceType)}/${encodeURIComponent(dep.Id)}`;