fhirsmith 0.5.6 → 0.7.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +2 -0
  3. package/configurations/projector.json +21 -0
  4. package/configurations/readme.md +5 -0
  5. package/library/html-server.js +2 -1
  6. package/library/package-manager.js +37 -34
  7. package/library/utilities.js +10 -1
  8. package/library/version-utilities.js +85 -0
  9. package/package.json +1 -1
  10. package/packages/package-crawler.js +144 -52
  11. package/packages/packages.js +15 -7
  12. package/publisher/publisher.js +15 -3
  13. package/registry/api.js +173 -191
  14. package/registry/crawler.js +100 -65
  15. package/registry/model.js +14 -8
  16. package/registry/registry.js +5 -0
  17. package/root-template.html +1 -0
  18. package/server.js +113 -45
  19. package/tx/README.md +4 -4
  20. package/tx/cs/cs-api.js +18 -1
  21. package/tx/cs/cs-base.js +1 -0
  22. package/tx/cs/cs-loinc.js +5 -2
  23. package/tx/cs/cs-provider-api.js +25 -1
  24. package/tx/cs/cs-provider-list.js +2 -2
  25. package/tx/cs/cs-rxnorm.js +9 -2
  26. package/tx/cs/cs-snomed.js +17 -2
  27. package/tx/html/codesystem-operations.liquid +17 -24
  28. package/tx/html/valueset-operations.liquid +46 -52
  29. package/tx/library/canonical-resource.js +6 -1
  30. package/tx/library/codesystem.js +6 -1
  31. package/tx/library/renderer.js +81 -7
  32. package/tx/library.js +145 -13
  33. package/tx/ocl/README.md +236 -0
  34. package/tx/ocl/cache/cache-paths.cjs +32 -0
  35. package/tx/ocl/cache/cache-paths.js +2 -0
  36. package/tx/ocl/cache/cache-utils.cjs +43 -0
  37. package/tx/ocl/cache/cache-utils.js +2 -0
  38. package/tx/ocl/cm-ocl.cjs +531 -0
  39. package/tx/ocl/cm-ocl.js +1 -105
  40. package/tx/ocl/cs-ocl.cjs +1779 -0
  41. package/tx/ocl/cs-ocl.js +1 -38
  42. package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
  43. package/tx/ocl/fingerprint/fingerprint.js +2 -0
  44. package/tx/ocl/http/client.cjs +31 -0
  45. package/tx/ocl/http/client.js +2 -0
  46. package/tx/ocl/http/pagination.cjs +98 -0
  47. package/tx/ocl/http/pagination.js +2 -0
  48. package/tx/ocl/jobs/background-queue.cjs +200 -0
  49. package/tx/ocl/jobs/background-queue.js +2 -0
  50. package/tx/ocl/mappers/concept-mapper.cjs +66 -0
  51. package/tx/ocl/mappers/concept-mapper.js +2 -0
  52. package/tx/ocl/model/concept-filter-context.cjs +51 -0
  53. package/tx/ocl/model/concept-filter-context.js +2 -0
  54. package/tx/ocl/shared/constants.cjs +15 -0
  55. package/tx/ocl/shared/constants.js +2 -0
  56. package/tx/ocl/shared/patches.cjs +224 -0
  57. package/tx/ocl/shared/patches.js +2 -0
  58. package/tx/ocl/vs-ocl.cjs +1848 -0
  59. package/tx/ocl/vs-ocl.js +1 -104
  60. package/tx/operation-context.js +8 -1
  61. package/tx/params.js +24 -3
  62. package/tx/provider.js +51 -2
  63. package/tx/sct/expressions.js +20 -9
  64. package/tx/tx-html.js +144 -51
  65. package/tx/tx.js +10 -2
  66. package/tx/vs/vs-vsac.js +4 -3
  67. package/tx/workers/batch-validate.js +3 -2
  68. package/tx/workers/batch.js +3 -2
  69. package/tx/workers/expand.js +125 -18
  70. package/tx/workers/lookup.js +5 -4
  71. package/tx/workers/read.js +2 -1
  72. package/tx/workers/related.js +3 -2
  73. package/tx/workers/search.js +6 -8
  74. package/tx/workers/subsumes.js +3 -2
  75. package/tx/workers/translate.js +4 -3
  76. package/tx/workers/validate.js +132 -40
  77. package/tx/workers/worker.js +1 -7
  78. package/tx/xversion/xv-terminologyCapabilities.js +1 -1
@@ -8,6 +8,8 @@ const {
8
8
  ServerInformation,
9
9
  ServerVersionInformation,
10
10
  } = require('./model');
11
+ const {Extensions} = require("../tx/library/extensions");
12
+ const {debugLog} = require("../tx/operation-context");
11
13
 
12
14
  const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
13
15
 
@@ -30,38 +32,13 @@ class RegistryCrawler {
30
32
  this.errors = [];
31
33
  this.totalBytes = 0;
32
34
  this.log = console;
35
+ this.abortController = null;
33
36
  }
34
37
 
35
38
  useLog(logv) {
36
39
  this.log = logv;
37
40
  }
38
41
 
39
- // /**
40
- // * Start the crawler with periodic updates
41
- // */
42
- // start() {
43
- // if (this.crawlTimer) {
44
- // return; // Already running
45
- // }
46
- //
47
- // // Initial crawl
48
- // this.crawl();
49
- //
50
- // // Set up periodic crawling
51
- // this.crawlTimer = setInterval(() => {
52
- // this.crawl();
53
- // }, this.config.crawlInterval);
54
- // }
55
- //
56
- // /**
57
- // * Stop the crawler
58
- // */
59
- // stop() {
60
- // if (this.crawlTimer) {
61
- // clearInterval(this.crawlTimer);
62
- // this.crawlTimer = null;
63
- // }
64
- // }
65
42
 
66
43
  /**
67
44
  * Main entry point - crawl the registry starting from the master URL
@@ -73,6 +50,7 @@ class RegistryCrawler {
73
50
  this.addLogEntry('warn', 'Crawl already in progress, skipping...');
74
51
  return this.currentData;
75
52
  }
53
+ this.abortController = new AbortController();
76
54
 
77
55
  this.isCrawling = true;
78
56
  const startTime = new Date();
@@ -100,6 +78,7 @@ class RegistryCrawler {
100
78
  // Process each registry
101
79
  const registries = masterJson.registries || [];
102
80
  for (const registryConfig of registries) {
81
+ if (this.abortController?.signal.aborted) break;
103
82
  const registry = await this.processRegistry(registryConfig);
104
83
  if (registry) {
105
84
  newData.registries.push(registry);
@@ -111,6 +90,7 @@ class RegistryCrawler {
111
90
  // Update the current data
112
91
  this.currentData = newData;
113
92
  } catch (error) {
93
+ debugLog(error);
114
94
  this.addLogEntry('error', 'Exception Scanning:', error);
115
95
  this.currentData.outcome = `Error: ${error.message}`;
116
96
  this.errors.push({
@@ -142,7 +122,7 @@ class RegistryCrawler {
142
122
  }
143
123
 
144
124
  if (!registry.address) {
145
- this.addLogEntry('error', `No url provided for ${registry.name, registry.name}`, '');
125
+ this.addLogEntry('error', `No url provided for ${registry.name}`, '');
146
126
  return registry;
147
127
  }
148
128
 
@@ -158,6 +138,7 @@ class RegistryCrawler {
158
138
  // Process each server in the registry
159
139
  const servers = registryJson.servers || [];
160
140
  for (const serverConfig of servers) {
141
+ if (this.abortController?.signal.aborted) break;
161
142
  const server = await this.processServer(serverConfig, registry.address);
162
143
  if (server) {
163
144
  registry.servers.push(server);
@@ -165,6 +146,7 @@ class RegistryCrawler {
165
146
  }
166
147
 
167
148
  } catch (error) {
149
+ debugLog(error);
168
150
  registry.error = error.message;
169
151
  this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
170
152
  }
@@ -181,7 +163,7 @@ class RegistryCrawler {
181
163
  server.name = serverConfig.name;
182
164
  server.address = serverConfig.url || '';
183
165
  server.accessInfo = serverConfig.access_info || '';
184
-
166
+
185
167
  if (!server.name) {
186
168
  this.addLogEntry('error', 'No name provided for server', source);
187
169
  return server;
@@ -200,7 +182,8 @@ class RegistryCrawler {
200
182
  // Process each FHIR version
201
183
  const fhirVersions = serverConfig.fhirVersions || [];
202
184
  for (const versionConfig of fhirVersions) {
203
- const version = await this.processServerVersion(versionConfig, server);
185
+ if (this.abortController?.signal.aborted) break;
186
+ const version = await this.processServerVersion(versionConfig, server, serverConfig.exclusions);
204
187
  if (version) {
205
188
  server.versions.push(version);
206
189
  }
@@ -212,7 +195,7 @@ class RegistryCrawler {
212
195
  /**
213
196
  * Process a single server version
214
197
  */
215
- async processServerVersion(versionConfig, server) {
198
+ async processServerVersion(versionConfig, server, exclusions) {
216
199
  const version = new ServerVersionInformation();
217
200
  version.version = versionConfig.version;
218
201
  version.address = versionConfig.url;
@@ -233,20 +216,20 @@ class RegistryCrawler {
233
216
 
234
217
  switch (majorVersion) {
235
218
  case 3:
236
- await this.processServerVersionR3(version, server);
219
+ await this.processServerVersionR3(version, server, exclusions);
237
220
  break;
238
221
  case 4:
239
- await this.processServerVersionR4(version, server);
222
+ await this.processServerVersionR4or5(version, server, '4.0.1', exclusions);
240
223
  break;
241
224
  case 5:
242
- await this.processServerVersionR5(version, server);
225
+ await this.processServerVersionR4or5(version, server, '5.0.0', exclusions);
243
226
  break;
244
227
  default:
245
228
  throw new Error(`Version ${version.version} not supported`);
246
229
  }
247
230
 
248
231
  // Sort and deduplicate
249
- version.codeSystems = [...new Set(version.codeSystems)].sort();
232
+ version.codeSystems.sort((a, b) => this.compareCS(a, b));
250
233
  version.valueSets = [...new Set(version.valueSets)].sort();
251
234
  version.lastSuccess = new Date();
252
235
  version.lastTat = `${Date.now() - startTime}ms`;
@@ -254,6 +237,7 @@ class RegistryCrawler {
254
237
  this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
255
238
 
256
239
  } catch (error) {
240
+ debugLog(error);
257
241
  const elapsed = Date.now() - startTime;
258
242
  this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
259
243
  version.error = error.message;
@@ -266,7 +250,7 @@ class RegistryCrawler {
266
250
  /**
267
251
  * Process an R3 server
268
252
  */
269
- async processServerVersionR3(version, server) {
253
+ async processServerVersionR3(version, server, exclusions) {
270
254
  // Get capability statement
271
255
  const capabilityUrl = `${version.address}/metadata`;
272
256
  const capability = await this.fetchJson(capabilityUrl, server.name);
@@ -283,12 +267,12 @@ class RegistryCrawler {
283
267
  termCap.parameter.forEach(param => {
284
268
  if (param.name === 'system') {
285
269
  const uri = param.valueUri || param.valueString;
286
- if (uri) {
270
+ if (uri && !this.isExcluded(uri, exclusions)) {
287
271
  version.codeSystems.push(uri);
288
272
  // Look for version parts
289
273
  if (param.part) {
290
274
  param.part.forEach(part => {
291
- if (part.name === 'version' && part.valueString) {
275
+ if (part.name === 'version' && part.valueString && !this.isExcluded(uri+'|'+part.valueString, exclusions)) {
292
276
  version.codeSystems.push(`${uri}|${part.valueString}`);
293
277
  }
294
278
  });
@@ -298,24 +282,28 @@ class RegistryCrawler {
298
282
  });
299
283
  }
300
284
  } catch (error) {
301
- this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
285
+ debugLog(error);
286
+ this.addLogEntry('error', `Could not fetch terminology capabilities from ${version.address}: ${error.message}`);
302
287
  }
303
-
288
+
289
+ if (this.abortController?.signal.aborted) return;
304
290
  // Search for value sets
305
- await this.fetchValueSets(version, server);
291
+ await this.fetchValueSets(version, server, exclusions);
306
292
  }
307
293
 
308
294
  /**
309
295
  * Process an R4 server
310
296
  */
311
- async processServerVersionR4(version, server) {
297
+ async processServerVersionR4or5(version, server, defVersion, exclusions) {
312
298
  // Get capability statement
313
299
  const capabilityUrl = `${version.address}/metadata`;
314
300
  const capability = await this.fetchJson(capabilityUrl, server.code);
315
301
 
316
- version.version = capability.fhirVersion || '4.0.1';
302
+ version.version = capability.fhirVersion || defVersion;
317
303
  version.software = capability.software ? capability.software.name : "unknown";
318
-
304
+
305
+ let set = new Set();
306
+
319
307
  // Get terminology capabilities
320
308
  try {
321
309
  const termCapUrl = `${version.address}/metadata?mode=terminology`;
@@ -323,12 +311,19 @@ class RegistryCrawler {
323
311
 
324
312
  if (termCap.codeSystem) {
325
313
  termCap.codeSystem.forEach(cs => {
326
- if (cs.uri) {
327
- version.codeSystems.push(cs.uri);
314
+ let content = cs.content || Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
315
+ if (cs.uri && !this.isExcluded(cs.uri, exclusions)) {
316
+ if (!set.has(cs.uri)) {
317
+ set.add(cs.uri);
318
+ version.codeSystems.push(this.addContent({uri: cs.uri}, content));
319
+ }
328
320
  if (cs.version) {
329
321
  cs.version.forEach(v => {
330
- if (v.code) {
331
- version.codeSystems.push(`${cs.uri}|${v.code}`);
322
+ if (v.code && !this.isExcluded(cs.uri+"|"+v.code, exclusions)) {
323
+ if (!set.has(cs.uri+"|"+v.code)) {
324
+ version.codeSystems.push(this.addContent({uri: cs.uri, version: v.code}, content));
325
+ set.add(cs.uri+"|"+v.code);
326
+ }
332
327
  }
333
328
  });
334
329
  }
@@ -336,20 +331,12 @@ class RegistryCrawler {
336
331
  });
337
332
  }
338
333
  } catch (error) {
339
- this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
334
+ debugLog(error);
335
+ this.addLogEntry('error', `Could not fetch terminology capabilities from ${version.address}: ${error.message}`);
340
336
  }
341
337
 
342
338
  // Search for value sets
343
- await this.fetchValueSets(version, server);
344
- }
345
-
346
- /**
347
- * Process an R5 server
348
- */
349
- async processServerVersionR5(version, server) {
350
- // R5 is essentially the same as R4 for our purposes
351
- await this.processServerVersionR4(version, server);
352
- version.version = version.version || '5.0.0';
339
+ await this.fetchValueSets(version, server, exclusions);
353
340
  }
354
341
 
355
342
  /**
@@ -360,16 +347,22 @@ class RegistryCrawler {
360
347
  * @param {Object} version - The server version information
361
348
  * @param {Object} server - The server information
362
349
  */
363
- async fetchValueSets(version, server) {
350
+ async fetchValueSets(version, server, exclusions) {
364
351
  // Initial search URL
365
- let searchUrl = `${version.address}/ValueSet?_elements=url,version`;
352
+ let count = 0;
353
+ let searchUrl = `${version.address}/ValueSet?_elements=url,version`+(version.address.includes("fhir.org") ? "&_count=200" : "");
366
354
  try {
367
355
  // Set of URLs to avoid duplicates
368
356
  const valueSetUrls = new Set();
369
357
 
370
-
371
358
  // Continue fetching while we have a URL
372
359
  while (searchUrl) {
360
+ count++;
361
+ if (count == 1000) {
362
+ throw new Error(`Fetch ValueSet loop exceeded 1000 iterations - a logic problem on the server? (${version.address})`);
363
+ }
364
+
365
+ if (this.abortController?.signal.aborted) break;
373
366
  this.log.debug(`Fetching value sets from ${searchUrl}`);
374
367
  const bundle = await this.fetchJson(searchUrl, server.code);
375
368
 
@@ -378,9 +371,9 @@ class RegistryCrawler {
378
371
  bundle.entry.forEach(entry => {
379
372
  if (entry.resource) {
380
373
  const vs = entry.resource;
381
- if (vs.url) {
374
+ if (vs.url && !this.isExcluded(vs.url, exclusions)) {
382
375
  valueSetUrls.add(vs.url);
383
- if (vs.version) {
376
+ if (vs.version && !this.isExcluded(vs.url+'|'+vs.version, exclusions)) {
384
377
  valueSetUrls.add(`${vs.url}|${vs.version}`);
385
378
  }
386
379
  }
@@ -402,6 +395,7 @@ class RegistryCrawler {
402
395
  version.valueSets = Array.from(valueSetUrls).sort();
403
396
 
404
397
  } catch (error) {
398
+ debugLog(error);
405
399
  this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
406
400
  }
407
401
  }
@@ -460,6 +454,7 @@ class RegistryCrawler {
460
454
  const response = await axios.get(fetchUrl, {
461
455
  timeout: this.config.timeout,
462
456
  headers: headers,
457
+ signal: this.abortController?.signal,
463
458
  validateStatus: (status) => status < 500 // Don't throw on 4xx
464
459
  });
465
460
 
@@ -478,6 +473,7 @@ class RegistryCrawler {
478
473
  return response.data;
479
474
 
480
475
  } catch (error) {
476
+ debugLog(error);
481
477
  if (error.response) {
482
478
  throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
483
479
  } else if (error.request) {
@@ -621,19 +617,58 @@ class RegistryCrawler {
621
617
  * @param {string} level - Filter by log level
622
618
  * @returns {Array} Array of log entries
623
619
  */
624
- getLogs(limit = 100)
620
+ getLogs(limit = 100, level = null)
625
621
  {
626
622
  if (!this.logs) {
627
623
  return [];
628
624
  }
629
625
 
630
626
  // Filter by level if specified
631
- let filteredLogs = this.logs;
627
+ let filteredLogs = level ? this.logs.filter(entry => entry.level === level) : this.logs;
632
628
 
633
629
  // Get the latest entries up to the limit
634
630
  return filteredLogs.slice(-limit);
635
631
  }
636
632
 
633
+ addContent(param, content) {
634
+ if (content) {
635
+ param.content = content;
636
+ }
637
+ return param;
638
+ }
639
+
640
+ compareCS(a, b) {
641
+ if (a.version || b.version) {
642
+ let s = (a.uri+'|'+a.version) || '';
643
+ return s.localeCompare(b.uri+'|'+b.version);
644
+ } else {
645
+ return (a.uri || '').localeCompare(b.uri);
646
+ }
647
+ }
648
+
649
+ isExcluded(url, exclusions) {
650
+ for (let exclusion of exclusions || []) {
651
+ let match = false;
652
+ if (exclusion.endsWith('*')) {
653
+ const prefix = exclusion.slice(0, -1);
654
+ match = url.startsWith(prefix);
655
+ } else {
656
+ // Otherwise do exact matching on both full and base URL
657
+ match = url === exclusion;
658
+ }
659
+ if (match) {
660
+ return true;
661
+ }
662
+ }
663
+ return false;
664
+ }
665
+
666
+ shutdown() {
667
+ if (this.abortController) {
668
+ this.abortController.abort();
669
+ }
670
+ }
671
+
637
672
  }
638
673
 
639
674
  module.exports = RegistryCrawler;
package/registry/model.js CHANGED
@@ -37,7 +37,7 @@ class ServerVersionInformation {
37
37
  getCsListHtml() {
38
38
  if (this.codeSystems.length === 0) return '<ul></ul>';
39
39
  return '<ul>' + this.codeSystems.map(cs =>
40
- `<li>${escape(cs)}</li>`
40
+ `<li>${escape(cs.uri+(cs.version ? '|'+cs.version : ''))}</li>`
41
41
  ).join('') + '</ul>';
42
42
  }
43
43
 
@@ -392,7 +392,7 @@ class ServerRegistryUtilities {
392
392
  return value === mask;
393
393
  }
394
394
 
395
- static hasMatchingCodeSystem(cs, list, supportMask) {
395
+ static hasMatchingCodeSystem(cs, list, supportMask, content) {
396
396
  if (!cs || list.length === 0) return false;
397
397
 
398
398
  // Handle URLs with pipes - extract base URL
@@ -403,13 +403,19 @@ class ServerRegistryUtilities {
403
403
 
404
404
  return list.some(item => {
405
405
  // If we support wildcards (masks) and the item ends with "*", do prefix matching
406
- if (supportMask && item.endsWith('*')) {
407
- const prefix = item.slice(0, -1);
408
- return cs.startsWith(prefix) || baseCs.startsWith(prefix);
406
+ let vurl = item.uri ? item.version ? item.uri+"|"+item.version : item.uri : item;
407
+ let ok = false;
408
+ if (supportMask && vurl.endsWith('*')) {
409
+ const prefix = vurl.slice(0, -1);
410
+ ok = cs.startsWith(prefix) || baseCs.startsWith(prefix);
411
+ } else {
412
+ // Otherwise do exact matching on both full and base URL
413
+ ok = vurl === cs || vurl === baseCs;
409
414
  }
410
-
411
- // Otherwise do exact matching on both full and base URL
412
- return item === cs || item === baseCs;
415
+ if (ok && content) {
416
+ content.content = item.content;
417
+ }
418
+ return ok;
413
419
  });
414
420
  }
415
421
 
@@ -926,6 +926,9 @@ class RegistryModule {
926
926
  this.crawlInterval = null;
927
927
  }
928
928
 
929
+ if (this.crawler) {
930
+ this.crawler.shutdown();
931
+ }
929
932
  // Save current data
930
933
  if (this.crawler && this.currentData) {
931
934
  await this.saveData();
@@ -1155,6 +1158,7 @@ class RegistryModule {
1155
1158
  html += '<th>URL</th>';
1156
1159
  html += '<th>Security</th>';
1157
1160
  html += '<th>Access Info</th>';
1161
+ html += '<th>Content</th>';
1158
1162
  html += '</tr>';
1159
1163
  html += '</thead>';
1160
1164
  html += '<tbody>';
@@ -1165,6 +1169,7 @@ class RegistryModule {
1165
1169
  html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
1166
1170
  html += `<td>${this.renderSecurityTags(server)}</td>`;
1167
1171
  html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
1172
+ html += `<td>${server.content ? escape(server.content) : ''}</td>`;
1168
1173
  html += '</tr>';
1169
1174
  });
1170
1175
 
@@ -74,6 +74,7 @@
74
74
 
75
75
  [%content%]
76
76
 
77
+ [%about%]
77
78
 
78
79
  </div>
79
80
 
package/server.js CHANGED
@@ -395,6 +395,14 @@ process.on('uncaughtException', (error) => {
395
395
  });
396
396
 
397
397
  app.get('/', async (req, res) => {
398
+ // If an override index.html exists, serve it instead of the FHIRsmith home page
399
+ if (config.server?.webBase) {
400
+ const overrideIndex = path.join(path.resolve(config.server.webBase), 'index.html');
401
+ if (fs.existsSync(overrideIndex)) {
402
+ return res.sendFile(overrideIndex);
403
+ }
404
+ }
405
+
398
406
  // Check if client wants HTML response
399
407
  const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
400
408
 
@@ -410,69 +418,50 @@ app.get('/', async (req, res) => {
410
418
 
411
419
  const content = await buildRootPageContent();
412
420
 
421
+ // Load optional about box fragment from data directory
422
+ let about = '';
423
+ const aboutPath = path.join(folders.dataDir(), 'about.html');
424
+ if (fs.existsSync(aboutPath)) {
425
+ about = fs.readFileSync(aboutPath, 'utf8');
426
+ }
427
+
413
428
  // Build basic stats for root page
414
429
  const stats = {
415
430
  version: packageJson.version,
416
431
  enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
417
- processingTime: Date.now() - startTime
432
+ processingTime: Date.now() - startTime,
433
+ about
418
434
  };
419
435
 
420
436
  const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
421
437
  res.setHeader('Content-Type', 'text/html');
422
438
  res.send(html);
439
+ return;
423
440
  } catch (error) {
424
441
  serverLog.error('Error rendering root page:', error);
425
442
  htmlServer.sendErrorResponse(res, 'root', error);
443
+ return;
426
444
  }
427
- } else {
428
- // Return JSON response for API clients
429
- const enabledModules = {};
430
- Object.keys(config.modules).forEach(moduleName => {
431
- if (config.modules[moduleName].enabled) {
432
- if (moduleName === 'tx') {
433
- // TX module has multiple endpoints
434
- enabledModules[moduleName] = {
435
- enabled: true,
436
- endpoints: config.modules.tx.endpoints.map(e => ({
437
- path: e.path,
438
- fhirVersion: e.fhirVersion,
439
- context: e.context || null
440
- }))
441
- };
442
- } else {
443
- enabledModules[moduleName] = {
444
- enabled: true,
445
- endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
446
- };
447
- }
448
- }
449
- });
450
-
451
- res.json({
452
- message: 'FHIR Development Server',
453
- version: '1.0.0',
454
- modules: enabledModules,
455
- endpoints: {
456
- health: '/health',
457
- ...Object.fromEntries(
458
- Object.keys(enabledModules)
459
- .filter(m => m !== 'tx')
460
- .map(m => [
461
- m,
462
- m === 'vcl' ? '/VCL' : `/${m}`
463
- ])
464
- ),
465
- // Add TX endpoints separately
466
- ...(enabledModules.tx ? {
467
- tx: config.modules.tx.endpoints.map(e => e.path)
468
- } : {})
469
- }
470
- });
471
445
  }
446
+ return serveFhirsmithHome(req, res);
472
447
  });
473
448
 
449
+ app.get('/fhirsmith', (req, res) => serveFhirsmithHome(req, res));
474
450
 
475
451
  // Serve static files
452
+ if (config.server?.webBase) {
453
+ const overrideDir = path.resolve(config.server.webBase);
454
+ app.use((req, res, next) => {
455
+ const filePath = path.join(overrideDir, req.path);
456
+ fs.access(filePath, fs.constants.F_OK, (err) => {
457
+ if (!err) {
458
+ res.sendFile(filePath);
459
+ } else {
460
+ next();
461
+ }
462
+ });
463
+ });
464
+ }
476
465
  app.use(express.static(path.join(__dirname, 'static')));
477
466
 
478
467
  // Health check endpoint
@@ -610,5 +599,84 @@ process.on('SIGINT', async () => {
610
599
  process.exit(0);
611
600
  });
612
601
 
602
+ async function serveFhirsmithHome(req, res) {
603
+ // Check if client wants HTML response
604
+ const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
605
+
606
+ if (acceptsHtml) {
607
+ try {
608
+ const startTime = Date.now();
609
+
610
+ // Load template if not already loaded
611
+ if (!htmlServer.hasTemplate('root')) {
612
+ const templatePath = path.join(__dirname, 'root-template.html');
613
+ htmlServer.loadTemplate('root', templatePath);
614
+ }
615
+
616
+ const content = await buildRootPageContent();
617
+
618
+ // Build basic stats for root page
619
+ const stats = {
620
+ version: packageJson.version,
621
+ enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
622
+ processingTime: Date.now() - startTime
623
+ };
624
+
625
+ const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
626
+ res.setHeader('Content-Type', 'text/html');
627
+ res.send(html);
628
+ return;
629
+ } catch (error) {
630
+ serverLog.error('Error rendering root page:', error);
631
+ htmlServer.sendErrorResponse(res, 'root', error);
632
+ return;
633
+ }
634
+ } else {
635
+ // Return JSON response for API clients
636
+ const enabledModules = {};
637
+ Object.keys(config.modules).forEach(moduleName => {
638
+ if (config.modules[moduleName].enabled) {
639
+ if (moduleName === 'tx') {
640
+ // TX module has multiple endpoints
641
+ enabledModules[moduleName] = {
642
+ enabled: true,
643
+ endpoints: config.modules.tx.endpoints.map(e => ({
644
+ path: e.path,
645
+ fhirVersion: e.fhirVersion,
646
+ context: e.context || null
647
+ }))
648
+ };
649
+ } else {
650
+ enabledModules[moduleName] = {
651
+ enabled: true,
652
+ endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
653
+ };
654
+ }
655
+ }
656
+ });
657
+
658
+ res.json({
659
+ message: 'FHIR Development Server',
660
+ version: '1.0.0',
661
+ modules: enabledModules,
662
+ endpoints: {
663
+ health: '/health',
664
+ ...Object.fromEntries(
665
+ Object.keys(enabledModules)
666
+ .filter(m => m !== 'tx')
667
+ .map(m => [
668
+ m,
669
+ m === 'vcl' ? '/VCL' : `/${m}`
670
+ ])
671
+ ),
672
+ // Add TX endpoints separately
673
+ ...(enabledModules.tx ? {
674
+ tx: config.modules.tx.endpoints.map(e => e.path)
675
+ } : {})
676
+ }
677
+ });
678
+ }
679
+ }
680
+
613
681
  // Start the server
614
682
  startServer();
package/tx/README.md CHANGED
@@ -33,22 +33,22 @@ Add the `tx` section to your `config.json`:
33
33
  "endpoints": [
34
34
  {
35
35
  "path": "/tx/r5",
36
- "fhirVersion": 5,
36
+ "fhirVersion": "5.0",
37
37
  "context": null
38
38
  },
39
39
  {
40
40
  "path": "/tx/r4",
41
- "fhirVersion": 4,
41
+ "fhirVersion": "4.0",
42
42
  "context": null
43
43
  },
44
44
  {
45
45
  "path": "/tx/r3",
46
- "fhirVersion": 3,
46
+ "fhirVersion": "3.0",
47
47
  "context": null
48
48
  },
49
49
  {
50
50
  "path": "/tx/r4/demo",
51
- "fhirVersion": 4,
51
+ "fhirVersion": "4.0",
52
52
  "context": "demo"
53
53
  }
54
54
  ]