fhirsmith 0.7.6 → 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.8.0] - 2026-03-27
9
+
10
+ ### Added
11
+
12
+ - XIG: add JSON and CSV downloads
13
+ - TX: Add snomed filter support for inactive, moduleId, and properties
14
+
15
+ ### Changed
16
+
17
+ - Improve Dashboard Presentation
18
+ - Make docker image platform compatible with apple silicon (arm)
19
+ - TX: update rxnorm version for tx.fhir.org
20
+ - TX: Improve VSAC information page
21
+
22
+ ### Fixed
23
+
24
+ - XIG: fix valueset source filter
25
+ - TX: Fix bug in language processing looking up country codes
26
+ - TX: Fix up terminology search for LOINC and generally
27
+ - TX: fix rxnorm property support and search performance
28
+ - Publisher: fix status display when building draft IG
29
+
30
+ ### Tx Conformance Statement
31
+
32
+ FHIRsmith passed all 1498 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.4)
33
+
8
34
  ## [v0.7.6] - 2026-03-25
9
35
 
10
36
  ### Added
@@ -455,6 +455,16 @@ class Languages {
455
455
  return true;
456
456
  }
457
457
 
458
+ includesLanguage(code) {
459
+ const llang = new Language(code);
460
+ for (const lang of this.languages) {
461
+ if (lang.matches(llang)) {
462
+ return true;
463
+ }
464
+ }
465
+ return false;
466
+ }
467
+
458
468
  /**
459
469
  * Convert to string representation (similar to Accept-Language header format)
460
470
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
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": {
@@ -120,7 +120,7 @@ class PackageCrawler {
120
120
  this.log.info(`Web crawler completed successfully in ${runTime}ms`);
121
121
  this.log.info(`Total bytes processed: ${this.totalBytes}`);
122
122
 
123
- this.stats.task('Package Crawler', 'Complete');
123
+ this.stats.taskDone('Package Crawler', 'Complete');
124
124
  return this.crawlerLog;
125
125
 
126
126
  } catch (error) {
@@ -128,7 +128,7 @@ class PackageCrawler {
128
128
  this.crawlerLog.runTime = `${runTime}ms`;
129
129
  this.crawlerLog.fatalException = error.message;
130
130
  this.crawlerLog.endTime = new Date().toISOString();
131
- this.stats.task('Package Crawler', 'Error: '+error.message);
131
+ this.stats.taskError('Package Crawler', 'Error: '+error.message);
132
132
 
133
133
  this.log.error('Web crawler failed: '+ error);
134
134
  throw error;
@@ -481,7 +481,7 @@ class PublisherModule {
481
481
 
482
482
  // Record the log file path and local folder immediately so they're accessible
483
483
  // even if the build fails later
484
- await this.updateTaskStatus(task.id, task.status, {
484
+ await this.updateTaskStatus(task.id, 'building', {
485
485
  build_output_path: logFile,
486
486
  local_folder: taskDir
487
487
  });
@@ -163,10 +163,10 @@ class RegistryModule {
163
163
  `Found ${newData.registries.length} registries, ` +
164
164
  `${metadata.errors.length} errors, ` +
165
165
  `downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
166
- this.stats.task('TxRegistry', 'Crawling Finished');
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
  }
@@ -57,11 +57,10 @@
57
57
  <div class="inner-wrapper">
58
58
  <div class="col-9">
59
59
 
60
- <h2><img border="0" src="/FHIRsmith32.png" style="vertical-align: text-bottom"/> FHIRsmith: [%title%] </h2>
60
+ <h2>[%title%] </h2>
61
61
 
62
62
  [%content%]
63
63
 
64
- [%about%]
65
64
 
66
65
  </div>
67
66
 
package/server.js CHANGED
@@ -267,60 +267,8 @@ async function loadTemplates() {
267
267
  }
268
268
  }
269
269
 
270
- async function buildDashboardContent() {
271
- // Calculate uptime
272
- const uptimeMs = Date.now() - stats.startTime;
273
- const uptimeSeconds = Math.floor(uptimeMs / 1000);
274
- const uptimeDays = Math.floor(uptimeSeconds / 86400);
275
- const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
276
- const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
277
- const uptimeSecs = uptimeSeconds % 60;
278
- let uptimeStr = '';
279
- if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
280
- if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
281
- if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
282
- uptimeStr += `${uptimeSecs}s`;
283
-
284
- // Memory usage
285
- const memUsage = process.memoryUsage();
286
- const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
287
- const heapAvailableMB = ((memUsage.heapTotal - memUsage.heapUsed) / 1024 / 1024).toFixed(2);
288
- const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
289
- const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
290
- const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
291
-
292
- let content = '';
293
- content += '<table class="grid">';
294
- content += '<tr>';
295
- content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
296
- content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
297
- content += `<td><strong>Free Memory:</strong> ${freeMemMB} MB of ${totalMemMB} MB</td>`;
298
- content += '</tr>';
299
- content += '<tr>';
300
- content += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
301
- content += `<td><strong>Heap Available:</strong> ${heapAvailableMB} MB</td>`;
302
- content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
303
- content += '</tr>';
304
- content += getLogStats();
305
- content += '</table>';
306
-
307
- // ===== Metrics Graphs =====
308
- const liquid = new Liquid({
309
- root: path.join(__dirname, 'tx', 'html'),
310
- extname: '.liquid'
311
- });
312
- content += await liquid.renderFile('home-metrics', {
313
- historyJson: JSON.stringify(stats.history),
314
- startTime: stats.startTime
315
- });
316
- content += stats.taskDetails();
317
-
318
- return content;
319
- }
320
-
321
270
  async function buildRootPageContent() {
322
271
  stats.requestCount++;
323
- let mc = 0;
324
272
  let content = '<div class="row mb-4">';
325
273
  content += '<div class="col-12">';
326
274
 
@@ -329,35 +277,30 @@ async function buildRootPageContent() {
329
277
 
330
278
  // Check which modules are enabled and add them to the list
331
279
  if (config.modules.packages.enabled) {
332
- mc++;
333
280
  content += '<li class="list-group-item">';
334
281
  content += '<a href="/packages" class="text-decoration-none">Package Server</a>: Browse and download FHIR Implementation Guide packages';
335
282
  content += '</li>';
336
283
  }
337
284
 
338
285
  if (config.modules.xig.enabled) {
339
- mc++;
340
286
  content += '<li class="list-group-item">';
341
287
  content += '<a href="/xig" class="text-decoration-none">FHIR IG Statistics</a>: Statistics and analysis of FHIR Implementation Guides';
342
288
  content += '</li>';
343
289
  }
344
290
 
345
291
  if (config.modules.shl.enabled) {
346
- mc++;
347
292
  content += '<li class="list-group-item">';
348
293
  content += '<a href="/shl" class="text-decoration-none">SHL Server</a>: SMART Health Links management and validation';
349
294
  content += '</li>';
350
295
  }
351
296
 
352
297
  if (config.modules.vcl.enabled) {
353
- mc++;
354
298
  content += '<li class="list-group-item">';
355
299
  content += '<a href="/VCL" class="text-decoration-none">VCL Server</a>: ValueSet Compose Language expression parsing';
356
300
  content += '</li>';
357
301
  }
358
302
 
359
303
  if (config.modules.registry && config.modules.registry.enabled) {
360
- mc++;
361
304
  content += '<li class="list-group-item">';
362
305
  content += '<a href="/tx-reg" class="text-decoration-none">Terminology Server Registry</a>: ';
363
306
  content += 'Discover and query FHIR terminology servers for code system and value set support';
@@ -365,7 +308,6 @@ async function buildRootPageContent() {
365
308
  }
366
309
 
367
310
  if (config.modules.publisher && config.modules.publisher.enabled) {
368
- mc++;
369
311
  content += '<li class="list-group-item">';
370
312
  content += '<a href="/publisher" class="text-decoration-none">FHIR Publisher</a>: ';
371
313
  content += 'Manage FHIR Implementation Guide publication tasks and approvals';
@@ -373,7 +315,6 @@ async function buildRootPageContent() {
373
315
  }
374
316
 
375
317
  if (config.modules.token && config.modules.token.enabled) {
376
- mc++;
377
318
  content += '<li class="list-group-item">';
378
319
  content += '<a href="/token" class="text-decoration-none">Token Server</a>: ';
379
320
  content += 'OAuth authentication and API key management for FHIR services';
@@ -381,7 +322,6 @@ async function buildRootPageContent() {
381
322
  }
382
323
 
383
324
  if (config.modules.npmprojector && config.modules.npmprojector.enabled) {
384
- mc++;
385
325
  content += '<li class="list-group-item">';
386
326
  content += '<a href="/npmprojector" class="text-decoration-none">NpmProjector</a>: ';
387
327
  content += 'Hot-reloading FHIR server with FHIRPath-based search indexes';
@@ -389,7 +329,6 @@ async function buildRootPageContent() {
389
329
  }
390
330
 
391
331
  if (config.modules?.['ext-tracker']?.enabled) {
392
- mc++;
393
332
  content += '<li class="list-group-item">';
394
333
  content += '<a href="/ext-tracker" class="text-decoration-none">Extension Tracker</a>: ';
395
334
  content += 'View of Extension Usage';
@@ -404,7 +343,6 @@ async function buildRootPageContent() {
404
343
  content += '<ul class="mt-2 mb-0">';
405
344
  for (const fc of folders) {
406
345
  if (fc.enabled === false) continue;
407
- mc++;
408
346
  content += '<li>';
409
347
  content += `<a href="${fc.url}" class="text-decoration-none">${fc.name}</a>: `;
410
348
  content += 'File folder with write control';
@@ -421,7 +359,6 @@ async function buildRootPageContent() {
421
359
  if (config.modules.tx.endpoints && config.modules.tx.endpoints.length > 0) {
422
360
  content += '<ul class="mt-2 mb-0">';
423
361
  for (const endpoint of config.modules.tx.endpoints) {
424
- mc++;
425
362
  content += `<li><a href="${endpoint.path}" class="text-decoration-none">${endpoint.path}</a> (FHIR v${endpoint.fhirVersion}${endpoint.context ? ', context: ' + endpoint.context : ''})</li>`;
426
363
  }
427
364
  content += '</ul>';
@@ -433,8 +370,51 @@ async function buildRootPageContent() {
433
370
 
434
371
  content += '<hr/>';
435
372
 
373
+ // Calculate uptime
374
+ const uptimeMs = Date.now() - stats.startTime;
375
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
376
+ const uptimeDays = Math.floor(uptimeSeconds / 86400);
377
+ const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
378
+ const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
379
+ const uptimeSecs = uptimeSeconds % 60;
380
+ let uptimeStr = '';
381
+ if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
382
+ if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
383
+ if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
384
+ uptimeStr += `${uptimeSecs}s`;
436
385
 
437
- content += await buildDashboardContent();
386
+ // Memory usage
387
+ const memUsage = process.memoryUsage();
388
+ const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
389
+ const heapAvailableMB = ((memUsage.heapTotal - memUsage.heapUsed) / 1024 / 1024).toFixed(2);
390
+ const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
391
+ const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
392
+ const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
393
+
394
+ content += '<table class="grid">';
395
+ content += '<tr>';
396
+ content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
397
+ content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
398
+ content += `<td><strong>Free Memory:</strong> ${freeMemMB} MB of ${totalMemMB} MB</td>`;
399
+ content += '</tr>';
400
+ content += '<tr>';
401
+ content += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
402
+ content += `<td><strong>Heap Available:</strong> ${heapAvailableMB} MB</td>`;
403
+ content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
404
+ content += '</tr>';
405
+ content += getLogStats();
406
+ content += '</table>';
407
+
408
+ // ===== Metrics Graphs =====
409
+ const liquid = new Liquid({
410
+ root: path.join(__dirname, 'tx', 'html'),
411
+ extname: '.liquid'
412
+ });
413
+ content += await liquid.renderFile('home-metrics', {
414
+ historyJson: JSON.stringify(stats.history),
415
+ startTime: stats.startTime
416
+ });
417
+ content += stats.taskDetails();
438
418
 
439
419
  content += '</div>';
440
420
  return content;
@@ -546,8 +526,52 @@ app.get('/dashboard', async (req, res) => {
546
526
  }
547
527
 
548
528
  const startTime = Date.now();
549
- const dashContent = await buildDashboardContent();
550
- const content = '<div class="row mb-4"><div class="col-12">' + dashContent + '</div></div>';
529
+ // Calculate uptime
530
+ const uptimeMs = Date.now() - stats.startTime;
531
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
532
+ const uptimeDays = Math.floor(uptimeSeconds / 86400);
533
+ const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
534
+ const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
535
+ const uptimeSecs = uptimeSeconds % 60;
536
+ let uptimeStr = '';
537
+ if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
538
+ if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
539
+ if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
540
+ uptimeStr += `${uptimeSecs}s`;
541
+
542
+ // Memory usage
543
+ const memUsage = process.memoryUsage();
544
+ const heapUsedPCT = (memUsage.heapUsed * 100) / memUsage.heapTotal;
545
+ const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
546
+ const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
547
+ const usedMemPCT = 100 - ((freeMemMB * 100) / totalMemMB);
548
+ const fstats = fs.statfsSync(folders.logsDir());
549
+ const diskPCT = (fstats.bavail * 100) / fstats.blocks;
550
+
551
+ let content = '';
552
+ content += '<table border="1">';
553
+ content += '<tr>';
554
+ content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
555
+ content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
556
+ content += `<td style="background-color:${pctColor(usedMemPCT)}"><strong>Memory:</strong> ${usedMemPCT.toFixed(0)}%</td>`;
557
+ content += `<td style="background-color:${pctColor(heapUsedPCT)}"><strong>Heap:</strong> ${heapUsedPCT.toFixed(0)}%</td>`;
558
+ content += `<td style="background-color:${pctColor(diskPCT)}"><strong>Disk:</strong> ${diskPCT.toFixed(0)}%</td>`;
559
+ content += '</tr>';
560
+ content += '</table>';
561
+
562
+ // ===== Metrics Graphs =====
563
+ const liquid = new Liquid({
564
+ root: path.join(__dirname, 'tx', 'html'),
565
+ extname: '.liquid'
566
+ });
567
+ content += await liquid.renderFile('dash-metrics', {
568
+ historyJson: JSON.stringify(stats.history),
569
+ startTime: stats.startTime
570
+ });
571
+ content += stats.taskDetails();
572
+ content += `<p>Data: ${folders.dataDir()}</p>`;
573
+
574
+ content = '<div class="row mb-4"><div class="col-12">' + content + '</div></div>';
551
575
 
552
576
  const pageStats = {
553
577
  version: packageJson.version,
@@ -565,6 +589,12 @@ app.get('/dashboard', async (req, res) => {
565
589
  }
566
590
  });
567
591
 
592
+ function pctColor(pct) {
593
+ const r = Math.round(pct * 2.55);
594
+ const g = Math.round((100 - pct) * 2.55);
595
+ return `rgb(${r}, ${g}, 100)`; // the 100 keeps it pastel/light
596
+ }
597
+
568
598
  // Health check endpoint
569
599
  app.get('/health', async (req, res) => {
570
600
  const healthStatus = {
@@ -611,10 +641,10 @@ function getLogStats() {
611
641
  let diskInfo = '';
612
642
  try {
613
643
  // statfs available in Node 18.15+
614
- const stats = fs.statfsSync(logDir);
615
- const blockSize = stats.bsize;
616
- const freeSpace = stats.bavail * blockSize;
617
- const totalSpace = stats.blocks * blockSize;
644
+ const fstats = fs.statfsSync(logDir);
645
+ const blockSize = fstats.bsize;
646
+ const freeSpace = fstats.bavail * blockSize;
647
+ const totalSpace = fstats.blocks * blockSize;
618
648
  const freeGB = (freeSpace / 1024 / 1024 / 1024).toFixed(2);
619
649
  const totalGB = (totalSpace / 1024 / 1024 / 1024).toFixed(2);
620
650
  diskInfo = `<td><strong>Disk Space:</strong> ${freeGB} GB of ${totalGB} GB</td>`;
package/stats.js CHANGED
@@ -84,6 +84,7 @@ class ServerStats {
84
84
  this.taskMap.set(name, info);
85
85
  info.frequency = frequency;
86
86
  info.state = "Started";
87
+ info.status = "started"
87
88
  }
88
89
 
89
90
  task(name, state) {
@@ -91,6 +92,25 @@ class ServerStats {
91
92
  if (info) {
92
93
  info.date = Date.now();
93
94
  info.state = state;
95
+ info.status = 'working';
96
+ }
97
+ }
98
+
99
+ taskDone(name, state) {
100
+ let info = this.taskMap.get(name);
101
+ if (info) {
102
+ info.date = Date.now();
103
+ info.state = state;
104
+ info.status = 'resting';
105
+ }
106
+ }
107
+
108
+ taskError(name, state) {
109
+ let info = this.taskMap.get(name);
110
+ if (info) {
111
+ info.date = Date.now();
112
+ info.state = state;
113
+ info.status = 'error';
94
114
  }
95
115
  }
96
116
 
@@ -98,17 +118,19 @@ class ServerStats {
98
118
  if (this.taskMap.size == 0) {
99
119
  return "";
100
120
  }
101
- let html = '<table class="grid"><tr style="background-color: #EEEEEE"><th colspan="4">Background Tasks</th></tr>';
102
- html += "<tr><th>Task</th><th>Status</th><th>Frequency</th><th>Last Seen</th></tr>";
121
+ let html = '<table class="grid">';
122
+ html += "<tr><th>Background Task</th><th>Status</th><th>Frequency</th><th>Last Seen</th></tr>";
103
123
  for (let m of this.taskMap.keys()) {
104
- html += "<tr><td>";
124
+ let mm = this.taskMap.get(m);
125
+ let color = this.getTaskColor(mm.status);
126
+ html += `<tr style="background-color: ${color}"><td>`;
105
127
  html += escape(m);
106
128
  html += "</td><td>";
107
- html += escape(this.taskMap.get(m).state);
129
+ html += escape(mm.state);
108
130
  html += "</td><td>";
109
- html += this.taskMap.get(m).frequency;
131
+ html += mm.frequency;
110
132
  html += "</td><td>";
111
- html += Utilities.formatDuration(this.taskMap.get(m).date, Date.now());
133
+ html += Utilities.formatDuration(mm.date, Date.now());
112
134
  html += "</td></tr>";
113
135
  }
114
136
  html += "</table>";
@@ -130,5 +152,14 @@ class ServerStats {
130
152
  return { idle, total };
131
153
  }
132
154
 
155
+ getTaskColor(status) {
156
+ switch (status) {
157
+ case "started": return "LightGrey";
158
+ case "working": return "LightGreen";
159
+ case "resting": return "White";
160
+ case "error": return "LightRed";
161
+ default: return "DarkBlue"; // should not happen
162
+ }
163
+ }
133
164
  }
134
165
  module.exports = ServerStats;
package/tx/cs/cs-loinc.js CHANGED
@@ -5,6 +5,7 @@ const { Language, Languages} = require('../../library/languages');
5
5
  const { CodeSystemFactoryProvider} = require('./cs-api');
6
6
  const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
7
7
  const {BaseCSServices} = require("./cs-base");
8
+ const {sqlEscapeString} = require("../../xig/xig");
8
9
 
9
10
  // Context kinds matching Pascal enum
10
11
  const LoincProviderContextKind = {
@@ -657,13 +658,17 @@ class LoincServices extends BaseCSServices {
657
658
  }
658
659
 
659
660
  async filter(filterContext, prop, op, value) {
660
-
661
-
662
661
  const filter = new LoincFilterHolder();
663
662
  await this.#executeFilterQuery(prop, op, value, filter);
664
663
  filterContext.filters.push(filter);
665
664
  }
666
665
 
666
+ async searchFilter(filterContext, filterText, sort) {
667
+ const filter = new LoincFilterHolder();
668
+ await this.#executeFilterQuery('$text', (sort ? '>' : '<'), filterText.filter, filter);
669
+ filterContext.filters.push(filter);
670
+ }
671
+
667
672
  async #executeFilterQuery(prop, op, value, filter) {
668
673
  let sql = '';
669
674
  let lsql = '';
@@ -913,6 +918,13 @@ class LoincServices extends BaseCSServices {
913
918
  WHERE CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
914
919
  AND CodeKey = `;
915
920
  }
921
+ } else if (prop === '$text' && (op === '>' || op === '<')) {
922
+ sql = `SELECT CodeKey as Key FROM Codes
923
+ WHERE Description like '%${sqlEscapeString(value)}%'
924
+ ORDER BY Description `+(op === '<' ? 'ASC' : 'DESC');
925
+ lsql = `SELECT COUNT(CodeKey) as Key FROM Codes
926
+ WHERE Codes.Description like '%${sqlEscapeString(value)}%'
927
+ AND TargetKey = `;
916
928
  }
917
929
 
918
930
  if (sql) {
@@ -331,20 +331,20 @@ class RxNormServices extends CodeSystemProvider {
331
331
  async doesFilter(prop, op, value) {
332
332
 
333
333
 
334
- prop = prop.toUpperCase();
334
+ let propUC = prop.toUpperCase();
335
335
 
336
336
  // TTY filters
337
- if (prop === 'TTY' && ['=', 'in'].includes(op)) {
337
+ if (propUC === 'TTY' && ['=', 'in'].includes(op)) {
338
338
  return true;
339
339
  }
340
340
 
341
341
  // STY filter
342
- if (prop === 'STY' && op === '=') {
342
+ if (propUC === 'STY' && op === '=') {
343
343
  return true;
344
344
  }
345
345
 
346
346
  // SAB filter
347
- if (prop === 'SAB' && op === '=') {
347
+ if (propUC === 'SAB' && op === '=') {
348
348
  return true;
349
349
  }
350
350
 
@@ -370,12 +370,12 @@ class RxNormServices extends CodeSystemProvider {
370
370
 
371
371
 
372
372
  const filter = new RxNormFilterHolder();
373
- prop = prop.toUpperCase();
373
+ let propUC = prop.toUpperCase();
374
374
 
375
375
  let sql = '';
376
376
  let params = {};
377
377
 
378
- if (op === 'in' && prop === 'TTY') {
378
+ if (op === 'in' && propUC === 'TTY') {
379
379
  const values = value.split(',').map(v => v.trim()).filter(v => v);
380
380
  const placeholders = values.map((_, i) => `$tty${i}`).join(',');
381
381
  sql = `AND TTY IN (${placeholders})`;
@@ -383,13 +383,13 @@ class RxNormServices extends CodeSystemProvider {
383
383
  params[`tty${i}`] = this.#sqlWrapString(val);
384
384
  });
385
385
  } else if (op === '=') {
386
- if (prop === 'STY') {
386
+ if (propUC === 'STY') {
387
387
  sql = `AND ${this.getCodeField()} IN (SELECT RXCUI FROM rxnsty WHERE TUI = $sty)`;
388
388
  params.sty = this.#sqlWrapString(value);
389
- } else if (prop === 'SAB') {
389
+ } else if (propUC === 'SAB') {
390
390
  sql = `AND ${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE SAB = $sab)`;
391
391
  params.sab = this.#sqlWrapString(value);
392
- } else if (prop === 'TTY') {
392
+ } else if (propUC === 'TTY') {
393
393
  sql = `AND TTY = $tty`;
394
394
  params.tty = this.#sqlWrapString(value);
395
395
  } else if (this.rels.includes(prop)) {
@@ -429,7 +429,6 @@ class RxNormServices extends CodeSystemProvider {
429
429
  }
430
430
 
431
431
  async searchFilter(filterContext, filter, sort) {
432
-
433
432
 
434
433
  if (!filter || !filter.stems || filter.stems.length === 0) {
435
434
  throw new Error('Invalid search filter');
@@ -709,6 +708,11 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
709
708
  const db = new sqlite3.Database(this.dbPath);
710
709
 
711
710
  try {
711
+ await new Promise((resolve, reject) => {
712
+ db.run(`CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)`,
713
+ err => err ? reject(err) : resolve());
714
+ });
715
+
712
716
  this._sharedData = {
713
717
  version: '',
714
718
  rels: [],