fhirsmith 0.18.2 → 1.0.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 (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. package/tx/html/home-metrics.liquid +0 -225
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const sqlite3 = require('sqlite3').verbose();
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
+ const terminal = require('./terminal');
6
7
 
7
8
  class NdcModule extends BaseTerminologyModule {
8
9
  getName() {
@@ -94,18 +95,18 @@ class NdcModule extends BaseTerminologyModule {
94
95
  const inquirer = require('inquirer');
95
96
  const chalk = require('chalk');
96
97
 
97
- console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
98
- console.log(` Source: ${chalk.white(config.source)}`);
99
- console.log(` Destination: ${chalk.white(config.dest)}`);
100
- console.log(` Version: ${chalk.white(config.version)}`);
101
- console.log(` Products Only: ${chalk.white(config.productsOnly ? 'Yes' : 'No')}`);
102
- console.log(` Packages Only: ${chalk.white(config.packagesOnly ? 'Yes' : 'No')}`);
103
- console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
104
- console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
105
- console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
98
+ terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
99
+ terminal.log(` Source: ${chalk.white(config.source)}`);
100
+ terminal.log(` Destination: ${chalk.white(config.dest)}`);
101
+ terminal.log(` Version: ${chalk.white(config.version)}`);
102
+ terminal.log(` Products Only: ${chalk.white(config.productsOnly ? 'Yes' : 'No')}`);
103
+ terminal.log(` Packages Only: ${chalk.white(config.packagesOnly ? 'Yes' : 'No')}`);
104
+ terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
105
+ terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
106
+ terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
106
107
 
107
108
  if (config.estimatedDuration) {
108
- console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
109
+ terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
109
110
  }
110
111
 
111
112
  const { confirmed } = await inquirer.prompt({
@@ -135,14 +136,14 @@ class NdcModule extends BaseTerminologyModule {
135
136
  const stats = await this.validateNdcDirectory(options.source, parseInt(options.sample));
136
137
 
137
138
  this.logSuccess('Directory validation passed');
138
- console.log(` Versions found: ${stats.versions.length}`);
139
- console.log(` Total products estimated: ${stats.totalProducts.toLocaleString()}`);
140
- console.log(` Total packages estimated: ${stats.totalPackages.toLocaleString()}`);
141
- console.log(` Versions: ${stats.versions.join(', ')}`);
139
+ terminal.log(` Versions found: ${stats.versions.length}`);
140
+ terminal.log(` Total products estimated: ${stats.totalProducts.toLocaleString()}`);
141
+ terminal.log(` Total packages estimated: ${stats.totalPackages.toLocaleString()}`);
142
+ terminal.log(` Versions: ${stats.versions.join(', ')}`);
142
143
 
143
144
  if (stats.warnings.length > 0) {
144
145
  this.logWarning('Validation warnings:');
145
- stats.warnings.forEach(warning => console.log(` ${warning}`));
146
+ stats.warnings.forEach(warning => terminal.log(` ${warning}`));
146
147
  }
147
148
 
148
149
  } catch (error) {
@@ -166,7 +167,7 @@ class NdcModule extends BaseTerminologyModule {
166
167
 
167
168
  this.logSuccess(`Found ${versions.length} NDC versions:`);
168
169
  versions.forEach((version, index) => {
169
- console.log(` ${index + 1}. ${version}`);
170
+ terminal.log(` ${index + 1}. ${version}`);
170
171
  });
171
172
 
172
173
  } catch (error) {
@@ -188,13 +189,13 @@ class NdcModule extends BaseTerminologyModule {
188
189
  const stats = await this.getDatabaseStats(dbPath);
189
190
 
190
191
  this.logSuccess('Database status:');
191
- console.log(` Versions: ${stats.versions.join(', ')}`);
192
- console.log(` Products: ${stats.productCount.toLocaleString()}`);
193
- console.log(` Packages: ${stats.packageCount.toLocaleString()}`);
194
- console.log(` Organizations: ${stats.orgCount.toLocaleString()}`);
195
- console.log(` Product Types: ${stats.typeCount.toLocaleString()}`);
196
- console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
197
- console.log(` Last Modified: ${stats.lastModified}`);
192
+ terminal.log(` Versions: ${stats.versions.join(', ')}`);
193
+ terminal.log(` Products: ${stats.productCount.toLocaleString()}`);
194
+ terminal.log(` Packages: ${stats.packageCount.toLocaleString()}`);
195
+ terminal.log(` Organizations: ${stats.orgCount.toLocaleString()}`);
196
+ terminal.log(` Product Types: ${stats.typeCount.toLocaleString()}`);
197
+ terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
198
+ terminal.log(` Last Modified: ${stats.lastModified}`);
198
199
 
199
200
  } catch (error) {
200
201
  this.logError(`Status check failed: ${error.message}`);
@@ -427,7 +428,7 @@ class NdcModule extends BaseTerminologyModule {
427
428
  db.serialize(() => {
428
429
  indexes.forEach(sql => {
429
430
  db.run(sql, (err) => {
430
- if (err) console.warn(`Index creation warning: ${err.message}`);
431
+ if (err) terminal.warn(`Index creation warning: ${err.message}`);
431
432
  });
432
433
  });
433
434
  });
@@ -447,7 +448,7 @@ class NdcDataMigrator {
447
448
  }
448
449
 
449
450
  async migrate(sourceDir, destFile = 'unknown', options = {}) {
450
- if (options.verbose) console.log('Starting NDC data migration...');
451
+ if (options.verbose) terminal.log('Starting NDC data migration...');
451
452
 
452
453
  // Ensure destination directory exists
453
454
  const destDir = path.dirname(destFile);
@@ -471,12 +472,12 @@ class NdcDataMigrator {
471
472
  const versions = await this.#findVersions(sourceDir);
472
473
 
473
474
  if (options.verbose) {
474
- console.log(`Found ${versions.length} versions: ${versions.join(', ')}`);
475
+ terminal.log(`Found ${versions.length} versions: ${versions.join(', ')}`);
475
476
  }
476
477
 
477
478
  // Process each version
478
479
  for (const versionName of versions) {
479
- if (options.verbose) console.log(`Processing version: ${versionName}`);
480
+ if (options.verbose) terminal.log(`Processing version: ${versionName}`);
480
481
 
481
482
  await this.#processVersion(db, sourceDir, versionName, options);
482
483
  }
@@ -489,7 +490,7 @@ class NdcDataMigrator {
489
490
  // Create lookup tables
490
491
  await this.#createLookupTables(db, options.verbose);
491
492
 
492
- if (options.verbose) console.log('NDC data migration completed successfully');
493
+ if (options.verbose) terminal.log('NDC data migration completed successfully');
493
494
  } finally {
494
495
  await this.#closeDatabase(db, options.verbose);
495
496
  }
@@ -511,7 +512,7 @@ class NdcDataMigrator {
511
512
  }
512
513
 
513
514
  async #createTables(db, verbose = true) {
514
- if (verbose) console.log('Creating database tables...');
515
+ if (verbose) terminal.log('Creating database tables...');
515
516
 
516
517
  const tableSQL = [
517
518
  `CREATE TABLE NDCProducts (
@@ -574,7 +575,7 @@ class NdcDataMigrator {
574
575
  });
575
576
  });
576
577
 
577
- if (verbose) console.log('Database tables created');
578
+ if (verbose) terminal.log('Database tables created');
578
579
  resolve();
579
580
  });
580
581
  });
@@ -599,11 +600,11 @@ class NdcDataMigrator {
599
600
  const productFile = path.join(versionDir, 'product.txt');
600
601
 
601
602
  if (!fs.existsSync(productFile)) {
602
- if (options.verbose) console.warn(`Product file not found: ${productFile}`);
603
+ if (options.verbose) terminal.warn(`Product file not found: ${productFile}`);
603
604
  return;
604
605
  }
605
606
 
606
- if (options.verbose) console.log(`Processing products from ${versionName}...`);
607
+ if (options.verbose) terminal.log(`Processing products from ${versionName}...`);
607
608
 
608
609
  const rl = readline.createInterface({
609
610
  input: fs.createReadStream(productFile),
@@ -668,7 +669,7 @@ class NdcDataMigrator {
668
669
  batch = [];
669
670
 
670
671
  if (processedCount % 10000 === 0 && options.verbose) {
671
- console.log(` Processed ${processedCount} products...`);
672
+ terminal.log(` Processed ${processedCount} products...`);
672
673
  }
673
674
  }
674
675
  }
@@ -690,7 +691,7 @@ class NdcDataMigrator {
690
691
  this.codesMap = codesMap;
691
692
 
692
693
  if (options.verbose) {
693
- console.log(` Completed products: ${processedCount} records`);
694
+ terminal.log(` Completed products: ${processedCount} records`);
694
695
  }
695
696
  }
696
697
 
@@ -698,11 +699,11 @@ class NdcDataMigrator {
698
699
  const packageFile = path.join(versionDir, 'package.txt');
699
700
 
700
701
  if (!fs.existsSync(packageFile)) {
701
- if (options.verbose) console.warn(`Package file not found: ${packageFile}`);
702
+ if (options.verbose) terminal.warn(`Package file not found: ${packageFile}`);
702
703
  return;
703
704
  }
704
705
 
705
- if (options.verbose) console.log(`Processing packages from ${versionName}...`);
706
+ if (options.verbose) terminal.log(`Processing packages from ${versionName}...`);
706
707
 
707
708
  const rl = readline.createInterface({
708
709
  input: fs.createReadStream(packageFile),
@@ -763,7 +764,7 @@ class NdcDataMigrator {
763
764
  batch = [];
764
765
 
765
766
  if (processedCount % 10000 === 0 && options.verbose) {
766
- console.log(` Processed ${processedCount} packages...`);
767
+ terminal.log(` Processed ${processedCount} packages...`);
767
768
  }
768
769
  }
769
770
  }
@@ -778,7 +779,7 @@ class NdcDataMigrator {
778
779
  insertPackage.finalize();
779
780
 
780
781
  if (options.verbose) {
781
- console.log(` Completed packages: ${processedCount} records`);
782
+ terminal.log(` Completed packages: ${processedCount} records`);
782
783
  }
783
784
  }
784
785
 
@@ -968,7 +969,7 @@ class NdcDataMigrator {
968
969
  }
969
970
 
970
971
  async #createLookupTables(db, verbose = true) {
971
- if (verbose) console.log('Creating lookup tables...');
972
+ if (verbose) terminal.log('Creating lookup tables...');
972
973
 
973
974
  const lookups = [
974
975
  { table: 'NDCProductTypes', map: this.typesMap },
@@ -977,8 +978,12 @@ class NdcDataMigrator {
977
978
  { table: 'NDCRoutes', map: this.routesMap }
978
979
  ];
979
980
 
980
- return new Promise((resolve) => {
981
+ // One transaction for all of them: otherwise each row commits (and waits on a disk sync)
982
+ // on its own, and there are thousands of organisations. A row that fails is warned about
983
+ // and skipped, as before: in SQLite a failed INSERT doesn't end the transaction
984
+ return new Promise((resolve, reject) => {
981
985
  db.serialize(() => {
986
+ db.run('BEGIN TRANSACTION');
982
987
  lookups.forEach(lookup => {
983
988
  if (lookup.map) {
984
989
  for (const [name, id] of lookup.map) {
@@ -987,7 +992,7 @@ class NdcDataMigrator {
987
992
  [id, name.substring(0, 500)],
988
993
  (err) => {
989
994
  if (err && verbose) {
990
- console.warn(`Warning inserting into ${lookup.table}: ${err.message}`);
995
+ terminal.warn(`Warning inserting into ${lookup.table}: ${err.message}`);
991
996
  }
992
997
  }
993
998
  );
@@ -995,8 +1000,15 @@ class NdcDataMigrator {
995
1000
  }
996
1001
  });
997
1002
 
998
- if (verbose) console.log('Lookup tables created');
999
- resolve();
1003
+ // resolves once the rows are in, not when they have only been queued
1004
+ db.run('COMMIT', (err) => {
1005
+ if (err) {
1006
+ reject(err);
1007
+ } else {
1008
+ if (verbose) terminal.log('Lookup tables created');
1009
+ resolve();
1010
+ }
1011
+ });
1000
1012
  });
1001
1013
  });
1002
1014
  }
@@ -1005,7 +1017,7 @@ class NdcDataMigrator {
1005
1017
  return new Promise((resolve) => {
1006
1018
  db.close((err) => {
1007
1019
  if (err && verbose) {
1008
- console.error('Error closing database:', err);
1020
+ terminal.error('Error closing database:', err);
1009
1021
  }
1010
1022
  resolve();
1011
1023
  });
@@ -4,6 +4,7 @@ const sqlite3 = require('sqlite3').verbose();
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
6
  const chalk = require('chalk');
7
+ const terminal = require('./terminal');
7
8
 
8
9
  class RxNormModule extends BaseTerminologyModule {
9
10
  constructor() {
@@ -98,7 +99,7 @@ class RxNormModule extends BaseTerminologyModule {
98
99
  } catch (error) {
99
100
  this.logError(`Import command failed: ${error.message}`);
100
101
  if (options.verbose) {
101
- console.error(error.stack);
102
+ terminal.error(error.stack);
102
103
  }
103
104
  throw error;
104
105
  }
@@ -121,17 +122,17 @@ class RxNormModule extends BaseTerminologyModule {
121
122
  async confirmImport(config) {
122
123
  const inquirer = require('inquirer');
123
124
 
124
- console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
125
- console.log(` Source: ${chalk.white(config.source)}`);
126
- console.log(` Destination: ${chalk.white(config.dest)}`);
127
- console.log(` Version: ${chalk.white(config.version || 'Auto-detect')}`);
128
- console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
129
- console.log(` Create Stems: ${chalk.white(config.createStems ? 'Yes' : 'No')}`);
130
- console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
131
- console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
125
+ terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
126
+ terminal.log(` Source: ${chalk.white(config.source)}`);
127
+ terminal.log(` Destination: ${chalk.white(config.dest)}`);
128
+ terminal.log(` Version: ${chalk.white(config.version || 'Auto-detect')}`);
129
+ terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
130
+ terminal.log(` Create Stems: ${chalk.white(config.createStems ? 'Yes' : 'No')}`);
131
+ terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
132
+ terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
132
133
 
133
134
  if (config.estimatedDuration) {
134
- console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
135
+ terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
135
136
  }
136
137
 
137
138
  const { confirmed } = await inquirer.prompt({
@@ -161,14 +162,14 @@ class RxNormModule extends BaseTerminologyModule {
161
162
  const stats = await this.validateRxNormDirectory(options.source);
162
163
 
163
164
  this.logSuccess('Directory validation passed');
164
- console.log(` Required files found: ${stats.requiredFiles.length}/3`);
165
- console.log(` Optional files found: ${stats.optionalFiles.length}/3`);
166
- console.log(` Estimated concepts: ${stats.estimatedConcepts.toLocaleString()}`);
167
- console.log(` Estimated relationships: ${stats.estimatedRelationships.toLocaleString()}`);
165
+ terminal.log(` Required files found: ${stats.requiredFiles.length}/3`);
166
+ terminal.log(` Optional files found: ${stats.optionalFiles.length}/3`);
167
+ terminal.log(` Estimated concepts: ${stats.estimatedConcepts.toLocaleString()}`);
168
+ terminal.log(` Estimated relationships: ${stats.estimatedRelationships.toLocaleString()}`);
168
169
 
169
170
  if (stats.warnings.length > 0) {
170
171
  this.logWarning('Validation warnings:');
171
- stats.warnings.forEach(warning => console.log(` ${warning}`));
172
+ stats.warnings.forEach(warning => terminal.log(` ${warning}`));
172
173
  }
173
174
 
174
175
  } catch (error) {
@@ -190,13 +191,13 @@ class RxNormModule extends BaseTerminologyModule {
190
191
  const stats = await this.getDatabaseStats(dbPath);
191
192
 
192
193
  this.logSuccess('Database status:');
193
- console.log(` Version: ${stats.version}`);
194
- console.log(` RXNCONSO Records: ${stats.rxnconsoCount.toLocaleString()}`);
195
- console.log(` RXNREL Records: ${stats.rxnrelCount.toLocaleString()}`);
196
- console.log(` RXNSTY Records: ${stats.rxnstyCount.toLocaleString()}`);
197
- console.log(` Stem Records: ${stats.stemCount.toLocaleString()}`);
198
- console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
199
- console.log(` Last Modified: ${stats.lastModified}`);
194
+ terminal.log(` Version: ${stats.version}`);
195
+ terminal.log(` RXNCONSO Records: ${stats.rxnconsoCount.toLocaleString()}`);
196
+ terminal.log(` RXNREL Records: ${stats.rxnrelCount.toLocaleString()}`);
197
+ terminal.log(` RXNSTY Records: ${stats.rxnstyCount.toLocaleString()}`);
198
+ terminal.log(` Stem Records: ${stats.stemCount.toLocaleString()}`);
199
+ terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
200
+ terminal.log(` Last Modified: ${stats.lastModified}`);
200
201
 
201
202
  } catch (error) {
202
203
  this.logError(`Status check failed: ${error.message}`);
@@ -428,15 +429,20 @@ class RxNormModule extends BaseTerminologyModule {
428
429
  'CREATE INDEX IF NOT EXISTS idx_rxnrel_rel ON RXNREL(REL)',
429
430
  'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela ON RXNREL(RELA)',
430
431
  'CREATE INDEX IF NOT EXISTS X_RXNSTY_2 ON RXNSTY(TUI)',
432
+ 'CREATE INDEX IF NOT EXISTS idx_rxnsty_rxcui ON RXNSTY(RXCUI)',
433
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rxcui2 ON RXNREL(RXCUI2, SAB)',
434
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela_rxcui1 ON RXNREL(RELA, RXCUI1)',
435
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela_rxaui1 ON RXNREL(RELA, RXAUI1)',
431
436
  'CREATE INDEX IF NOT EXISTS idx_rxnstems_stem_cui ON RXNSTEMS(stem, CUI)',
432
- 'CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)'
437
+ 'CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)',
438
+ 'CREATE INDEX IF NOT EXISTS idx_rxnatomarchive_rxcui_sab ON RXNATOMARCHIVE(RXCUI, SAB)'
433
439
  ];
434
440
 
435
441
  return new Promise((resolve, reject) => {
436
442
  db.serialize(() => {
437
443
  indexes.forEach(sql => {
438
444
  db.run(sql, (err) => {
439
- if (err) console.warn(`Index creation warning: ${err.message}`);
445
+ if (err) terminal.warn(`Index creation warning: ${err.message}`);
440
446
  });
441
447
  });
442
448
  });
@@ -472,7 +478,7 @@ class RxNormImporter {
472
478
  }
473
479
 
474
480
  async import() {
475
- if (this.options.verbose) console.log('Starting RxNorm import...');
481
+ if (this.options.verbose) terminal.log('Starting RxNorm import...');
476
482
 
477
483
  // Ensure destination directory exists
478
484
  const destDir = path.dirname(this.destFile);
@@ -501,7 +507,7 @@ class RxNormImporter {
501
507
  await this.makeStems(db);
502
508
  }
503
509
 
504
- if (this.options.verbose) console.log('RxNorm import completed successfully');
510
+ if (this.options.verbose) terminal.log('RxNorm import completed successfully');
505
511
  } finally {
506
512
  await this.closeDatabase(db);
507
513
  }
@@ -634,12 +640,12 @@ class RxNormImporter {
634
640
  tableSQL.forEach(sql => {
635
641
  db.run(sql, (err) => {
636
642
  if (err && this.options.verbose) {
637
- console.warn(`Table creation warning: ${err.message}`);
643
+ terminal.warn(`Table creation warning: ${err.message}`);
638
644
  }
639
645
  });
640
646
  });
641
647
 
642
- if (this.options.verbose) console.log('Database tables created');
648
+ if (this.options.verbose) terminal.log('Database tables created');
643
649
  resolve();
644
650
  });
645
651
  });
@@ -701,83 +707,116 @@ class RxNormImporter {
701
707
  async loadRRFFile(db, fileName, insertSQL, extractValues) {
702
708
  const filePath = path.join(this.sourceDir, fileName);
703
709
  if (!fs.existsSync(filePath)) {
704
- if (this.options.verbose) console.warn(`File not found: ${fileName}`);
710
+ if (this.options.verbose) terminal.warn(`File not found: ${fileName}`);
705
711
  return;
706
712
  }
707
713
 
708
714
  this.updateProgress(0, `Loading ${fileName}`);
709
715
 
716
+ // Rows go in as multi-row INSERTs, as many rows per statement as fit in SQLite's 999
717
+ // parameters: one statement (and one callback) per row made the import slow, and slower
718
+ // still when the process is busy collecting a large heap - the test suite runs it in a
719
+ // jest process already holding GBs. OR IGNORE keeps the old per-row behaviour, where a row
720
+ // that broke a constraint was dropped and the rest of the file went in
721
+ const m = insertSQL.match(/^\s*INSERT\s+(?:OR\s+IGNORE\s+)?INTO([\s\S]*?VALUES\s*)(\([^)]*\))\s*$/i);
722
+ if (!m) {
723
+ throw new Error(`Unexpected insert statement for ${fileName}: ${insertSQL}`);
724
+ }
725
+ const head = 'INSERT OR IGNORE INTO' + m[1];
726
+ const group = m[2];
727
+ const cols = (group.match(/\?/g) || []).length;
728
+ const batchRows = Math.max(1, Math.floor(999 / cols));
729
+ const sqlFor = (n) => head + new Array(n).fill(group).join(', ');
730
+ const fullSql = sqlFor(batchRows);
731
+
710
732
  const rl = readline.createInterface({
711
733
  input: fs.createReadStream(filePath),
712
734
  crlfDelay: Infinity
713
735
  });
714
736
 
715
737
  return new Promise((resolve, reject) => {
716
- db.serialize(() => {
717
- db.run('BEGIN TRANSACTION');
718
-
719
- const stmt = db.prepare(insertSQL);
720
- let lineCount = 0;
721
- let processedCount = 0;
722
- let errorCount = 0;
723
-
724
- rl.on('line', (line) => {
725
- lineCount++;
726
-
727
- const items = line.split('|');
728
- if (items.length === 0) return;
729
-
730
- try {
731
- const values = extractValues(items);
732
- stmt.run(values, (err) => {
733
- if (err && err.code !== 'SQLITE_CONSTRAINT') {
734
- errorCount++;
735
- if (this.options.verbose && errorCount <= 10) {
736
- console.warn(`Error processing line ${lineCount} in ${fileName}: ${err.message}`);
737
- }
738
- }
739
- });
740
- processedCount++;
738
+ // serialized: BEGIN, every batch, and COMMIT run in the order they are queued
739
+ db.serialize();
740
+ db.run('BEGIN TRANSACTION');
741
741
 
742
- if (processedCount % 1000 === 0) {
743
- this.updateProgress(1000);
744
- }
745
- } catch (error) {
742
+ let lineCount = 0;
743
+ let processedCount = 0;
744
+ let errorCount = 0;
745
+ let pending = [];
746
+ let pendingRows = 0;
747
+
748
+ const flush = () => {
749
+ if (pendingRows === 0) {
750
+ return;
751
+ }
752
+ const rows = pendingRows;
753
+ db.run(rows === batchRows ? fullSql : sqlFor(rows), pending, (err) => {
754
+ if (err) {
746
755
  errorCount++;
747
756
  if (this.options.verbose && errorCount <= 10) {
748
- console.warn(`Error processing line ${lineCount} in ${fileName}: ${error.message}`);
757
+ terminal.warn(`Error loading ${rows} rows of ${fileName}: ${err.message}`);
749
758
  }
750
759
  }
751
760
  });
761
+ pending = [];
762
+ pendingRows = 0;
763
+ };
752
764
 
753
- rl.on('close', () => {
754
- stmt.finalize();
755
- db.run('COMMIT', (err) => {
756
- if (err) reject(err);
757
- else {
758
- // Update progress for remaining records
759
- const remaining = processedCount % 1000;
760
- if (remaining > 0) {
761
- this.updateProgress(remaining);
762
- }
765
+ rl.on('line', (line) => {
766
+ lineCount++;
763
767
 
764
- if (this.options.verbose) {
765
- console.log(` Loaded ${processedCount} records from ${fileName}${errorCount > 0 ? ` (${errorCount} errors)` : ''}`);
766
- }
767
- resolve();
768
+ const items = line.split('|');
769
+ if (items.length === 0) return;
770
+
771
+ try {
772
+ const values = extractValues(items);
773
+ for (let i = 0; i < cols; i++) {
774
+ pending.push(values[i] === undefined ? null : values[i]);
775
+ }
776
+ pendingRows++;
777
+ if (pendingRows === batchRows) {
778
+ flush();
779
+ }
780
+ processedCount++;
781
+
782
+ if (processedCount % 1000 === 0) {
783
+ this.updateProgress(1000);
784
+ }
785
+ } catch (error) {
786
+ errorCount++;
787
+ if (this.options.verbose && errorCount <= 10) {
788
+ terminal.warn(`Error processing line ${lineCount} in ${fileName}: ${error.message}`);
789
+ }
790
+ }
791
+ });
792
+
793
+ rl.on('close', () => {
794
+ flush();
795
+ db.run('COMMIT', (err) => {
796
+ if (err) reject(err);
797
+ else {
798
+ // Update progress for remaining records
799
+ const remaining = processedCount % 1000;
800
+ if (remaining > 0) {
801
+ this.updateProgress(remaining);
768
802
  }
769
- });
770
- });
771
803
 
772
- rl.on('error', reject);
804
+ if (this.options.verbose) {
805
+ terminal.log(` Loaded ${processedCount} records from ${fileName}${errorCount > 0 ? ` (${errorCount} errors)` : ''}`);
806
+ }
807
+ resolve();
808
+ }
809
+ });
773
810
  });
811
+
812
+ rl.on('error', reject);
774
813
  });
775
814
  }
776
815
 
777
816
  async makeStems(db) {
778
817
  this.updateProgress(0, 'Generating Stems');
779
818
 
780
- if (this.options.verbose) console.log('Generating word stems...');
819
+ if (this.options.verbose) terminal.log('Generating word stems...');
781
820
 
782
821
  // Simple English stemmer implementation
783
822
  var natural = require('natural/lib/natural/stemmers'); // just the stemmers: the package index also loads its storage backends, and dotenv with them
@@ -833,7 +872,7 @@ class RxNormImporter {
833
872
  }
834
873
 
835
874
  if (this.options.verbose) {
836
- console.log(` Generated ${totalInserts} stem entries from ${processedStems} unique stems`);
875
+ terminal.log(` Generated ${totalInserts} stem entries from ${processedStems} unique stems`);
837
876
  }
838
877
  resolve();
839
878
  }
@@ -857,7 +896,7 @@ class RxNormImporter {
857
896
  return new Promise((resolve) => {
858
897
  db.close((err) => {
859
898
  if (err && this.options.verbose) {
860
- console.error('Error closing database:', err);
899
+ terminal.error('Error closing database:', err);
861
900
  }
862
901
  resolve();
863
902
  });