fhirsmith 0.7.5 → 0.8.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 (46) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -0
  3. package/library/html.js +4 -0
  4. package/library/languages.js +10 -0
  5. package/package.json +1 -1
  6. package/packages/package-crawler.js +106 -51
  7. package/packages/packages.js +14 -0
  8. package/publisher/publisher.js +118 -28
  9. package/registry/registry.js +99 -91
  10. package/root-bare-template.html +92 -0
  11. package/security.md +32 -0
  12. package/server.js +99 -22
  13. package/stats.js +43 -10
  14. package/tx/README.md +6 -6
  15. package/tx/cs/cs-api.js +3 -0
  16. package/tx/cs/cs-api.md +285 -0
  17. package/tx/cs/cs-loinc.js +14 -2
  18. package/tx/cs/cs-rxnorm.js +14 -10
  19. package/tx/cs/cs-snomed.js +166 -5
  20. package/tx/html/dash-metrics.liquid +147 -0
  21. package/tx/importers/import-rxnorm.module.js +4 -30
  22. package/tx/importers/readme.md +3 -1
  23. package/tx/library/canonical-resource.js +8 -0
  24. package/tx/library/conceptmap.js +3 -1
  25. package/tx/library/designations.js +4 -8
  26. package/tx/library/renderer.js +9 -9
  27. package/tx/library.js +10 -4
  28. package/tx/ocl/cm-ocl.cjs +185 -65
  29. package/tx/ocl/cs-ocl.cjs +69 -50
  30. package/tx/ocl/jobs/background-queue.cjs +0 -8
  31. package/tx/ocl/mappers/concept-mapper.cjs +13 -3
  32. package/tx/ocl/shared/patches.cjs +1 -0
  33. package/tx/ocl/vs-ocl.cjs +137 -157
  34. package/tx/operation-context.js +3 -3
  35. package/tx/provider.js +4 -3
  36. package/tx/sct/structures.js +5 -0
  37. package/tx/tx-html.js +36 -9
  38. package/tx/tx.fhir.org.yml +1 -1
  39. package/tx/tx.js +34 -11
  40. package/tx/vs/vs-database.js +127 -6
  41. package/tx/vs/vs-vsac.js +98 -3
  42. package/tx/workers/search.js +2 -1
  43. package/tx/workers/translate.js +39 -14
  44. package/tx/workers/validate.js +3 -3
  45. package/utilities/dashboard.html +274 -0
  46. package/xig/xig.js +171 -9
@@ -21,7 +21,7 @@ class RegistryModule {
21
21
  this.isInitialized = false;
22
22
  this.lastCrawlTime = null;
23
23
  this.crawlInProgress = false;
24
-
24
+
25
25
  // Thread-safe data storage
26
26
  this.currentData = null;
27
27
  this.dataLock = false;
@@ -46,7 +46,7 @@ class RegistryModule {
46
46
 
47
47
  this.crawler = new RegistryCrawler(crawlerConfig, this.stats);
48
48
  this.crawler.useLog(regLog);
49
-
49
+
50
50
  // Initialize API with crawler
51
51
  this.api = new RegistryAPI(this.crawler);
52
52
 
@@ -79,13 +79,13 @@ class RegistryModule {
79
79
  const dataPath = folders.ensureFilePath('registry', 'registry-data.json'); // <-- CHANGE
80
80
  const data = await fs.readFile(dataPath, 'utf8');
81
81
  const jsonData = JSON.parse(data);
82
-
82
+
83
83
  // Thread-safe update
84
84
  await this.updateData(() => {
85
85
  this.crawler.loadData(jsonData);
86
86
  this.currentData = this.crawler.getData();
87
87
  });
88
-
88
+
89
89
  this.logger.info('Loaded saved registry data');
90
90
  } catch (error) {
91
91
  this.logger.info('No saved registry data found, will fetch fresh data');
@@ -113,7 +113,7 @@ class RegistryModule {
113
113
  */
114
114
  startPeriodicCrawl(intervalMinutes) {
115
115
  const intervalMs = intervalMinutes * 60 * 1000;
116
-
116
+
117
117
  // Run initial crawl after a short delay
118
118
  setTimeout(() => {
119
119
  this.performCrawl();
@@ -145,7 +145,7 @@ class RegistryModule {
145
145
  try {
146
146
  // Perform the crawl
147
147
  const newData = await this.crawler.crawl(this.config.masterUrl);
148
-
148
+
149
149
  // Thread-safe update of current data
150
150
  await this.updateData(() => {
151
151
  this.currentData = newData;
@@ -153,40 +153,30 @@ class RegistryModule {
153
153
 
154
154
  this.lastCrawlTime = new Date();
155
155
  const elapsed = Date.now() - startTime;
156
-
156
+
157
157
  // Save to disk
158
158
  await this.saveData();
159
-
159
+
160
160
  // Get metadata
161
161
  const metadata = this.crawler.getMetadata();
162
162
  this.logger.info(`Crawl completed in ${(elapsed/1000).toFixed(1)}s. ` +
163
- `Found ${newData.registries.length} registries, ` +
164
- `${metadata.errors.length} errors, ` +
165
- `downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
166
- this.stats.task('TxRegistry', 'Crawling Finished');
163
+ `Found ${newData.registries.length} registries, ` +
164
+ `${metadata.errors.length} errors, ` +
165
+ `downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
166
+ this.stats.taskDone('TxRegistry', 'Crawling Finished');
167
167
  } catch (error) {
168
168
  this.logger.error('Crawl failed:', error);
169
- this.stats.task('TxRegistry', 'Crawling Error: '+error.message);
169
+ this.stats.taskError('TxRegistry', 'Crawling Error: '+error.message);
170
170
  } finally {
171
171
  this.crawlInProgress = false;
172
172
  }
173
173
  }
174
174
 
175
175
  /**
176
- * Thread-safe data update
176
+ * Data update - no locking needed, Node.js is single-threaded
177
177
  */
178
178
  async updateData(updateFn) {
179
- // Simple lock mechanism - in production, consider using a proper mutex
180
- while (this.dataLock) {
181
- await new Promise(resolve => setTimeout(resolve, 10));
182
- }
183
-
184
- this.dataLock = true;
185
- try {
186
- updateFn();
187
- } finally {
188
- this.dataLock = false;
189
- }
179
+ updateFn();
190
180
  }
191
181
 
192
182
  _normalizeQueryParams(query) {
@@ -248,12 +238,12 @@ class RegistryModule {
248
238
  */
249
239
  renderHtmlPage(req, res, jsonResult, basePath, registry, server, fhirVersion, codeSystem, valueSet) {
250
240
  // Generate path with query parameters
251
- let path = basePath;
252
- if (registry) path += `&registry=${encodeURIComponent(registry)}`;
253
- if (server) path += `&server=${encodeURIComponent(server)}`;
254
- if (fhirVersion) path += `&fhirVersion=${encodeURIComponent(fhirVersion)}`;
255
- if (codeSystem) path += `&url=${encodeURIComponent(codeSystem)}`;
256
- if (valueSet) path += `&valueSet=${encodeURIComponent(valueSet)}`;
241
+ let pagePath = basePath;
242
+ if (registry) pagePath += `&registry=${encodeURIComponent(registry)}`;
243
+ if (server) pagePath += `&server=${encodeURIComponent(server)}`;
244
+ if (fhirVersion) pagePath += `&fhirVersion=${encodeURIComponent(fhirVersion)}`;
245
+ if (codeSystem) pagePath += `&url=${encodeURIComponent(codeSystem)}`;
246
+ if (valueSet) pagePath += `&valueSet=${encodeURIComponent(valueSet)}`;
257
247
 
258
248
  // Get registry documentation and info
259
249
  const data = this.api.getData();
@@ -264,7 +254,7 @@ class RegistryModule {
264
254
 
265
255
  // Render matches table
266
256
  const matchesTable = this.api.renderJsonToHtml(
267
- jsonResult, path, registry, server, fhirVersion
257
+ jsonResult, pagePath, registry, server, fhirVersion
268
258
  );
269
259
 
270
260
  // Render registry info
@@ -272,7 +262,7 @@ class RegistryModule {
272
262
 
273
263
  // Assemble template variables
274
264
  const templateVars = {
275
- path,
265
+ path: pagePath,
276
266
  matches: matchesTable,
277
267
  count: jsonResult.results.length,
278
268
  registry: registry || '',
@@ -288,7 +278,7 @@ class RegistryModule {
288
278
  // Use HTML server to render the page
289
279
  try {
290
280
  if (!htmlServer.hasTemplate('registry')) {
291
- const templatePath = path.join(__dirname, 'tx-registry-template.html');
281
+ const templatePath = path.join(__dirname, 'registry-template.html');
292
282
  htmlServer.loadTemplate('registry', templatePath);
293
283
  }
294
284
 
@@ -303,7 +293,7 @@ class RegistryModule {
303
293
  );
304
294
  } catch (error) {
305
295
  this.logger.error('Error rendering page:', error);
306
- return `<html><body><h1>Error rendering page</h1><p>${error.message}</p></body></html>`;
296
+ return `<html><body><h1>Error rendering page</h1><p>${escape(error.message)}</p></body></html>`;
307
297
  }
308
298
  }
309
299
 
@@ -315,9 +305,9 @@ class RegistryModule {
315
305
  if (this.crawlInProgress) {
316
306
  return 'Scanning for updates now';
317
307
  } else if (!this.lastCrawlTime) {
318
- const nextScan = this.crawlInterval ?
308
+ const nextScan = this.crawlInterval ?
319
309
  new Date(Date.now() + this.crawlInterval) : null;
320
-
310
+
321
311
  if (nextScan) {
322
312
  const timeUntil = this.describePeriod(nextScan - Date.now());
323
313
  return `First Scan in ${timeUntil}`;
@@ -325,9 +315,9 @@ class RegistryModule {
325
315
  return 'No automatic scanning configured';
326
316
  }
327
317
  } else {
328
- const nextScan = this.crawlInterval ?
318
+ const nextScan = this.crawlInterval ?
329
319
  new Date(this.lastCrawlTime.getTime() + (this.config.crawlInterval * 60 * 1000)) : null;
330
-
320
+
331
321
  if (nextScan) {
332
322
  const timeUntil = this.describePeriod(nextScan - Date.now());
333
323
  const timeSince = this.describePeriod(Date.now() - this.lastCrawlTime);
@@ -345,7 +335,7 @@ class RegistryModule {
345
335
  */
346
336
  describePeriod(milliseconds) {
347
337
  const seconds = Math.floor(milliseconds / 1000);
348
-
338
+
349
339
  if (seconds < 60) {
350
340
  return `${seconds} seconds`;
351
341
  } else if (seconds < 3600) {
@@ -364,56 +354,63 @@ class RegistryModule {
364
354
  const start = Date.now();
365
355
  try {
366
356
 
367
- const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
368
-
369
- if (!acceptsHtml) {
370
- // Return JSON overview
371
- return res.json({
372
- name: 'FHIR Terminology Server Registry',
373
- description: 'Registry and discovery service for FHIR terminology servers',
374
- endpoints: {
375
- status: '/registry/api/status',
376
- statistics: '/registry/api/stats',
377
- registries: '/registry/api/registries',
378
- queryCodeSystem: '/registry/api/query/codesystem',
379
- queryValueSet: '/registry/api/query/valueset',
380
- bestServer: '/registry/api/best-server/{type}',
381
- errors: '/registry/api/errors'
382
- },
383
- documentation: 'https://github.com/your-org/fhir-registry'
384
- });
385
- }
386
-
387
- // Render HTML page
388
- try {
389
- const startTime = Date.now();
390
-
391
- // Load template if needed
392
- if (!htmlServer.hasTemplate('registry')) {
393
- const templatePath = path.join(__dirname, 'registry-template.html');
394
- htmlServer.loadTemplate('registry', templatePath);
357
+ const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
358
+
359
+ if (!acceptsHtml) {
360
+ // Return JSON overview
361
+ return res.json({
362
+ name: 'FHIR Terminology Server Registry',
363
+ description: 'Registry and discovery service for FHIR terminology servers',
364
+ endpoints: {
365
+ status: '/registry/api/status',
366
+ statistics: '/registry/api/stats',
367
+ registries: '/registry/api/registries',
368
+ queryCodeSystem: '/registry/api/query/codesystem',
369
+ queryValueSet: '/registry/api/query/valueset',
370
+ bestServer: '/registry/api/best-server/{type}',
371
+ errors: '/registry/api/errors'
372
+ },
373
+ documentation: 'https://github.com/your-org/fhir-registry'
374
+ });
395
375
  }
396
376
 
397
- const content = await this.buildHtmlContent();
398
- const stats = this.api.getStatistics();
399
- stats.processingTime = Date.now() - startTime;
400
- stats.crawlInProgress = this.crawlInProgress;
401
- stats.lastCrawl = this.lastCrawlTime;
377
+ // Render HTML page
378
+ try {
379
+ const startTime = Date.now();
402
380
 
403
- const html = htmlServer.renderPage(
404
- 'registry',
405
- 'FHIR Terminology Server Registry',
406
- content,
407
- stats
408
- );
409
-
410
- res.setHeader('Content-Type', 'text/html');
411
- res.send(html);
412
-
413
- } catch (error) {
414
- this.logger.error('Error rendering registry page:', error);
415
- htmlServer.sendErrorResponse(res, 'registry', error);
416
- }
381
+ // Load template if needed
382
+ if (!htmlServer.hasTemplate('registry')) {
383
+ const templatePath = path.join(__dirname, 'registry-template.html');
384
+ try {
385
+ htmlServer.loadTemplate('registry', templatePath);
386
+ } catch (templateError) {
387
+ this.logger.error('Failed to load registry template:', templateError);
388
+ return res.status(500).send(`<html><body><h1>Template Error</h1><p>Could not load registry-template.html: ${escape(templateError.message)}</p></body></html>`);
389
+ }
390
+ }
391
+
392
+ const content = await this.buildHtmlContent();
393
+ this.logger.info('Registry: buildHtmlContent completed');
394
+ const stats = this.api.getStatistics();
395
+ this.logger.info('Registry: getStatistics completed');
396
+ stats.processingTime = Date.now() - startTime;
397
+ stats.crawlInProgress = this.crawlInProgress;
398
+ stats.lastCrawl = this.lastCrawlTime;
399
+
400
+ const html = htmlServer.renderPage(
401
+ 'registry',
402
+ 'FHIR Terminology Server Registry',
403
+ content,
404
+ stats
405
+ );
406
+
407
+ res.setHeader('Content-Type', 'text/html');
408
+ res.send(html);
409
+
410
+ } catch (error) {
411
+ this.logger.error('Error rendering registry page:', error);
412
+ res.status(500).send(`<html><body><h1>Error rendering registry page</h1><pre>${escape(error.message)}\n${escape(error.stack || '')}</pre></body></html>`);
413
+ }
417
414
  } finally {
418
415
  this.stats.countRequest('home', Date.now() - start);
419
416
  }
@@ -429,11 +426,18 @@ class RegistryModule {
429
426
  const stats = this.api.getStatistics();
430
427
  let html = '';
431
428
 
432
- // Skip the overview card and search forms
429
+ const data = this.api.getData();
430
+
431
+ if (!data || !data.registries) {
432
+ html += '<div class="alert alert-info">';
433
+ html += '<h4>Registry data not yet available</h4>';
434
+ html += '<p>The initial crawl is in progress. Please refresh in a moment.</p>';
435
+ html += '</div>';
436
+ return html;
437
+ }
433
438
 
434
439
  // Gather all server versions into a flat list
435
440
  const serverVersions = [];
436
- const data = this.api.getData();
437
441
 
438
442
  data.registries.forEach(registry => {
439
443
  const authority = registry.authority || '';
@@ -546,6 +550,8 @@ class RegistryModule {
546
550
  const data = this.crawler.getData();
547
551
  const authCSMap = new Map();
548
552
 
553
+ if (!data || !data.registries) return [];
554
+
549
555
  // Gather all authoritative code systems
550
556
  data.registries.forEach(registry => {
551
557
  registry.servers.forEach(server => {
@@ -606,6 +612,8 @@ class RegistryModule {
606
612
  const data = this.crawler.getData();
607
613
  const authVSMap = new Map();
608
614
 
615
+ if (!data || !data.registries) return [];
616
+
609
617
  // Gather all authoritative value sets
610
618
  data.registries.forEach(registry => {
611
619
  registry.servers.forEach(server => {
@@ -902,7 +910,7 @@ class RegistryModule {
902
910
  getStatus() {
903
911
  const metadata = this.crawler ? this.crawler.getMetadata() : null;
904
912
  const stats = this.api ? this.api.getStatistics() : null;
905
-
913
+
906
914
  return {
907
915
  enabled: true,
908
916
  initialized: this.isInitialized,
@@ -919,7 +927,7 @@ class RegistryModule {
919
927
  */
920
928
  async shutdown() {
921
929
  this.logger.info('Shutting down Registry module...');
922
-
930
+
923
931
  // Stop periodic crawling
924
932
  if (this.crawlInterval) {
925
933
  clearInterval(this.crawlInterval);
@@ -0,0 +1,92 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <html xml:lang="en" lang="en">
4
+ <head>
5
+ <title>FHIRsmith: [%title%]</title>
6
+
7
+ <meta charset="utf-8"/>
8
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
9
+ <meta content="http://hl7.org/fhir" name="author"/>
10
+ <meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
11
+
12
+ <link rel="stylesheet" href="/fhir.css"/>
13
+
14
+
15
+ <!-- Bootstrap core CSS -->
16
+ <link rel="stylesheet" href="/assets/css/bootstrap.css"/>
17
+ <link rel="stylesheet" href="/assets/css/bootstrap-fhir.css"/>
18
+
19
+ <!-- Project extras -->
20
+ <link rel="stylesheet" href="/assets/css/project.css"/>
21
+ <link rel="stylesheet" href="/assets/css/pygments-manni.css"/>
22
+
23
+ <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
24
+ <!-- [if lt IE 9]>
25
+ <script src="/assets/js/html5shiv.js"></script>
26
+ <script src="/assets/js/respond.min.js"></script>
27
+ <![endif] -->
28
+
29
+ <!-- Favicons -->
30
+ <link sizes="144x144" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-144-precomposed.png"/>
31
+ <link sizes="114x114" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-114-precomposed.png"/>
32
+ <link sizes="72x72" rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-72-precomposed.png"/>
33
+ <link rel="apple-touch-icon-precomposed" href="/assets/ico/apple-touch-icon-57-precomposed.png"/>
34
+ <link rel="shortcut icon" href="/assets/ico/favicon.png"/>
35
+ <script type="text/javascript" src="/assets/js/json2.js"></script>
36
+ <script type="text/javascript" src="/assets/js/statuspage.js"></script>
37
+ <script type="text/javascript" src="/assets/js/jquery.min.js"></script>
38
+ <script type="text/javascript" src="/assets/js/jquery-ui.min.js"></script>
39
+ <link rel="stylesheet" href="/assets/css/jquery.ui.all.css">
40
+ <script type="text/javascript" src="/assets/js/jquery.ui.core.js"></script>
41
+ <script type="text/javascript" src="/assets/js/jquery.ui.widget.js"></script>
42
+ <script type="text/javascript" src="/assets/js/jquery.ui.mouse.js"></script>
43
+ <script type="text/javascript" src="/assets/js/jquery.ui.resizable.js"></script>
44
+ <script type="text/javascript" src="/assets/js/jquery.ui.draggable.js"></script>
45
+ <script type="text/javascript" src="/assets/js/jtip.js"></script>
46
+ <script type="text/javascript" src="/assets/js/jcookie.js"></script>
47
+ <script type="text/javascript" src="/assets/js/fhir-gw.js"></script>
48
+ </head>
49
+
50
+ <body>
51
+
52
+ <!-- /segment-breadcrumb -->
53
+
54
+ <div id="segment-content" class="segment"> <!-- segment-content -->
55
+ <div class="container"> <!-- container -->
56
+ <div class="row">
57
+ <div class="inner-wrapper">
58
+ <div class="col-9">
59
+
60
+ <h2>[%title%] </h2>
61
+
62
+ [%content%]
63
+
64
+
65
+ </div>
66
+
67
+
68
+ </div> <!-- /inner-wrapper -->
69
+ </div> <!-- /row -->
70
+ </div> <!-- /container -->
71
+ </div> <!-- /segment-content -->
72
+
73
+
74
+
75
+
76
+
77
+ <!-- JS and analytics only. -->
78
+ <!-- Bootstrap core JavaScript
79
+ ================================================== -->
80
+ <!-- Placed at the end of the document so the pages load faster -->
81
+ <!-- <script src="/assets/js/jquery.js"/> -->
82
+ <script src="/assets/js/bootstrap.min.js"/>
83
+ <script src="/assets/js/respond.min.js"/>
84
+
85
+ <script src="/assets/js/fhir.js"/>
86
+
87
+ <!-- Analytics Below
88
+ ================================================== -->
89
+
90
+ </body>
91
+
92
+ </html>
package/security.md ADDED
@@ -0,0 +1,32 @@
1
+ # Security Notes for FHIRsmith
2
+
3
+ ## Introduction
4
+
5
+ FHIRsmith is a public ready web server. All the modules are considered safe
6
+ to deploy on the public web, but with some caveats that administrators need
7
+ to pay attention to.
8
+
9
+ ## Supported Versions
10
+
11
+ At this time, only the latest version is supported for security updates.
12
+
13
+ ## Reporting Security Issues
14
+
15
+ Use the standard GitHub security reporting framework.
16
+
17
+ ## Rate Limiting
18
+
19
+ Some modules make extensive use of the file system and/or SQLite databases.
20
+ The server does not itself have any rate limiting arrangements; instead, it
21
+ is expected that the server will be deployed behind NGINX (or similar), and
22
+ that NGINX will be configured to provide rate limiting as appropriate.
23
+
24
+ A typical NGINX configuration would be:
25
+
26
+ ```
27
+ limit_req zone=general burst=6000 delay=20;
28
+ limit_req_status 429;
29
+ limit_conn perip 50;
30
+ limit_conn perserver 500;
31
+ ```
32
+