fhirsmith 0.3.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 (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. package/xig/xig.js +3049 -0
@@ -0,0 +1,898 @@
1
+ const { BaseTerminologyModule } = require('./tx-import-base');
2
+ const fs = require('fs');
3
+ const sqlite3 = require('sqlite3').verbose();
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const chalk = require('chalk');
7
+
8
+ class RxNormModule extends BaseTerminologyModule {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ getName() {
14
+ return 'rxnorm';
15
+ }
16
+
17
+ getDescription() {
18
+ return 'RxNorm prescribable drug nomenclature from the National Library of Medicine';
19
+ }
20
+
21
+ getSupportedFormats() {
22
+ return ['rrf', 'directory'];
23
+ }
24
+
25
+ getDefaultConfig() {
26
+ return {
27
+ ...super.getDefaultConfig(),
28
+ createIndexes: true,
29
+ createStems: true,
30
+ dest: './data/rxnorm.db'
31
+ };
32
+ }
33
+
34
+ getEstimatedDuration() {
35
+ return '15-45 minutes (depending on stem generation)';
36
+ }
37
+
38
+ registerCommands(terminologyCommand, globalOptions) {
39
+ // Import command
40
+ terminologyCommand
41
+ .command('import')
42
+ .description('Import RxNorm data from source directory')
43
+ .option('-s, --source <directory>', 'Source directory containing RRF files')
44
+ .option('-d, --dest <file>', 'Destination SQLite database')
45
+ .option('-v, --version <version>', 'RxNorm version identifier')
46
+ .option('-y, --yes', 'Skip confirmations')
47
+ .option('--no-indexes', 'Skip index creation for faster import')
48
+ .option('--no-stems', 'Skip stem generation for faster import')
49
+ .action(async (options) => {
50
+ await this.handleImportCommand({...globalOptions, ...options});
51
+ });
52
+
53
+ // Validate command
54
+ terminologyCommand
55
+ .command('validate')
56
+ .description('Validate RxNorm source directory structure')
57
+ .option('-s, --source <directory>', 'Source directory to validate')
58
+ .action(async (options) => {
59
+ await this.handleValidateCommand({...globalOptions, ...options});
60
+ });
61
+
62
+ // Status command
63
+ terminologyCommand
64
+ .command('status')
65
+ .description('Show status of RxNorm database')
66
+ .option('-d, --dest <file>', 'Database file to check')
67
+ .action(async (options) => {
68
+ await this.handleStatusCommand({...globalOptions, ...options});
69
+ });
70
+ }
71
+
72
+ async handleImportCommand(options) {
73
+ try {
74
+ // Gather configuration with remembered values
75
+ const config = await this.gatherCommonConfig(options);
76
+
77
+ // RxNorm-specific configuration
78
+ config.createIndexes = !options.noIndexes;
79
+ config.createStems = !options.noStems;
80
+ config.estimatedDuration = this.getEstimatedDuration();
81
+
82
+ // Auto-detect version from path if not provided
83
+ if (!config.version) {
84
+ config.version = this.detectVersionFromPath(config.source);
85
+ }
86
+
87
+ // Show confirmation unless --yes is specified
88
+ if (!options.yes) {
89
+ const confirmed = await this.confirmImport(config);
90
+ if (!confirmed) {
91
+ this.logInfo('Import cancelled');
92
+ return;
93
+ }
94
+ }
95
+
96
+ // Run the import
97
+ await this.runImport(config);
98
+ } catch (error) {
99
+ this.logError(`Import command failed: ${error.message}`);
100
+ if (options.verbose) {
101
+ console.error(error.stack);
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ detectVersionFromPath(sourcePath) {
108
+ // Try to extract version from path like "RxNorm_full_08042025"
109
+ const pathMatch = sourcePath.match(/RxNorm_full_(\d{8})/);
110
+ if (pathMatch) {
111
+ const dateStr = pathMatch[1];
112
+ // Convert MMDDYYYY to YYYY-MM-DD format
113
+ const month = dateStr.substring(0, 2);
114
+ const day = dateStr.substring(2, 4);
115
+ const year = dateStr.substring(4, 8);
116
+ return `RXNORM-${year}-${month}-${day}`;
117
+ }
118
+ return 'RXNORM-UNKNOWN';
119
+ }
120
+
121
+ async confirmImport(config) {
122
+ const inquirer = require('inquirer');
123
+
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')}`);
132
+
133
+ if (config.estimatedDuration) {
134
+ console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
135
+ }
136
+
137
+ const { confirmed } = await inquirer.prompt({
138
+ type: 'confirm',
139
+ name: 'confirmed',
140
+ message: 'Proceed with import?',
141
+ default: true
142
+ });
143
+
144
+ return confirmed;
145
+ }
146
+
147
+ async handleValidateCommand(options) {
148
+ if (!options.source) {
149
+ const answers = await require('inquirer').prompt({
150
+ type: 'input',
151
+ name: 'source',
152
+ message: 'Source directory to validate:',
153
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
154
+ });
155
+ options.source = answers.source;
156
+ }
157
+
158
+ this.logInfo(`Validating RxNorm directory: ${options.source}`);
159
+
160
+ try {
161
+ const stats = await this.validateRxNormDirectory(options.source);
162
+
163
+ 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()}`);
168
+
169
+ if (stats.warnings.length > 0) {
170
+ this.logWarning('Validation warnings:');
171
+ stats.warnings.forEach(warning => console.log(` ${warning}`));
172
+ }
173
+
174
+ } catch (error) {
175
+ this.logError(`Validation failed: ${error.message}`);
176
+ }
177
+ }
178
+
179
+ async handleStatusCommand(options) {
180
+ const dbPath = options.dest || './data/rxnorm.db';
181
+
182
+ if (!fs.existsSync(dbPath)) {
183
+ this.logError(`Database not found: ${dbPath}`);
184
+ return;
185
+ }
186
+
187
+ this.logInfo(`Checking RxNorm database: ${dbPath}`);
188
+
189
+ try {
190
+ const stats = await this.getDatabaseStats(dbPath);
191
+
192
+ 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}`);
200
+
201
+ } catch (error) {
202
+ this.logError(`Status check failed: ${error.message}`);
203
+ }
204
+ }
205
+
206
+ async validatePrerequisites(config) {
207
+ const baseValid = await super.validatePrerequisites(config);
208
+
209
+ try {
210
+ this.logInfo('Validating RxNorm directory structure...');
211
+ await this.validateRxNormDirectory(config.source);
212
+ this.logSuccess('RxNorm directory structure valid');
213
+ } catch (error) {
214
+ this.logError(`RxNorm directory validation failed: ${error.message}`);
215
+ return false;
216
+ }
217
+
218
+ return baseValid;
219
+ }
220
+
221
+ async executeImport(config) {
222
+ this.logInfo('Starting RxNorm data import...');
223
+
224
+ const importer = new RxNormImporter(
225
+ config.source,
226
+ config.dest,
227
+ config.version,
228
+ {
229
+ verbose: config.verbose,
230
+ createStems: config.createStems,
231
+ progressCallback: (current, operation) => {
232
+ if (operation && this.progressBar) {
233
+ // Update operation display
234
+ this.progressBar.update(current, {
235
+ operation: chalk.cyan(operation.padEnd(20).substring(0, 20))
236
+ });
237
+ } else if (this.progressBar) {
238
+ this.progressBar.update(current);
239
+ }
240
+ }
241
+ }
242
+ );
243
+
244
+ // Estimate total work for progress bar
245
+ const totalWork = await this.estimateWorkload(config.source);
246
+
247
+ const progressFormat = '{operation} |{bar}| {percentage}% | {value}/{total} | ETA: {eta}s';
248
+ this.createProgressBar(progressFormat);
249
+ this.progressBar.start(totalWork, 0, {
250
+ operation: chalk.cyan('Starting'.padEnd(20).substring(0, 20))
251
+ });
252
+
253
+ try {
254
+ await importer.import();
255
+ } finally {
256
+ this.stopProgress();
257
+ }
258
+
259
+ if (config.createIndexes) {
260
+ this.logInfo('Creating database indexes...');
261
+ await this.createIndexes(config.dest);
262
+ this.logSuccess('Indexes created');
263
+ }
264
+ }
265
+
266
+ async validateRxNormDirectory(sourceDir) {
267
+ if (!fs.existsSync(sourceDir)) {
268
+ throw new Error(`Source directory not found: ${sourceDir}`);
269
+ }
270
+
271
+ const requiredFiles = [
272
+ 'RXNCONSO.RRF',
273
+ 'RXNREL.RRF',
274
+ 'RXNSTY.RRF'
275
+ ];
276
+
277
+ const optionalFiles = [
278
+ 'RXNSAB.RRF',
279
+ 'RXNATOMARCHIVE.RRF',
280
+ 'RXNCUI.RRF'
281
+ ];
282
+
283
+ const warnings = [];
284
+ let requiredFound = [];
285
+ let optionalFound = [];
286
+ let estimatedConcepts = 0;
287
+ let estimatedRelationships = 0;
288
+
289
+ // Check required files
290
+ for (const file of requiredFiles) {
291
+ const filePath = path.join(sourceDir, file);
292
+ if (!fs.existsSync(filePath)) {
293
+ throw new Error(`Required file missing: ${file}`);
294
+ }
295
+ requiredFound.push(file);
296
+
297
+ // Estimate record counts
298
+ if (file === 'RXNCONSO.RRF') {
299
+ estimatedConcepts = await this.countLines(filePath) - 1;
300
+ } else if (file === 'RXNREL.RRF') {
301
+ estimatedRelationships = await this.countLines(filePath) - 1;
302
+ }
303
+ }
304
+
305
+ // Check optional files
306
+ for (const file of optionalFiles) {
307
+ const filePath = path.join(sourceDir, file);
308
+ if (!fs.existsSync(filePath)) {
309
+ warnings.push(`Optional file missing: ${file}`);
310
+ } else {
311
+ optionalFound.push(file);
312
+ }
313
+ }
314
+
315
+ return {
316
+ requiredFiles: requiredFound,
317
+ optionalFiles: optionalFound,
318
+ estimatedConcepts,
319
+ estimatedRelationships,
320
+ warnings
321
+ };
322
+ }
323
+
324
+ async countLines(filePath) {
325
+ return new Promise((resolve, reject) => {
326
+ let lineCount = 0;
327
+ const rl = readline.createInterface({
328
+ input: fs.createReadStream(filePath),
329
+ crlfDelay: Infinity
330
+ });
331
+
332
+ rl.on('line', () => lineCount++);
333
+ rl.on('close', () => resolve(lineCount));
334
+ rl.on('error', reject);
335
+ });
336
+ }
337
+
338
+ async estimateWorkload(sourceDir) {
339
+ let totalWork = 0;
340
+ const files = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF', 'RXNSAB.RRF', 'RXNATOMARCHIVE.RRF', 'RXNCUI.RRF'];
341
+
342
+ for (const file of files) {
343
+ const filePath = path.join(sourceDir, file);
344
+ if (fs.existsSync(filePath)) {
345
+ const lines = await this.countLines(filePath);
346
+ totalWork += Math.max(lines - 1, 0);
347
+ }
348
+ }
349
+
350
+ // Add estimated work for stem generation (roughly equal to concept count)
351
+ const conceptsPath = path.join(sourceDir, 'RXNCONSO.RRF');
352
+ if (fs.existsSync(conceptsPath)) {
353
+ const concepts = await this.countLines(conceptsPath) - 1;
354
+ totalWork += concepts; // Stem generation work
355
+ }
356
+
357
+ return Math.max(totalWork, 1);
358
+ }
359
+
360
+ async getDatabaseStats(dbPath) {
361
+ const db = new sqlite3.Database(dbPath);
362
+
363
+ return new Promise((resolve, reject) => {
364
+ const stats = {};
365
+
366
+ // Try to get version from a version table or derive from path
367
+ db.get('SELECT name FROM sqlite_master WHERE type="table" AND name="RXNVer"', (err, row) => {
368
+ if (row) {
369
+ db.get('SELECT version FROM RXNVer LIMIT 1', (err, versionRow) => {
370
+ stats.version = versionRow ? versionRow.version : 'Unknown';
371
+ this.getTableCounts(db, stats, resolve, reject);
372
+ });
373
+ } else {
374
+ // Derive version from path or set as unknown
375
+ const pathVersion = path.basename(dbPath, '.db').match(/\d+$/);
376
+ stats.version = pathVersion ? pathVersion[0] : 'Unknown';
377
+ this.getTableCounts(db, stats, resolve, reject);
378
+ }
379
+ });
380
+ });
381
+ }
382
+
383
+ getTableCounts(db, stats, resolve) {
384
+ const queries = [
385
+ { name: 'rxnconsoCount', sql: 'SELECT COUNT(*) as count FROM RXNCONSO' },
386
+ { name: 'rxnrelCount', sql: 'SELECT COUNT(*) as count FROM RXNREL' },
387
+ { name: 'rxnstyCount', sql: 'SELECT COUNT(*) as count FROM RXNSTY' },
388
+ { name: 'stemCount', sql: 'SELECT COUNT(*) as count FROM RXNSTEMS' }
389
+ ];
390
+
391
+ let completed = 0;
392
+
393
+ queries.forEach(query => {
394
+ db.get(query.sql, (err, row) => {
395
+ if (err) {
396
+ stats[query.name] = 0;
397
+ } else {
398
+ stats[query.name] = row ? row.count : 0;
399
+ }
400
+ completed++;
401
+
402
+ if (completed === queries.length) {
403
+ const fileStat = fs.statSync(this.dbPath || './data/rxnorm.db');
404
+ stats.sizeGB = fileStat.size / (1024 * 1024 * 1024);
405
+ stats.lastModified = fileStat.mtime.toISOString();
406
+
407
+ db.close();
408
+ resolve(stats);
409
+ }
410
+ });
411
+ });
412
+ }
413
+
414
+ async createIndexes(dbPath) {
415
+ const db = new sqlite3.Database(dbPath);
416
+
417
+ const indexes = [
418
+ 'CREATE INDEX IF NOT EXISTS X_RXNCONSO_1 ON RXNCONSO(RXCUI)',
419
+ 'CREATE INDEX IF NOT EXISTS X_RXNCONSO_2 ON RXNCONSO(SAB, TTY)',
420
+ 'CREATE INDEX IF NOT EXISTS X_RXNCONSO_3 ON RXNCONSO(CODE, SAB, TTY)',
421
+ 'CREATE INDEX IF NOT EXISTS X_RXNCONSO_4 ON RXNCONSO(TTY, SAB)',
422
+ 'CREATE INDEX IF NOT EXISTS X_RXNCONSO_6 ON RXNCONSO(RXAUI)',
423
+ 'CREATE INDEX IF NOT EXISTS idx_rxnconso_sab_tty_rxcui ON RXNCONSO(SAB, TTY, RXCUI)',
424
+ 'CREATE INDEX IF NOT EXISTS X_RXNREL_2 ON RXNREL(REL, RXAUI1)',
425
+ 'CREATE INDEX IF NOT EXISTS X_RXNREL_3 ON RXNREL(REL, RXCUI1)',
426
+ 'CREATE INDEX IF NOT EXISTS X_RXNREL_4 ON RXNREL(RELA, RXAUI2)',
427
+ 'CREATE INDEX IF NOT EXISTS X_RXNREL_5 ON RXNREL(RELA, RXCUI2)',
428
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rel ON RXNREL(REL)',
429
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela ON RXNREL(RELA)',
430
+ 'CREATE INDEX IF NOT EXISTS X_RXNSTY_2 ON RXNSTY(TUI)',
431
+ 'CREATE INDEX IF NOT EXISTS idx_rxnstems_stem_cui ON RXNSTEMS(stem, CUI)'
432
+ ];
433
+
434
+ return new Promise((resolve, reject) => {
435
+ db.serialize(() => {
436
+ indexes.forEach(sql => {
437
+ db.run(sql, (err) => {
438
+ if (err) console.warn(`Index creation warning: ${err.message}`);
439
+ });
440
+ });
441
+ });
442
+
443
+ db.close((err) => {
444
+ if (err) reject(err);
445
+ else resolve();
446
+ });
447
+ });
448
+ }
449
+ }
450
+
451
+ // RxNorm data importer
452
+ class RxNormImporter {
453
+ constructor(sourceDir, destFile, version, options = {}) {
454
+ this.sourceDir = sourceDir;
455
+ this.destFile = destFile;
456
+ this.version = version;
457
+ this.options = {
458
+ verbose: true,
459
+ createStems: true,
460
+ progressCallback: null,
461
+ ...options
462
+ };
463
+ this.currentProgress = 0;
464
+ }
465
+
466
+ updateProgress(amount = 1, operation = null) {
467
+ this.currentProgress += amount;
468
+ if (this.options.progressCallback) {
469
+ this.options.progressCallback(this.currentProgress, operation);
470
+ }
471
+ }
472
+
473
+ async import() {
474
+ if (this.options.verbose) console.log('Starting RxNorm import...');
475
+
476
+ // Ensure destination directory exists
477
+ const destDir = path.dirname(this.destFile);
478
+ if (!fs.existsSync(destDir)) {
479
+ fs.mkdirSync(destDir, { recursive: true });
480
+ }
481
+
482
+ // Remove existing database
483
+ if (fs.existsSync(this.destFile)) {
484
+ fs.unlinkSync(this.destFile);
485
+ }
486
+
487
+ const db = new sqlite3.Database(this.destFile);
488
+
489
+ try {
490
+ await this.checkFiles();
491
+ await this.createTables(db);
492
+ await this.loadRXNSAB(db);
493
+ await this.loadRXNATOMARCHIVE(db);
494
+ await this.loadRXNCUI(db);
495
+ await this.loadRXNCONSO(db);
496
+ await this.loadRXNREL(db);
497
+ await this.loadRXNSTY(db);
498
+
499
+ if (this.options.createStems) {
500
+ await this.makeStems(db);
501
+ }
502
+
503
+ if (this.options.verbose) console.log('RxNorm import completed successfully');
504
+ } finally {
505
+ await this.closeDatabase(db);
506
+ }
507
+ }
508
+
509
+ async checkFiles() {
510
+ this.updateProgress(0, 'Checking Files');
511
+
512
+ const requiredFiles = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF'];
513
+
514
+ for (const file of requiredFiles) {
515
+ const filePath = path.join(this.sourceDir, file);
516
+ if (!fs.existsSync(filePath)) {
517
+ throw new Error(`Required file not found: ${file}`);
518
+ }
519
+ }
520
+ }
521
+
522
+ async createTables(db) {
523
+ this.updateProgress(0, 'Creating Tables');
524
+
525
+ const tableSQL = [
526
+ // Drop existing tables
527
+ 'DROP TABLE IF EXISTS RXNCONSO',
528
+ 'DROP TABLE IF EXISTS RXNREL',
529
+ 'DROP TABLE IF EXISTS RXNSTY',
530
+ 'DROP TABLE IF EXISTS RXNSTEMS',
531
+ 'DROP TABLE IF EXISTS RXNATOMARCHIVE',
532
+ 'DROP TABLE IF EXISTS RXNCUI',
533
+ 'DROP TABLE IF EXISTS RXNSAB',
534
+
535
+ // Create RXNCONSO table
536
+ `CREATE TABLE RXNCONSO (
537
+ RXCUI varchar(8) NOT NULL,
538
+ RXAUI varchar(8) NOT NULL,
539
+ SAB varchar(20) NOT NULL,
540
+ TTY varchar(20) NOT NULL,
541
+ CODE varchar(50) NOT NULL,
542
+ STR varchar(3000) NOT NULL,
543
+ SUPPRESS varchar(1)
544
+ )`,
545
+
546
+ // Create RXNREL table
547
+ `CREATE TABLE RXNREL (
548
+ RXCUI1 varchar(8),
549
+ RXAUI1 varchar(8),
550
+ REL varchar(4),
551
+ RXCUI2 varchar(8),
552
+ RXAUI2 varchar(8),
553
+ RELA varchar(100),
554
+ SAB varchar(20) NOT NULL
555
+ )`,
556
+
557
+ // Create RXNSTY table
558
+ `CREATE TABLE RXNSTY (
559
+ RXCUI varchar(8) NOT NULL,
560
+ TUI varchar(4)
561
+ )`,
562
+
563
+ // Create RXNATOMARCHIVE table
564
+ `CREATE TABLE RXNATOMARCHIVE (
565
+ RXAUI varchar(8) NOT NULL PRIMARY KEY,
566
+ AUI varchar(10),
567
+ STR varchar(4000) NOT NULL,
568
+ ARCHIVE_TIMESTAMP varchar(280) NOT NULL,
569
+ CREATED_TIMESTAMP varchar(280) NOT NULL,
570
+ UPDATED_TIMESTAMP varchar(280) NOT NULL,
571
+ CODE varchar(50),
572
+ IS_BRAND varchar(1),
573
+ LAT varchar(3),
574
+ LAST_RELEASED varchar(30),
575
+ SAUI varchar(50),
576
+ VSAB varchar(40),
577
+ RXCUI varchar(8),
578
+ SAB varchar(20),
579
+ TTY varchar(20),
580
+ MERGED_TO_RXCUI varchar(8)
581
+ )`,
582
+
583
+ // Create RXNSTEMS table
584
+ `CREATE TABLE RXNSTEMS (
585
+ stem CHAR(20) NOT NULL,
586
+ CUI VARCHAR(8) NOT NULL,
587
+ PRIMARY KEY (stem, CUI)
588
+ )`,
589
+
590
+ // Create RXNSAB table
591
+ `CREATE TABLE RXNSAB (
592
+ VCUI varchar(8),
593
+ RCUI varchar(8),
594
+ VSAB varchar(40),
595
+ RSAB varchar(20) NOT NULL,
596
+ SON varchar(3000),
597
+ SF varchar(20),
598
+ SVER varchar(20),
599
+ VSTART varchar(10),
600
+ VEND varchar(10),
601
+ IMETA varchar(10),
602
+ RMETA varchar(10),
603
+ SLC varchar(1000),
604
+ SCC varchar(1000),
605
+ SRL integer,
606
+ TFR integer,
607
+ CFR integer,
608
+ CXTY varchar(50),
609
+ TTYL varchar(300),
610
+ ATNL varchar(1000),
611
+ LAT varchar(3),
612
+ CENC varchar(20),
613
+ CURVER varchar(1),
614
+ SABIN varchar(1),
615
+ SSN varchar(3000),
616
+ SCIT varchar(4000),
617
+ PRIMARY KEY (VCUI)
618
+ )`,
619
+
620
+ // Create RXNCUI table
621
+ `CREATE TABLE RXNCUI (
622
+ cui1 VARCHAR(8),
623
+ ver_start VARCHAR(40),
624
+ ver_end VARCHAR(40),
625
+ cardinality VARCHAR(8),
626
+ cui2 VARCHAR(8),
627
+ PRIMARY KEY (cui1)
628
+ )`
629
+ ];
630
+
631
+ return new Promise((resolve) => {
632
+ db.serialize(() => {
633
+ tableSQL.forEach(sql => {
634
+ db.run(sql, (err) => {
635
+ if (err && this.options.verbose) {
636
+ console.warn(`Table creation warning: ${err.message}`);
637
+ }
638
+ });
639
+ });
640
+
641
+ if (this.options.verbose) console.log('Database tables created');
642
+ resolve();
643
+ });
644
+ });
645
+ }
646
+
647
+ async loadRXNCONSO(db) {
648
+ await this.loadRRFFile(db, 'RXNCONSO.RRF',
649
+ 'INSERT INTO RXNCONSO (RXCUI, RXAUI, SAB, TTY, CODE, STR, SUPPRESS) VALUES (?, ?, ?, ?, ?, ?, ?)',
650
+ (items) => [items[0], items[7], items[11], items[12], items[13], items[14], items[16]]
651
+ );
652
+ }
653
+
654
+ async loadRXNSAB(db) {
655
+ const filePath = path.join(this.sourceDir, 'RXNSAB.RRF');
656
+ if (!fs.existsSync(filePath)) return;
657
+
658
+ await this.loadRRFFile(db, 'RXNSAB.RRF',
659
+ `INSERT OR IGNORE INTO RXNSAB (VCUI, RCUI, VSAB, RSAB, SON, SF, SVER, VSTART, VEND, IMETA, RMETA, SLC, SCC, SRL, TFR, CFR, CXTY, TTYL, ATNL, LAT, CENC, CURVER, SABIN, SSN, SCIT)
660
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
661
+ (items) => items.slice(0, 25)
662
+ );
663
+ }
664
+
665
+ async loadRXNCUI(db) {
666
+ const filePath = path.join(this.sourceDir, 'RXNCUI.RRF');
667
+ if (!fs.existsSync(filePath)) return;
668
+
669
+ await this.loadRRFFile(db, 'RXNCUI.RRF',
670
+ 'INSERT OR IGNORE INTO RXNCUI (cui1, ver_start, ver_end, cardinality, cui2) VALUES (?, ?, ?, ?, ?)',
671
+ (items) => items.slice(0, 5)
672
+ );
673
+ }
674
+
675
+ async loadRXNATOMARCHIVE(db) {
676
+ const filePath = path.join(this.sourceDir, 'RXNATOMARCHIVE.RRF');
677
+ if (!fs.existsSync(filePath)) return;
678
+
679
+ await this.loadRRFFile(db, 'RXNATOMARCHIVE.RRF',
680
+ `INSERT OR IGNORE INTO RXNATOMARCHIVE (RXAUI, AUI, STR, ARCHIVE_TIMESTAMP, CREATED_TIMESTAMP, UPDATED_TIMESTAMP, CODE, IS_BRAND, LAT, LAST_RELEASED, SAUI, VSAB, RXCUI, SAB, TTY, MERGED_TO_RXCUI)
681
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
682
+ (items) => items.slice(0, 16)
683
+ );
684
+ }
685
+
686
+ async loadRXNREL(db) {
687
+ await this.loadRRFFile(db, 'RXNREL.RRF',
688
+ 'INSERT INTO RXNREL (RXCUI1, RXAUI1, REL, RXCUI2, RXAUI2, RELA, SAB) VALUES (?, ?, ?, ?, ?, ?, ?)',
689
+ (items) => [items[0], items[1], items[3], items[4], items[5], items[7], items[10]]
690
+ );
691
+ }
692
+
693
+ async loadRXNSTY(db) {
694
+ await this.loadRRFFile(db, 'RXNSTY.RRF',
695
+ 'INSERT INTO RXNSTY (RXCUI, TUI) VALUES (?, ?)',
696
+ (items) => [items[0], items[1]]
697
+ );
698
+ }
699
+
700
+ async loadRRFFile(db, fileName, insertSQL, extractValues) {
701
+ const filePath = path.join(this.sourceDir, fileName);
702
+ if (!fs.existsSync(filePath)) {
703
+ if (this.options.verbose) console.warn(`File not found: ${fileName}`);
704
+ return;
705
+ }
706
+
707
+ this.updateProgress(0, `Loading ${fileName}`);
708
+
709
+ const rl = readline.createInterface({
710
+ input: fs.createReadStream(filePath),
711
+ crlfDelay: Infinity
712
+ });
713
+
714
+ return new Promise((resolve, reject) => {
715
+ db.serialize(() => {
716
+ db.run('BEGIN TRANSACTION');
717
+
718
+ const stmt = db.prepare(insertSQL);
719
+ let lineCount = 0;
720
+ let processedCount = 0;
721
+ let errorCount = 0;
722
+
723
+ rl.on('line', (line) => {
724
+ lineCount++;
725
+
726
+ const items = line.split('|');
727
+ if (items.length === 0) return;
728
+
729
+ try {
730
+ const values = extractValues(items);
731
+ stmt.run(values, (err) => {
732
+ if (err && err.code !== 'SQLITE_CONSTRAINT') {
733
+ errorCount++;
734
+ if (this.options.verbose && errorCount <= 10) {
735
+ console.warn(`Error processing line ${lineCount} in ${fileName}: ${err.message}`);
736
+ }
737
+ }
738
+ });
739
+ processedCount++;
740
+
741
+ if (processedCount % 1000 === 0) {
742
+ this.updateProgress(1000);
743
+ }
744
+ } catch (error) {
745
+ errorCount++;
746
+ if (this.options.verbose && errorCount <= 10) {
747
+ console.warn(`Error processing line ${lineCount} in ${fileName}: ${error.message}`);
748
+ }
749
+ }
750
+ });
751
+
752
+ rl.on('close', () => {
753
+ stmt.finalize();
754
+ db.run('COMMIT', (err) => {
755
+ if (err) reject(err);
756
+ else {
757
+ // Update progress for remaining records
758
+ const remaining = processedCount % 1000;
759
+ if (remaining > 0) {
760
+ this.updateProgress(remaining);
761
+ }
762
+
763
+ if (this.options.verbose) {
764
+ console.log(` Loaded ${processedCount} records from ${fileName}${errorCount > 0 ? ` (${errorCount} errors)` : ''}`);
765
+ }
766
+ resolve();
767
+ }
768
+ });
769
+ });
770
+
771
+ rl.on('error', reject);
772
+ });
773
+ });
774
+ }
775
+
776
+ async makeStems(db) {
777
+ this.updateProgress(0, 'Generating Stems');
778
+
779
+ if (this.options.verbose) console.log('Generating word stems...');
780
+
781
+ // Simple English stemmer implementation
782
+ const stemmer = new SimpleStemmer();
783
+ const stems = new Map();
784
+
785
+ // Get all RXNORM concepts
786
+ return new Promise((resolve, reject) => {
787
+ db.all("SELECT RXCUI, STR FROM RXNCONSO WHERE SAB = 'RXNORM'", (err, rows) => {
788
+ if (err) return reject(err);
789
+
790
+ // Process each concept and generate stems
791
+ rows.forEach(row => {
792
+ const words = this.extractWords(row.STR);
793
+ words.forEach(word => {
794
+ const stem = stemmer.stem(word.toLowerCase());
795
+ if (stem.length > 0 && stem.length <= 20) {
796
+ if (!stems.has(stem)) {
797
+ stems.set(stem, new Set());
798
+ }
799
+ stems.get(stem).add(row.RXCUI);
800
+ }
801
+ });
802
+ });
803
+
804
+ // Insert stems into database
805
+ db.serialize(() => {
806
+ db.run('BEGIN TRANSACTION');
807
+ const stmt = db.prepare('INSERT OR IGNORE INTO RXNSTEMS (stem, CUI) VALUES (?, ?)');
808
+
809
+ let totalInserts = 0;
810
+ let processedStems = 0;
811
+
812
+ for (const [stem, cuis] of stems) {
813
+ for (const cui of cuis) {
814
+ stmt.run([stem, cui]);
815
+ totalInserts++;
816
+
817
+ if (totalInserts % 1000 === 0) {
818
+ this.updateProgress(1000);
819
+ }
820
+ }
821
+ processedStems++;
822
+ }
823
+
824
+ stmt.finalize();
825
+ db.run('COMMIT', (err) => {
826
+ if (err) reject(err);
827
+ else {
828
+ // Update progress for remaining inserts
829
+ const remaining = totalInserts % 1000;
830
+ if (remaining > 0) {
831
+ this.updateProgress(remaining);
832
+ }
833
+
834
+ if (this.options.verbose) {
835
+ console.log(` Generated ${totalInserts} stem entries from ${processedStems} unique stems`);
836
+ }
837
+ resolve();
838
+ }
839
+ });
840
+ });
841
+ });
842
+ });
843
+ }
844
+
845
+ extractWords(text) {
846
+ // Extract words from text, removing punctuation and numbers
847
+ return text
848
+ .toLowerCase()
849
+ .replace(/[^\w\s]/g, ' ')
850
+ .split(/\s+/)
851
+ .filter(word => word.length > 2 && !/^\d+$/.test(word))
852
+ .filter(word => word.match(/^[a-z]/));
853
+ }
854
+
855
+ async closeDatabase(db) {
856
+ return new Promise((resolve) => {
857
+ db.close((err) => {
858
+ if (err && this.options.verbose) {
859
+ console.error('Error closing database:', err);
860
+ }
861
+ resolve();
862
+ });
863
+ });
864
+ }
865
+ }
866
+
867
+ // Simple English stemmer (basic implementation)
868
+ class SimpleStemmer {
869
+ constructor() {
870
+ // Common English suffixes to remove
871
+ this.suffixes = [
872
+ 'ing', 'ly', 'ed', 'ies', 'ied', 'ies', 'ies', 'y', 's',
873
+ 'tion', 'sion', 'ness', 'ment', 'able', 'ible', 'ant', 'ent'
874
+ ].sort((a, b) => b.length - a.length); // Longest first
875
+ }
876
+
877
+ stem(word) {
878
+ if (word.length <= 3) return word;
879
+
880
+ // Try to remove suffixes
881
+ for (const suffix of this.suffixes) {
882
+ if (word.endsWith(suffix) && word.length > suffix.length + 2) {
883
+ const stem = word.substring(0, word.length - suffix.length);
884
+ // Basic validation - stem should still be reasonable length
885
+ if (stem.length >= 3) {
886
+ return stem;
887
+ }
888
+ }
889
+ }
890
+
891
+ return word;
892
+ }
893
+ }
894
+
895
+ module.exports = {
896
+ RxNormModule,
897
+ RxNormImporter
898
+ };