fhirsmith 0.7.3 → 0.7.5

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.
@@ -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
@@ -100,6 +101,7 @@ class Library {
100
101
  this.conceptMapProviders = [];
101
102
  this.oclProviderSets = new Map();
102
103
  this.oclConfig = {};
104
+ this.ignored = new Set();
103
105
 
104
106
  // Create package manager for FHIR packages
105
107
  const packageServers = ['https://packages2.fhir.org/packages'];
@@ -160,6 +162,7 @@ class Library {
160
162
  const config = yaml.parse(yamlContent);
161
163
  this.baseUrl = config.base.url;
162
164
  this.oclConfig = config.ocl && typeof config.ocl === 'object' ? config.ocl : {};
165
+ this.ignored = new Set(Array.isArray(config.ignored) ? config.ignored : []);
163
166
 
164
167
  this.log.info('Fetching Data from '+this.baseUrl);
165
168
 
@@ -287,7 +290,7 @@ class Library {
287
290
  case 'ocl':
288
291
  await this.loadOcl(details, isDefault, mode);
289
292
  break;
290
-
293
+
291
294
  default:
292
295
  throw new Error(`Unknown source type: ${type}`);
293
296
  }
@@ -454,6 +457,12 @@ class Library {
454
457
  this.registerProvider('internal', hgvs);
455
458
  break;
456
459
  }
460
+ case "urls" : {
461
+ const urls = new UriServicesFactory(this.i18n);
462
+ await urls.load();
463
+ this.registerProvider('internal', urls);
464
+ break;
465
+ }
457
466
  case "vsac" : {
458
467
  if (!this.vsacCfg || !this.vsacCfg.apiKey) {
459
468
  throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
@@ -560,6 +569,17 @@ class Library {
560
569
  this.registerProvider(omopFN, omop, isDefault);
561
570
  }
562
571
 
572
+ /**
573
+ * Returns true if the given url/version should be excluded from npm/url package loading.
574
+ * Matches against the ignored list using either plain url or url#version.
575
+ */
576
+ #isIgnored(url, version) {
577
+ if (this.ignored.size === 0) return false;
578
+ if (this.ignored.has(url)) return true;
579
+ if (version && this.ignored.has(`${url}#${version}`)) return true;
580
+ return false;
581
+ }
582
+
563
583
  async loadNpm(packageManager, details, isDefault, mode, csOnly) {
564
584
  // Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2")
565
585
  let packageId = details;
@@ -584,6 +604,10 @@ class Library {
584
604
  let csc = 0;
585
605
  for (const resource of resources) {
586
606
  const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
607
+ if (this.#isIgnored(cs.url, cs.version)) {
608
+ this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
609
+ continue;
610
+ }
587
611
  cs.sourcePackage = contentLoader.pid();
588
612
  cp.codeSystems.push(cs);
589
613
  csc++;
@@ -618,6 +642,10 @@ class Library {
618
642
  let csc = 0;
619
643
  for (const resource of resources) {
620
644
  const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
645
+ if (this.#isIgnored(cs.url, cs.version)) {
646
+ this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
647
+ continue;
648
+ }
621
649
  cs.sourcePackage = contentLoader.pid();
622
650
  cp.codeSystems.set(cs.url, cs);
623
651
  cp.codeSystems.set(cs.vurl, cs);
@@ -898,4 +926,4 @@ class Library {
898
926
 
899
927
  }
900
928
 
901
- module.exports = { Library };
929
+ module.exports = { Library };
@@ -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
@@ -39,3 +40,6 @@ sources:
39
40
  - npm:us.cdc.phinvads
40
41
  - npm:hl7.fhir.uv.sdc
41
42
  - internal:vsac
43
+
44
+ ignored:
45
+ - urn:iso:std:iso:3166#20210120
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);
@@ -1389,17 +1389,22 @@ class ValueSetChecker {
1389
1389
  let hd = list.hasDisplay(this.params.workingLanguages(), null, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE)
1390
1390
  if (!hd.found) {
1391
1391
  let baseMsg;
1392
- if (hd.difference === DisplayDifference.Normalized) {
1392
+ let severity = this.dispWarning();
1393
+ if (list.userDefined) {
1394
+ baseMsg = 'Display_Name_Not_Fixed_use_Supplement';
1395
+ severity = 'information';
1396
+ } else if (hd.difference === DisplayDifference.Normalized) {
1393
1397
  baseMsg = 'Display_Name_WS_for__should_be_one_of__instead_of';
1394
1398
  } else {
1395
1399
  baseMsg = 'Display_Name_for__should_be_one_of__instead_of';
1396
1400
  }
1397
- let mid = baseMsg;
1398
1401
  let dc = list.displayCount(this.params.workingLanguages(), null, true);
1399
- let severity = this.dispWarning();
1400
- if (dc === 0) {
1401
- severity = 'warning';
1402
- dc = list.displayCount(this.params.workingLanguages(), null, false);
1402
+ let mid = baseMsg;
1403
+ if (severity !== 'information') {
1404
+ if (dc === 0) {
1405
+ severity = 'warning';
1406
+ dc = list.displayCount(this.params.workingLanguages(), null, false);
1407
+ }
1403
1408
  }
1404
1409
 
1405
1410
  let m, ds;
@@ -276,12 +276,9 @@ class TerminologyWorker {
276
276
  if (this.hasSupplement(cs, supplements)) {
277
277
  continue;
278
278
  }
279
- // Handle exact URL match (no version specified in supplements)
279
+ // Handle exact URL match (no version specified in supplements field)
280
280
  if (supplementsUrl === url) {
281
- // If we're looking for a specific version, only include if no version in supplements URL
282
- if (!version) {
283
- supplements.push(cs);
284
- }
281
+ supplements.push(cs);
285
282
  continue;
286
283
  }
287
284
 
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)}`;