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,938 @@
1
+ const { BaseTerminologyModule } = require('./tx-import-base');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+ const chalk = require('chalk');
6
+
7
+ class RxNormSubsetModule extends BaseTerminologyModule {
8
+ constructor() {
9
+ super();
10
+ }
11
+
12
+ getName() {
13
+ return 'rxnorm-subset';
14
+ }
15
+
16
+ getDescription() {
17
+ return 'Create a subset of RxNorm data for testing purposes';
18
+ }
19
+
20
+ getSupportedFormats() {
21
+ return ['directory', 'txt'];
22
+ }
23
+
24
+ getDefaultConfig() {
25
+ return {
26
+ ...super.getDefaultConfig(),
27
+ dest: './rxnorm-subset',
28
+ expandRelationships: true,
29
+ includeSynonyms: false,
30
+ includeArchived: false,
31
+ maxIterations: 5
32
+ };
33
+ }
34
+
35
+ getEstimatedDuration() {
36
+ return '10-30 minutes (depending on relationship expansion)';
37
+ }
38
+
39
+ registerCommands(terminologyCommand, globalOptions) {
40
+ // Subset command
41
+ terminologyCommand
42
+ .command('subset')
43
+ .description('Create an RxNorm subset from a list of codes')
44
+ .option('-s, --source <directory>', 'Source RxNorm directory (RRF files)')
45
+ .option('-d, --dest <directory>', 'Destination directory for subset')
46
+ .option('-c, --codes <file>', 'Text file with RxNorm codes (one per line)')
47
+ .option('--no-expand', 'Skip relationship expansion')
48
+ .option('--include-synonyms', 'Include synonym (SY) terms')
49
+ .option('--include-archived', 'Include archived concepts')
50
+ .option('--max-iterations <n>', 'Maximum relationship expansion iterations', '5')
51
+ .option('-y, --yes', 'Skip confirmations')
52
+ .action(async (options) => {
53
+ await this.handleSubsetCommand({...globalOptions, ...options});
54
+ });
55
+
56
+ // Validate command
57
+ terminologyCommand
58
+ .command('validate')
59
+ .description('Validate subset inputs')
60
+ .option('-s, --source <directory>', 'Source RxNorm directory to validate')
61
+ .option('-c, --codes <file>', 'Codes file to validate')
62
+ .action(async (options) => {
63
+ await this.handleValidateCommand({...globalOptions, ...options});
64
+ });
65
+ }
66
+
67
+ async handleSubsetCommand(options) {
68
+ try {
69
+ // Gather configuration
70
+ const config = await this.gatherSubsetConfig(options);
71
+
72
+ // Show confirmation unless --yes is specified
73
+ if (!options.yes) {
74
+ const confirmed = await this.confirmSubset(config);
75
+ if (!confirmed) {
76
+ this.logInfo('Subset operation cancelled');
77
+ return;
78
+ }
79
+ }
80
+
81
+ // Save configuration
82
+ this.rememberSuccessfulConfig(config);
83
+
84
+ // Run the subset operation
85
+ await this.runSubset(config);
86
+ } catch (error) {
87
+ this.logError(`Subset operation failed: ${error.message}`);
88
+ if (options.verbose) {
89
+ console.error(error.stack);
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ async gatherSubsetConfig(options) {
96
+ const terminology = this.getName();
97
+ const smartDefaults = this.configManager.generateDefaults(terminology);
98
+ const recentSources = this.configManager.getRecentSources(terminology, 3);
99
+
100
+ const questions = [];
101
+
102
+ // Source directory
103
+ if (!options.source) {
104
+ const sourceQuestion = {
105
+ type: 'input',
106
+ name: 'source',
107
+ message: 'Source RxNorm directory (RRF files):',
108
+ validate: (input) => {
109
+ if (!input) return 'Source directory is required';
110
+ if (!fs.existsSync(input)) return 'Source directory does not exist';
111
+ return true;
112
+ },
113
+ filter: (input) => path.resolve(input)
114
+ };
115
+
116
+ if (smartDefaults.source) {
117
+ sourceQuestion.default = smartDefaults.source;
118
+ }
119
+
120
+ if (recentSources.length > 0) {
121
+ sourceQuestion.type = 'list';
122
+ sourceQuestion.choices = [
123
+ ...recentSources.map(src => ({
124
+ name: `${src} ${src === smartDefaults.source ? '(last used)' : ''}`.trim(),
125
+ value: src
126
+ })),
127
+ { name: 'Enter new path...', value: 'NEW_PATH' }
128
+ ];
129
+ sourceQuestion.message = 'Select source RxNorm directory:';
130
+ }
131
+
132
+ questions.push(sourceQuestion);
133
+
134
+ if (recentSources.length > 0) {
135
+ questions.push({
136
+ type: 'input',
137
+ name: 'source',
138
+ message: 'Enter new source path:',
139
+ when: (answers) => answers.source === 'NEW_PATH',
140
+ validate: (input) => {
141
+ if (!input) return 'Source directory is required';
142
+ if (!fs.existsSync(input)) return 'Source directory does not exist';
143
+ return true;
144
+ },
145
+ filter: (input) => path.resolve(input)
146
+ });
147
+ }
148
+ }
149
+
150
+ // Destination directory
151
+ if (!options.dest) {
152
+ questions.push({
153
+ type: 'input',
154
+ name: 'dest',
155
+ message: 'Destination directory for subset:',
156
+ default: smartDefaults.dest || './rxnorm-subset',
157
+ validate: (input) => {
158
+ if (!input) return 'Destination directory is required';
159
+ return true;
160
+ },
161
+ filter: (input) => path.resolve(input)
162
+ });
163
+ }
164
+
165
+ // Codes file - default to /tx/data/rxnorm-subset if it exists
166
+ if (!options.codes) {
167
+ const defaultCodesFile = '/tx/data/rxnorm-subset';
168
+ questions.push({
169
+ type: 'input',
170
+ name: 'codes',
171
+ message: 'Codes file (one code per line):',
172
+ default: fs.existsSync(defaultCodesFile) ? defaultCodesFile : smartDefaults.codes,
173
+ validate: (input) => {
174
+ if (!input) return 'Codes file is required';
175
+ if (!fs.existsSync(input)) return 'Codes file does not exist';
176
+ return true;
177
+ },
178
+ filter: (input) => path.resolve(input)
179
+ });
180
+ }
181
+
182
+ // Overwrite confirmation
183
+ questions.push({
184
+ type: 'confirm',
185
+ name: 'overwrite',
186
+ message: 'Overwrite destination directory if it exists?',
187
+ default: smartDefaults.overwrite !== undefined ? smartDefaults.overwrite : false,
188
+ when: (answers) => {
189
+ const destPath = options.dest || answers.dest;
190
+ return fs.existsSync(destPath);
191
+ }
192
+ });
193
+
194
+ // Expansion options
195
+ questions.push({
196
+ type: 'confirm',
197
+ name: 'expandRelationships',
198
+ message: 'Expand codes based on relationships (ingredients, forms, etc.)?',
199
+ default: smartDefaults.expandRelationships !== undefined ? smartDefaults.expandRelationships : true
200
+ });
201
+
202
+ questions.push({
203
+ type: 'confirm',
204
+ name: 'includeSynonyms',
205
+ message: 'Include synonym (SY) terms?',
206
+ default: smartDefaults.includeSynonyms !== undefined ? smartDefaults.includeSynonyms : false
207
+ });
208
+
209
+ questions.push({
210
+ type: 'confirm',
211
+ name: 'includeArchived',
212
+ message: 'Include archived concepts?',
213
+ default: smartDefaults.includeArchived !== undefined ? smartDefaults.includeArchived : false
214
+ });
215
+
216
+ questions.push({
217
+ type: 'confirm',
218
+ name: 'verbose',
219
+ message: 'Show verbose output?',
220
+ default: smartDefaults.verbose !== undefined ? smartDefaults.verbose : true
221
+ });
222
+
223
+ const answers = await require('inquirer').prompt(questions);
224
+
225
+ const finalConfig = {
226
+ ...this.getDefaultConfig(),
227
+ ...smartDefaults,
228
+ ...options,
229
+ ...answers,
230
+ expandRelationships: !options.noExpand && (answers.expandRelationships !== false),
231
+ includeSynonyms: options.includeSynonyms || answers.includeSynonyms,
232
+ includeArchived: options.includeArchived || answers.includeArchived,
233
+ maxIterations: parseInt(options.maxIterations) || 5
234
+ };
235
+
236
+ return finalConfig;
237
+ }
238
+
239
+ async confirmSubset(config) {
240
+ console.log(chalk.cyan(`\n📋 RxNorm Subset Configuration:`));
241
+ console.log(` Source: ${chalk.white(config.source)}`);
242
+ console.log(` Destination: ${chalk.white(config.dest)}`);
243
+ console.log(` Codes File: ${chalk.white(config.codes)}`);
244
+ console.log(` Expand Relationships: ${chalk.white(config.expandRelationships ? 'Yes' : 'No')}`);
245
+ console.log(` Include Synonyms: ${chalk.white(config.includeSynonyms ? 'Yes' : 'No')}`);
246
+ console.log(` Include Archived: ${chalk.white(config.includeArchived ? 'Yes' : 'No')}`);
247
+ console.log(` Max Iterations: ${chalk.white(config.maxIterations)}`);
248
+ console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
249
+
250
+ if (config.estimatedDuration) {
251
+ console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
252
+ }
253
+
254
+ const { confirmed } = await require('inquirer').prompt({
255
+ type: 'confirm',
256
+ name: 'confirmed',
257
+ message: 'Proceed with subset creation?',
258
+ default: true
259
+ });
260
+
261
+ return confirmed;
262
+ }
263
+
264
+ async runSubset(config) {
265
+ try {
266
+ console.log(chalk.blue.bold(`🧬 Starting RxNorm Subset Creation...\n`));
267
+
268
+ // Pre-flight checks
269
+ this.logInfo('Running pre-flight checks...');
270
+ const prerequisitesPassed = await this.validateSubsetPrerequisites(config);
271
+
272
+ if (!prerequisitesPassed) {
273
+ throw new Error('Pre-flight checks failed');
274
+ }
275
+
276
+ // Execute the subset creation
277
+ await this.executeSubset(config);
278
+
279
+ this.logSuccess('RxNorm subset created successfully!');
280
+
281
+ } catch (error) {
282
+ this.stopProgress();
283
+ this.logError(`RxNorm subset creation failed: ${error.message}`);
284
+ if (config.verbose) {
285
+ console.error(error.stack);
286
+ }
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ async handleValidateCommand(options) {
292
+ if (!options.source || !options.codes) {
293
+ const answers = await require('inquirer').prompt([
294
+ {
295
+ type: 'input',
296
+ name: 'source',
297
+ message: 'Source RxNorm directory:',
298
+ when: !options.source,
299
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
300
+ },
301
+ {
302
+ type: 'input',
303
+ name: 'codes',
304
+ message: 'Codes file:',
305
+ when: !options.codes,
306
+ validate: (input) => input && fs.existsSync(input) ? true : 'File does not exist'
307
+ }
308
+ ]);
309
+ Object.assign(options, answers);
310
+ }
311
+
312
+ this.logInfo('Validating subset inputs...');
313
+
314
+ try {
315
+ const stats = await this.validateSubsetInputs(options.source, options.codes);
316
+
317
+ this.logSuccess('Validation passed');
318
+ console.log(` Required files found: ${stats.requiredFiles.length}/3`);
319
+ console.log(` Optional files found: ${stats.optionalFiles.length}/3`);
320
+ console.log(` Codes in list: ${stats.codeCount.toLocaleString()}`);
321
+ console.log(` Unique codes: ${stats.uniqueCodes.toLocaleString()}`);
322
+
323
+ if (stats.warnings.length > 0) {
324
+ this.logWarning('Validation warnings:');
325
+ stats.warnings.forEach(warning => console.log(` ${warning}`));
326
+ }
327
+
328
+ } catch (error) {
329
+ this.logError(`Validation failed: ${error.message}`);
330
+ }
331
+ }
332
+
333
+ async validateSubsetPrerequisites(config) {
334
+ const checks = [
335
+ {
336
+ name: 'Source directory exists',
337
+ check: () => fs.existsSync(config.source)
338
+ },
339
+ {
340
+ name: 'Codes file exists',
341
+ check: () => fs.existsSync(config.codes)
342
+ },
343
+ {
344
+ name: 'Source contains RxNorm RRF files',
345
+ check: () => {
346
+ const requiredFiles = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF'];
347
+ return requiredFiles.every(file =>
348
+ fs.existsSync(path.join(config.source, file))
349
+ );
350
+ }
351
+ }
352
+ ];
353
+
354
+ let allPassed = true;
355
+
356
+ for (const { name, check } of checks) {
357
+ try {
358
+ const passed = await check();
359
+ if (passed) {
360
+ this.logSuccess(name);
361
+ } else {
362
+ this.logError(name);
363
+ allPassed = false;
364
+ }
365
+ } catch (error) {
366
+ this.logError(`${name}: ${error.message}`);
367
+ allPassed = false;
368
+ }
369
+ }
370
+
371
+ return allPassed;
372
+ }
373
+
374
+ async executeSubset(config) {
375
+ this.logInfo('Loading target codes...');
376
+
377
+ // Load initial target codes
378
+ const initialTargetCodes = await this.loadTargetCodes(config.codes);
379
+ this.logInfo(`Loaded ${initialTargetCodes.size.toLocaleString()} initial target codes`);
380
+
381
+ if (config.verbose) {
382
+ const sampleCodes = Array.from(initialTargetCodes).slice(0, 10);
383
+ console.log(`Sample codes: ${sampleCodes.join(', ')}`);
384
+ }
385
+
386
+ let finalTargetCodes = initialTargetCodes;
387
+
388
+ // Expand target codes based on relationships if requested
389
+ if (config.expandRelationships) {
390
+ this.logInfo('Expanding target codes based on RxNorm relationships...');
391
+
392
+ const expander = new RxNormRelationshipExpander(
393
+ config.source,
394
+ config.verbose,
395
+ config.maxIterations
396
+ );
397
+
398
+ finalTargetCodes = await expander.expandCodes(initialTargetCodes);
399
+
400
+ const addedCodes = finalTargetCodes.size - initialTargetCodes.size;
401
+ this.logInfo(`Added ${addedCodes.toLocaleString()} related codes through relationship expansion`);
402
+
403
+ if (config.verbose && addedCodes > 0) {
404
+ const newCodes = Array.from(finalTargetCodes).filter(code => !initialTargetCodes.has(code));
405
+ const sampleNewCodes = newCodes.slice(0, 10);
406
+ console.log(`Sample newly added codes: ${sampleNewCodes.join(', ')}`);
407
+ }
408
+ }
409
+
410
+ this.logInfo(`Final target codes: ${finalTargetCodes.size.toLocaleString()}`);
411
+
412
+ // Export final codes for inspection
413
+ if (config.verbose) {
414
+ const codesOutputPath = path.join(process.cwd(), 'rxnorm-final-target-codes.txt');
415
+ await this.exportCodesToFile(finalTargetCodes, codesOutputPath);
416
+ }
417
+
418
+ // Create subset processor
419
+ const processor = new RxNormSubsetProcessor(this, config.verbose);
420
+
421
+ await processor.createSubset(
422
+ config.source,
423
+ config.dest,
424
+ finalTargetCodes,
425
+ {
426
+ verbose: config.verbose,
427
+ overwrite: config.overwrite,
428
+ includeSynonyms: config.includeSynonyms,
429
+ includeArchived: config.includeArchived
430
+ }
431
+ );
432
+ }
433
+
434
+ async loadTargetCodes(codesFile) {
435
+ const codes = new Set();
436
+
437
+ const rl = readline.createInterface({
438
+ input: fs.createReadStream(codesFile),
439
+ crlfDelay: Infinity
440
+ });
441
+
442
+ for await (const line of rl) {
443
+ const trimmedLine = line.trim();
444
+
445
+ // Skip empty lines and comments
446
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
447
+ continue;
448
+ }
449
+
450
+ // Extract code (everything before # if there's an inline comment)
451
+ const code = trimmedLine.split('#')[0].trim();
452
+ if (code) {
453
+ codes.add(code);
454
+ }
455
+ }
456
+
457
+ return codes;
458
+ }
459
+
460
+ async exportCodesToFile(codeSet, filePath) {
461
+ const sortedCodes = Array.from(codeSet).sort();
462
+ const content = sortedCodes.join('\n') + '\n';
463
+
464
+ fs.writeFileSync(filePath, content, 'utf8');
465
+ this.logInfo(`Exported ${sortedCodes.length.toLocaleString()} codes to ${filePath}`);
466
+ }
467
+
468
+ async validateSubsetInputs(sourceDir, codesFile) {
469
+ const stats = {
470
+ requiredFiles: [],
471
+ optionalFiles: [],
472
+ codeCount: 0,
473
+ uniqueCodes: 0,
474
+ warnings: []
475
+ };
476
+
477
+ // Check for RxNorm RRF files
478
+ const requiredFiles = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF'];
479
+ const optionalFiles = ['RXNSAB.RRF', 'RXNATOMARCHIVE.RRF', 'RXNCUI.RRF'];
480
+
481
+ for (const file of requiredFiles) {
482
+ const filePath = path.join(sourceDir, file);
483
+ if (fs.existsSync(filePath)) {
484
+ stats.requiredFiles.push(file);
485
+ } else {
486
+ stats.warnings.push(`Required file not found: ${file}`);
487
+ }
488
+ }
489
+
490
+ for (const file of optionalFiles) {
491
+ const filePath = path.join(sourceDir, file);
492
+ if (fs.existsSync(filePath)) {
493
+ stats.optionalFiles.push(file);
494
+ } else {
495
+ stats.warnings.push(`Optional file not found: ${file}`);
496
+ }
497
+ }
498
+
499
+ // Validate codes file
500
+ const codes = await this.loadTargetCodes(codesFile);
501
+ stats.codeCount = codes.size;
502
+ stats.uniqueCodes = codes.size;
503
+
504
+ return stats;
505
+ }
506
+ }
507
+
508
+ // RxNorm relationship expander
509
+ class RxNormRelationshipExpander {
510
+ constructor(sourceDir, verbose = false, maxIterations = 5) {
511
+ this.sourceDir = sourceDir;
512
+ this.verbose = verbose;
513
+ this.maxIterations = maxIterations;
514
+
515
+ // Relationships that define components/ingredients of target codes (inward expansion)
516
+ this.inwardRelationships = new Set([
517
+ 'has_ingredient',
518
+ 'has_form',
519
+ 'has_dose_form',
520
+ 'form_of',
521
+ 'ingredient_of',
522
+ 'consists_of',
523
+ 'contains'
524
+ ]);
525
+
526
+ // REL codes for inward relationships
527
+ this.inwardRels = new Set(['RN', 'IN']); // Ingredient relationships
528
+ }
529
+
530
+ async expandCodes(initialCodes) {
531
+ if (this.verbose) {
532
+ console.log(` Starting relationship expansion with ${initialCodes.size} codes`);
533
+ }
534
+
535
+ let currentCodes = new Set(initialCodes);
536
+ let iteration = 0;
537
+
538
+ while (iteration < this.maxIterations) {
539
+ iteration++;
540
+ const sizeBefore = currentCodes.size;
541
+
542
+ if (this.verbose) {
543
+ console.log(` Iteration ${iteration}: Starting with ${sizeBefore} codes`);
544
+ }
545
+
546
+ const newCodes = await this.findRelatedCodes(currentCodes);
547
+
548
+ // Add new codes to current set
549
+ for (const code of newCodes) {
550
+ currentCodes.add(code);
551
+ }
552
+
553
+ const sizeAfter = currentCodes.size;
554
+ const added = sizeAfter - sizeBefore;
555
+
556
+ if (this.verbose) {
557
+ console.log(` Iteration ${iteration}: Added ${added} codes (total: ${sizeAfter})`);
558
+ }
559
+
560
+ // Stop if no new codes were found
561
+ if (added === 0) {
562
+ if (this.verbose) {
563
+ console.log(` Expansion converged after ${iteration} iterations`);
564
+ }
565
+ break;
566
+ }
567
+ }
568
+
569
+ return currentCodes;
570
+ }
571
+
572
+ async findRelatedCodes(targetCodes) {
573
+ const relatedCodes = new Set();
574
+ const rxnrelPath = path.join(this.sourceDir, 'RXNREL.RRF');
575
+
576
+ if (!fs.existsSync(rxnrelPath)) {
577
+ if (this.verbose) {
578
+ console.log(` RXNREL file not found: ${rxnrelPath}`);
579
+ }
580
+ return relatedCodes;
581
+ }
582
+
583
+ const rl = readline.createInterface({
584
+ input: fs.createReadStream(rxnrelPath),
585
+ crlfDelay: Infinity
586
+ });
587
+
588
+ let processedLines = 0;
589
+ let matchedRelationships = 0;
590
+
591
+ for await (const line of rl) {
592
+ processedLines++;
593
+
594
+ const items = line.split('|');
595
+ if (items.length < 11) continue;
596
+
597
+ const rxcui1 = items[0]; // Source concept
598
+ const rel = items[3]; // Relationship type
599
+ const rxcui2 = items[4]; // Target concept
600
+ const rela = items[7]; // Specific relationship
601
+ const sab = items[10]; // Source
602
+
603
+ // Focus on RXNORM relationships
604
+ if (sab !== 'RXNORM') continue;
605
+
606
+ // Find inward relationships - where our target codes are the "complex" concept
607
+ // and we want to include their "simple" components
608
+ if (targetCodes.has(rxcui1)) {
609
+ // Target code is source - include target (RXCUI2) for inward relationships
610
+ if (this.isInwardRelationship(rel, rela)) {
611
+ relatedCodes.add(rxcui2);
612
+ matchedRelationships++;
613
+ }
614
+ }
615
+
616
+ // Also check reverse relationships - if our target is a component,
617
+ // include the complex concept that contains it
618
+ if (targetCodes.has(rxcui2)) {
619
+ // Target code is target - include source (RXCUI1) for outward relationships
620
+ if (this.isReverseInwardRelationship(rel, rela)) {
621
+ relatedCodes.add(rxcui1);
622
+ matchedRelationships++;
623
+ }
624
+ }
625
+
626
+ if (processedLines % 100000 === 0 && this.verbose) {
627
+ console.log(` Processed ${processedLines} relationships, found ${matchedRelationships} matches`);
628
+ }
629
+ }
630
+
631
+ if (this.verbose) {
632
+ console.log(` Found ${relatedCodes.size} related codes from ${matchedRelationships} relationships`);
633
+ }
634
+
635
+ return relatedCodes;
636
+ }
637
+
638
+ isInwardRelationship(rel, rela) {
639
+ // REL-based relationships
640
+ if (this.inwardRels.has(rel)) {
641
+ return true;
642
+ }
643
+
644
+ // RELA-based relationships (more specific)
645
+ if (rela && this.inwardRelationships.has(rela)) {
646
+ return true;
647
+ }
648
+
649
+ // Other common inward relationship patterns
650
+ const inwardPatterns = [
651
+ 'has_ingredient',
652
+ 'has_active_ingredient',
653
+ 'has_precise_ingredient',
654
+ 'has_form',
655
+ 'has_dose_form',
656
+ 'contains',
657
+ 'consists_of'
658
+ ];
659
+
660
+ return rela && inwardPatterns.some(pattern => rela.includes(pattern));
661
+ }
662
+
663
+ isReverseInwardRelationship(rel, rela) {
664
+ // These are relationships where if our target is RXCUI2,
665
+ // we want to include RXCUI1 as it helps define our target
666
+ const reversePatterns = [
667
+ 'ingredient_of',
668
+ 'form_of',
669
+ 'active_ingredient_of',
670
+ 'precise_ingredient_of',
671
+ 'contained_in',
672
+ 'part_of'
673
+ ];
674
+
675
+ return rela && reversePatterns.some(pattern => rela.includes(pattern));
676
+ }
677
+ }
678
+
679
+ // RxNorm subset processor
680
+ class RxNormSubsetProcessor {
681
+ constructor(moduleInstance, verbose = true) {
682
+ this.module = moduleInstance;
683
+ this.verbose = verbose;
684
+ this.targetCodes = null;
685
+ this.processedFiles = 0;
686
+ this.totalFiles = 0;
687
+ }
688
+
689
+ async createSubset(sourceDir, destDir, targetCodes, options) {
690
+ this.targetCodes = targetCodes;
691
+
692
+ // Create destination directory structure
693
+ await this.createDirectoryStructure(destDir, options.overwrite);
694
+
695
+ // Define RRF files to process
696
+ const filesToProcess = [
697
+ {
698
+ source: 'RXNCONSO.RRF',
699
+ dest: 'RXNCONSO.RRF',
700
+ handler: 'processRXNCONSO'
701
+ },
702
+ {
703
+ source: 'RXNREL.RRF',
704
+ dest: 'RXNREL.RRF',
705
+ handler: 'processRXNREL'
706
+ },
707
+ {
708
+ source: 'RXNSTY.RRF',
709
+ dest: 'RXNSTY.RRF',
710
+ handler: 'processRXNSTY'
711
+ },
712
+ {
713
+ source: 'RXNSAB.RRF',
714
+ dest: 'RXNSAB.RRF',
715
+ handler: 'processRXNSAB'
716
+ },
717
+ {
718
+ source: 'RXNCUI.RRF',
719
+ dest: 'RXNCUI.RRF',
720
+ handler: 'processRXNCUI'
721
+ }
722
+ ];
723
+
724
+ // Conditionally add archived file
725
+ if (options.includeArchived) {
726
+ filesToProcess.push({
727
+ source: 'RXNATOMARCHIVE.RRF',
728
+ dest: 'RXNATOMARCHIVE.RRF',
729
+ handler: 'processRXNATOMARCHIVE'
730
+ });
731
+ }
732
+
733
+ // Count existing files
734
+ this.totalFiles = filesToProcess.filter(file =>
735
+ fs.existsSync(path.join(sourceDir, file.source))
736
+ ).length;
737
+
738
+ this.module.logInfo(`Processing ${this.totalFiles} RRF files...`);
739
+ this.module.createProgressBar();
740
+ this.module.updateProgress(0, this.totalFiles);
741
+
742
+ // Process each file
743
+ for (const file of filesToProcess) {
744
+ const sourcePath = path.join(sourceDir, file.source);
745
+ const destPath = path.join(destDir, file.dest);
746
+
747
+ if (fs.existsSync(sourcePath)) {
748
+ if (this.verbose) {
749
+ this.module.logInfo(`Processing ${file.source}...`);
750
+ }
751
+
752
+ await this[file.handler](sourcePath, destPath, options);
753
+ this.processedFiles++;
754
+ this.module.updateProgress(this.processedFiles);
755
+ }
756
+ }
757
+
758
+ this.module.stopProgress();
759
+
760
+ // Generate subset statistics
761
+ await this.generateSubsetStats(destDir, targetCodes);
762
+ }
763
+
764
+ async createDirectoryStructure(destDir, overwrite) {
765
+ if (fs.existsSync(destDir)) {
766
+ if (overwrite) {
767
+ fs.rmSync(destDir, { recursive: true, force: true });
768
+ } else {
769
+ throw new Error(`Destination directory already exists: ${destDir}`);
770
+ }
771
+ }
772
+
773
+ fs.mkdirSync(destDir, { recursive: true });
774
+ }
775
+
776
+ async processRXNCONSO(sourcePath, destPath, options) {
777
+ await this.processRRFFile(sourcePath, destPath, (items) => {
778
+ const rxcui = items[0];
779
+ const tty = items[12];
780
+
781
+ // Include if RXCUI is in target set
782
+ if (this.targetCodes.has(rxcui)) {
783
+ // Optionally filter out synonyms
784
+ if (!options.includeSynonyms && tty === 'SY') {
785
+ return false;
786
+ }
787
+ return true;
788
+ }
789
+
790
+ return false;
791
+ });
792
+ }
793
+
794
+ async processRXNREL(sourcePath, destPath) {
795
+ await this.processRRFFile(sourcePath, destPath, (items) => {
796
+ const rxcui1 = items[0];
797
+ const rxcui2 = items[4];
798
+
799
+ // Include if either RXCUI is in target set
800
+ return this.targetCodes.has(rxcui1) || this.targetCodes.has(rxcui2);
801
+ });
802
+ }
803
+
804
+ async processRXNSTY(sourcePath, destPath) {
805
+ await this.processRRFFile(sourcePath, destPath, (items) => {
806
+ const rxcui = items[0];
807
+ return this.targetCodes.has(rxcui);
808
+ });
809
+ }
810
+
811
+ async processRXNSAB(sourcePath, destPath) {
812
+ // For RXNSAB, we need to find which sources are referenced
813
+ // First pass: collect all SABs referenced in target concepts
814
+ const referencedSabs = await this.findReferencedSabs(sourcePath.replace('RXNSAB.RRF', 'RXNCONSO.RRF'));
815
+
816
+ await this.processRRFFile(sourcePath, destPath, (items) => {
817
+ const rsab = items[3]; // RSAB field
818
+ return referencedSabs.has(rsab);
819
+ });
820
+ }
821
+
822
+ async processRXNCUI(sourcePath, destPath) {
823
+ await this.processRRFFile(sourcePath, destPath, (items) => {
824
+ const cui1 = items[0];
825
+ const cui2 = items[4];
826
+
827
+ // Include if either CUI is in target set
828
+ return this.targetCodes.has(cui1) || (cui2 && this.targetCodes.has(cui2));
829
+ });
830
+ }
831
+
832
+ async processRXNATOMARCHIVE(sourcePath, destPath) {
833
+ await this.processRRFFile(sourcePath, destPath, (items) => {
834
+ const rxcui = items[12]; // RXCUI field in archive
835
+ return rxcui && this.targetCodes.has(rxcui);
836
+ });
837
+ }
838
+
839
+ async findReferencedSabs(rxnconsoPath) {
840
+ const referencedSabs = new Set();
841
+
842
+ if (!fs.existsSync(rxnconsoPath)) {
843
+ return referencedSabs;
844
+ }
845
+
846
+ const rl = readline.createInterface({
847
+ input: fs.createReadStream(rxnconsoPath),
848
+ crlfDelay: Infinity
849
+ });
850
+
851
+ for await (const line of rl) {
852
+ const items = line.split('|');
853
+ if (items.length < 12) continue;
854
+
855
+ const rxcui = items[0];
856
+ const sab = items[11];
857
+
858
+ if (this.targetCodes.has(rxcui)) {
859
+ referencedSabs.add(sab);
860
+ }
861
+ }
862
+
863
+ return referencedSabs;
864
+ }
865
+
866
+ async processRRFFile(sourcePath, destPath, filterFunction) {
867
+ const readStream = fs.createReadStream(sourcePath);
868
+ const writeStream = fs.createWriteStream(destPath);
869
+
870
+ const rl = readline.createInterface({
871
+ input: readStream,
872
+ crlfDelay: Infinity
873
+ });
874
+
875
+ let lineNum = 0;
876
+ let includedLines = 0;
877
+
878
+ for await (const line of rl) {
879
+ lineNum++;
880
+
881
+ const items = line.split('|');
882
+
883
+ if (filterFunction(items)) {
884
+ writeStream.write(line + '\n');
885
+ includedLines++;
886
+ }
887
+ }
888
+
889
+ writeStream.end();
890
+
891
+ if (this.verbose && lineNum > 0) {
892
+ const filename = path.basename(sourcePath);
893
+ console.log(` Included ${includedLines.toLocaleString()} of ${lineNum.toLocaleString()} lines in ${filename}`);
894
+ }
895
+ }
896
+
897
+ async generateSubsetStats(destDir, targetCodes) {
898
+ const stats = {
899
+ originalTargetCodes: targetCodes.size,
900
+ timestamp: new Date().toISOString(),
901
+ files: {}
902
+ };
903
+
904
+ // Count lines in each output file
905
+ const files = fs.readdirSync(destDir);
906
+ for (const file of files) {
907
+ if (file.endsWith('.RRF')) {
908
+ const filePath = path.join(destDir, file);
909
+ const lineCount = await this.countLines(filePath);
910
+ stats.files[file] = lineCount;
911
+ }
912
+ }
913
+
914
+ // Write stats file
915
+ const statsPath = path.join(destDir, 'subset-stats.json');
916
+ fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2));
917
+
918
+ this.module.logInfo(`Subset statistics written to ${statsPath}`);
919
+ }
920
+
921
+ async countLines(filePath) {
922
+ return new Promise((resolve, reject) => {
923
+ let lineCount = 0;
924
+ const rl = readline.createInterface({
925
+ input: fs.createReadStream(filePath),
926
+ crlfDelay: Infinity
927
+ });
928
+
929
+ rl.on('line', () => lineCount++);
930
+ rl.on('close', () => resolve(lineCount));
931
+ rl.on('error', reject);
932
+ });
933
+ }
934
+ }
935
+
936
+ module.exports = {
937
+ RxNormSubsetModule
938
+ };