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,1088 @@
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
+
7
+ class NdcModule extends BaseTerminologyModule {
8
+ getName() {
9
+ return 'ndc';
10
+ }
11
+
12
+ getDescription() {
13
+ return 'National Drug Code (NDC) Directory from FDA';
14
+ }
15
+
16
+ getSupportedFormats() {
17
+ return ['txt', 'tsv', 'directory'];
18
+ }
19
+
20
+ getEstimatedDuration() {
21
+ return '30-90 minutes (depending on number of versions)';
22
+ }
23
+
24
+ registerCommands(terminologyCommand, globalOptions) {
25
+ // Import command
26
+ terminologyCommand
27
+ .command('import')
28
+ .description('Import NDC data from multiple version snapshots')
29
+ .option('-s, --source <directory>', 'Source directory containing version subdirectories')
30
+ .option('-d, --dest <file>', 'Destination SQLite database')
31
+ .option('-v, --version <version>', 'Data version identifier')
32
+ .option('-y, --yes', 'Skip confirmations')
33
+ .option('--no-indexes', 'Skip index creation for faster import')
34
+ .option('--products-only', 'Import only products (skip packages)')
35
+ .option('--packages-only', 'Import only packages (requires existing products)')
36
+ .action(async (options) => {
37
+ await this.handleImportCommand({...globalOptions, ...options});
38
+ });
39
+
40
+ // Validate command
41
+ terminologyCommand
42
+ .command('validate')
43
+ .description('Validate NDC source directory structure')
44
+ .option('-s, --source <directory>', 'Source directory to validate')
45
+ .option('--sample <lines>', 'Number of lines to sample per file', '100')
46
+ .action(async (options) => {
47
+ await this.handleValidateCommand({...globalOptions, ...options});
48
+ });
49
+
50
+ // Status command
51
+ terminologyCommand
52
+ .command('status')
53
+ .description('Show status of NDC database')
54
+ .option('-d, --dest <file>', 'Database file to check')
55
+ .action(async (options) => {
56
+ await this.handleStatusCommand({...globalOptions, ...options});
57
+ });
58
+
59
+ // List versions command
60
+ terminologyCommand
61
+ .command('versions')
62
+ .description('List available NDC versions in source directory')
63
+ .option('-s, --source <directory>', 'Source directory to scan')
64
+ .action(async (options) => {
65
+ await this.handleVersionsCommand({...globalOptions, ...options});
66
+ });
67
+ }
68
+
69
+ async handleImportCommand(options) {
70
+ // Gather configuration
71
+ const config = await this.gatherCommonConfig(options);
72
+
73
+ // NDC-specific configuration
74
+ if (options.noIndexes) {
75
+ config.createIndexes = false;
76
+ }
77
+ config.productsOnly = options.productsOnly || false;
78
+ config.packagesOnly = options.packagesOnly || false;
79
+
80
+ // Show confirmation unless --yes is specified
81
+ if (!options.yes) {
82
+ const confirmed = await this.confirmImport(config);
83
+ if (!confirmed) {
84
+ this.logInfo('Import cancelled');
85
+ return;
86
+ }
87
+ }
88
+
89
+ // Run the import
90
+ await this.runImport(config);
91
+ }
92
+
93
+ async confirmImport(config) {
94
+ const inquirer = require('inquirer');
95
+ const chalk = require('chalk');
96
+
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')}`);
106
+
107
+ if (config.estimatedDuration) {
108
+ console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
109
+ }
110
+
111
+ const { confirmed } = await inquirer.prompt({
112
+ type: 'confirm',
113
+ name: 'confirmed',
114
+ message: 'Proceed with import?',
115
+ default: true
116
+ });
117
+
118
+ return confirmed;
119
+ }
120
+
121
+ async handleValidateCommand(options) {
122
+ if (!options.source) {
123
+ const answers = await require('inquirer').prompt({
124
+ type: 'input',
125
+ name: 'source',
126
+ message: 'Source directory to validate:',
127
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
128
+ });
129
+ options.source = answers.source;
130
+ }
131
+
132
+ this.logInfo(`Validating NDC directory: ${options.source}`);
133
+
134
+ try {
135
+ const stats = await this.validateNdcDirectory(options.source, parseInt(options.sample));
136
+
137
+ 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(', ')}`);
142
+
143
+ if (stats.warnings.length > 0) {
144
+ this.logWarning('Validation warnings:');
145
+ stats.warnings.forEach(warning => console.log(` ${warning}`));
146
+ }
147
+
148
+ } catch (error) {
149
+ this.logError(`Validation failed: ${error.message}`);
150
+ }
151
+ }
152
+
153
+ async handleVersionsCommand(options) {
154
+ if (!options.source) {
155
+ const answers = await require('inquirer').prompt({
156
+ type: 'input',
157
+ name: 'source',
158
+ message: 'Source directory to scan:',
159
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
160
+ });
161
+ options.source = answers.source;
162
+ }
163
+
164
+ try {
165
+ const versions = await this.findVersions(options.source);
166
+
167
+ this.logSuccess(`Found ${versions.length} NDC versions:`);
168
+ versions.forEach((version, index) => {
169
+ console.log(` ${index + 1}. ${version}`);
170
+ });
171
+
172
+ } catch (error) {
173
+ this.logError(`Failed to scan versions: ${error.message}`);
174
+ }
175
+ }
176
+
177
+ async handleStatusCommand(options) {
178
+ const dbPath = options.dest || './data/ndc.db';
179
+
180
+ if (!fs.existsSync(dbPath)) {
181
+ this.logError(`Database not found: ${dbPath}`);
182
+ return;
183
+ }
184
+
185
+ this.logInfo(`Checking NDC database: ${dbPath}`);
186
+
187
+ try {
188
+ const stats = await this.getDatabaseStats(dbPath);
189
+
190
+ 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}`);
198
+
199
+ } catch (error) {
200
+ this.logError(`Status check failed: ${error.message}`);
201
+ }
202
+ }
203
+
204
+ async validatePrerequisites(config) {
205
+ const baseValid = await super.validatePrerequisites(config);
206
+
207
+ // NDC-specific validation
208
+ try {
209
+ this.logInfo('Validating NDC directory structure...');
210
+ await this.validateNdcDirectory(config.source, 10);
211
+ this.logSuccess('NDC directory structure valid');
212
+ } catch (error) {
213
+ this.logError(`NDC directory validation failed: ${error.message}`);
214
+ return false;
215
+ }
216
+
217
+ return baseValid;
218
+ }
219
+
220
+ async executeImport(config) {
221
+ this.logInfo('Starting NDC data migration...');
222
+
223
+ // Create enhanced migrator with progress reporting
224
+ const enhancedMigrator = new NdcDataMigratorWithProgress(
225
+ this,
226
+ config.verbose
227
+ );
228
+
229
+ await enhancedMigrator.migrate(
230
+ config.source,
231
+ config.dest,
232
+ config.version,
233
+ {
234
+ verbose: config.verbose,
235
+ productsOnly: config.productsOnly,
236
+ packagesOnly: config.packagesOnly
237
+ }
238
+ );
239
+
240
+ if (config.createIndexes) {
241
+ this.logInfo('Creating database indexes...');
242
+ await this.createIndexes(config.dest);
243
+ this.logSuccess('Indexes created');
244
+ }
245
+ }
246
+
247
+ async findVersions(sourceDir) {
248
+ if (!fs.existsSync(sourceDir)) {
249
+ throw new Error(`Source directory not found: ${sourceDir}`);
250
+ }
251
+
252
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
253
+ const versions = entries
254
+ .filter(entry => entry.isDirectory())
255
+ .map(entry => entry.name)
256
+ .sort();
257
+
258
+ return versions;
259
+ }
260
+
261
+ async validateNdcDirectory(sourceDir, sampleLines = 100) {
262
+ const versions = await this.findVersions(sourceDir);
263
+
264
+ if (versions.length === 0) {
265
+ throw new Error('No version subdirectories found');
266
+ }
267
+
268
+ let totalProducts = 0;
269
+ let totalPackages = 0;
270
+ const warnings = [];
271
+
272
+ for (const version of versions) {
273
+ const versionDir = path.join(sourceDir, version);
274
+ const productFile = path.join(versionDir, 'product.txt');
275
+ const packageFile = path.join(versionDir, 'package.txt');
276
+
277
+ if (!fs.existsSync(productFile)) {
278
+ warnings.push(`Missing product.txt in version ${version}`);
279
+ continue;
280
+ }
281
+
282
+ if (!fs.existsSync(packageFile)) {
283
+ warnings.push(`Missing package.txt in version ${version}`);
284
+ }
285
+
286
+ // Sample product file
287
+ try {
288
+ const productStats = await this.validateNdcFile(productFile, sampleLines, 'product');
289
+ totalProducts += productStats.estimatedRecords;
290
+ } catch (error) {
291
+ warnings.push(`Product file validation failed for ${version}: ${error.message}`);
292
+ }
293
+
294
+ // Sample package file if it exists
295
+ if (fs.existsSync(packageFile)) {
296
+ try {
297
+ const packageStats = await this.validateNdcFile(packageFile, sampleLines, 'package');
298
+ totalPackages += packageStats.estimatedRecords;
299
+ } catch (error) {
300
+ warnings.push(`Package file validation failed for ${version}: ${error.message}`);
301
+ }
302
+ }
303
+ }
304
+
305
+ return {
306
+ versions,
307
+ totalProducts,
308
+ totalPackages,
309
+ warnings
310
+ };
311
+ }
312
+
313
+ async validateNdcFile(filePath, sampleLines, fileType) {
314
+ const rl = readline.createInterface({
315
+ input: fs.createReadStream(filePath),
316
+ crlfDelay: Infinity
317
+ });
318
+
319
+ let lineCount = 0;
320
+ let sampleCount = 0;
321
+ let headerFound = false;
322
+ let estimatedRecords = 0;
323
+
324
+ const requiredFields = fileType === 'product'
325
+ ? ['PRODUCTNDC', 'PRODUCTTYPENAME', 'PROPRIETARYNAME']
326
+ : ['PRODUCTNDC', 'NDCPACKAGECODE', 'PACKAGEDESCRIPTION'];
327
+
328
+ for await (const line of rl) {
329
+ lineCount++;
330
+
331
+ if (lineCount === 1) {
332
+ // Check header
333
+ const header = line.split('\t');
334
+ const hasRequiredFields = requiredFields.every(field =>
335
+ header.some(h => h.toUpperCase().includes(field))
336
+ );
337
+
338
+ if (hasRequiredFields) {
339
+ headerFound = true;
340
+ } else {
341
+ throw new Error(`Missing required fields in ${fileType} file header`);
342
+ }
343
+ continue;
344
+ }
345
+
346
+ if (sampleCount < sampleLines) {
347
+ const cols = line.split('\t');
348
+ if (cols.length < 3) {
349
+ throw new Error(`Insufficient columns at line ${lineCount}`);
350
+ }
351
+ sampleCount++;
352
+ }
353
+
354
+ estimatedRecords++;
355
+ }
356
+
357
+ if (!headerFound) {
358
+ throw new Error(`No valid header found in ${fileType} file`);
359
+ }
360
+
361
+ return {
362
+ totalLines: lineCount,
363
+ estimatedRecords,
364
+ formatValid: true
365
+ };
366
+ }
367
+
368
+ async getDatabaseStats(dbPath) {
369
+ const sqlite3 = require('sqlite3').verbose();
370
+ const db = new sqlite3.Database(dbPath);
371
+
372
+ return new Promise((resolve, reject) => {
373
+ const stats = {};
374
+
375
+ // Get versions
376
+ db.all('SELECT Version FROM NDCVersion', (err, rows) => {
377
+ if (err) return reject(err);
378
+ stats.versions = rows.map(row => row.Version);
379
+
380
+ // Get counts
381
+ const queries = [
382
+ { name: 'productCount', sql: 'SELECT COUNT(*) as count FROM NDCProducts' },
383
+ { name: 'packageCount', sql: 'SELECT COUNT(*) as count FROM NDCPackages' },
384
+ { name: 'orgCount', sql: 'SELECT COUNT(*) as count FROM NDCOrganizations' },
385
+ { name: 'typeCount', sql: 'SELECT COUNT(*) as count FROM NDCProductTypes' }
386
+ ];
387
+
388
+ let completed = 0;
389
+
390
+ queries.forEach(query => {
391
+ db.get(query.sql, (err, row) => {
392
+ if (err) return reject(err);
393
+ stats[query.name] = row.count;
394
+ completed++;
395
+
396
+ if (completed === queries.length) {
397
+ // Get file stats
398
+ const fileStat = fs.statSync(dbPath);
399
+ stats.sizeGB = fileStat.size / (1024 * 1024 * 1024);
400
+ stats.lastModified = fileStat.mtime.toISOString();
401
+
402
+ db.close();
403
+ resolve(stats);
404
+ }
405
+ });
406
+ });
407
+ });
408
+ });
409
+ }
410
+
411
+ async createIndexes(dbPath) {
412
+ const sqlite3 = require('sqlite3').verbose();
413
+ const db = new sqlite3.Database(dbPath);
414
+
415
+ const indexes = [
416
+ 'CREATE INDEX IF NOT EXISTS idx_ndcproducts_code ON NDCProducts(Code)',
417
+ 'CREATE INDEX IF NOT EXISTS idx_ndcproducts_active ON NDCProducts(Active)',
418
+ 'CREATE INDEX IF NOT EXISTS idx_ndcproducts_type ON NDCProducts(Type)',
419
+ 'CREATE INDEX IF NOT EXISTS idx_ndcproducts_company ON NDCProducts(Company)',
420
+ 'CREATE INDEX IF NOT EXISTS idx_ndcpackages_code ON NDCPackages(Code)',
421
+ 'CREATE INDEX IF NOT EXISTS idx_ndcpackages_code11 ON NDCPackages(Code11)',
422
+ 'CREATE INDEX IF NOT EXISTS idx_ndcpackages_productkey ON NDCPackages(ProductKey)',
423
+ 'CREATE INDEX IF NOT EXISTS idx_ndcpackages_active ON NDCPackages(Active)'
424
+ ];
425
+
426
+ return new Promise((resolve, reject) => {
427
+ db.serialize(() => {
428
+ indexes.forEach(sql => {
429
+ db.run(sql, (err) => {
430
+ if (err) console.warn(`Index creation warning: ${err.message}`);
431
+ });
432
+ });
433
+ });
434
+
435
+ db.close((err) => {
436
+ if (err) reject(err);
437
+ else resolve();
438
+ });
439
+ });
440
+ }
441
+ }
442
+
443
+ class NdcDataMigrator {
444
+ constructor(progressCallback = null) {
445
+ this.progressCallback = progressCallback;
446
+ this.currentProgress = 0;
447
+ }
448
+
449
+ async migrate(sourceDir, destFile = 'unknown', options = {}) {
450
+ if (options.verbose) console.log('Starting NDC data migration...');
451
+
452
+ // Ensure destination directory exists
453
+ const destDir = path.dirname(destFile);
454
+ if (!fs.existsSync(destDir)) {
455
+ fs.mkdirSync(destDir, { recursive: true });
456
+ }
457
+
458
+ // Remove existing database file if it exists
459
+ if (fs.existsSync(destFile)) {
460
+ fs.unlinkSync(destFile);
461
+ }
462
+
463
+ // Create new SQLite database
464
+ const db = new sqlite3.Database(destFile);
465
+
466
+ try {
467
+ // Create tables
468
+ await this.#createTables(db, options.verbose);
469
+
470
+ // Find and process all versions
471
+ const versions = await this.#findVersions(sourceDir);
472
+
473
+ if (options.verbose) {
474
+ console.log(`Found ${versions.length} versions: ${versions.join(', ')}`);
475
+ }
476
+
477
+ // Process each version
478
+ for (const versionName of versions) {
479
+ if (options.verbose) console.log(`Processing version: ${versionName}`);
480
+
481
+ await this.#processVersion(db, sourceDir, versionName, options);
482
+ }
483
+
484
+ // Record only the latest version (matches Pascal behavior)
485
+ if (this.latestVersion) {
486
+ await this.#recordVersion(db, this.latestVersion);
487
+ }
488
+
489
+ // Create lookup tables
490
+ await this.#createLookupTables(db, options.verbose);
491
+
492
+ if (options.verbose) console.log('NDC data migration completed successfully');
493
+ } finally {
494
+ await this.#closeDatabase(db, options.verbose);
495
+ }
496
+ }
497
+
498
+ updateProgress(amount = 1) {
499
+ this.currentProgress += amount;
500
+ if (this.progressCallback) {
501
+ this.progressCallback(this.currentProgress);
502
+ }
503
+ }
504
+
505
+ async #findVersions(sourceDir) {
506
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
507
+ return entries
508
+ .filter(entry => entry.isDirectory())
509
+ .map(entry => entry.name)
510
+ .sort();
511
+ }
512
+
513
+ async #createTables(db, verbose = true) {
514
+ if (verbose) console.log('Creating database tables...');
515
+
516
+ const tableSQL = [
517
+ `CREATE TABLE NDCProducts (
518
+ NDCKey INTEGER NOT NULL PRIMARY KEY,
519
+ Code TEXT(11) NOT NULL,
520
+ LastSeen TEXT(20) NULL,
521
+ Active INTEGER NOT NULL,
522
+ Type INTEGER NOT NULL,
523
+ TradeName TEXT(255) NOT NULL,
524
+ Suffix TEXT(180) NOT NULL,
525
+ DoseForm INTEGER NOT NULL,
526
+ Route INTEGER NOT NULL,
527
+ StartDate TEXT(20) NULL,
528
+ EndDate TEXT(20) NULL,
529
+ Category TEXT(40) NOT NULL,
530
+ Company INTEGER NOT NULL,
531
+ Generics TEXT NULL
532
+ )`,
533
+
534
+ `CREATE TABLE NDCPackages (
535
+ NDCKey INTEGER NOT NULL PRIMARY KEY,
536
+ ProductKey INTEGER NOT NULL,
537
+ Code TEXT(12) NOT NULL,
538
+ Code11 TEXT(11) NOT NULL,
539
+ LastSeen TEXT(20) NULL,
540
+ Active INTEGER NOT NULL,
541
+ Description TEXT(255) NOT NULL
542
+ )`,
543
+
544
+ `CREATE TABLE NDCProductTypes (
545
+ NDCKey INTEGER NOT NULL PRIMARY KEY,
546
+ Name TEXT(255) NOT NULL
547
+ )`,
548
+
549
+ `CREATE TABLE NDCOrganizations (
550
+ NDCKey INTEGER NOT NULL PRIMARY KEY,
551
+ Name TEXT(500) NOT NULL
552
+ )`,
553
+
554
+ `CREATE TABLE NDCDoseForms (
555
+ NDCKey INTEGER NOT NULL PRIMARY KEY,
556
+ Name TEXT(255) NOT NULL
557
+ )`,
558
+
559
+ `CREATE TABLE NDCRoutes (
560
+ NDCKey INTEGER NOT NULL PRIMARY KEY,
561
+ Name TEXT(255) NOT NULL
562
+ )`,
563
+
564
+ `CREATE TABLE NDCVersion (
565
+ Version TEXT(50) NOT NULL
566
+ )`
567
+ ];
568
+
569
+ return new Promise((resolve, reject) => {
570
+ db.serialize(() => {
571
+ tableSQL.forEach(sql => {
572
+ db.run(sql, (err) => {
573
+ if (err) return reject(err);
574
+ });
575
+ });
576
+
577
+ if (verbose) console.log('Database tables created');
578
+ resolve();
579
+ });
580
+ });
581
+ }
582
+
583
+ async #processVersion(db, sourceDir, versionName, options) {
584
+ const versionDir = path.join(sourceDir, versionName);
585
+
586
+ if (!options.packagesOnly) {
587
+ await this.#processProducts(db, versionDir, versionName, options);
588
+ }
589
+
590
+ if (!options.productsOnly) {
591
+ await this.#processPackages(db, versionDir, versionName, options);
592
+ }
593
+
594
+ // Store this as the latest version (will be the last one processed)
595
+ this.latestVersion = versionName;
596
+ }
597
+
598
+ async #processProducts(db, versionDir, versionName, options) {
599
+ const productFile = path.join(versionDir, 'product.txt');
600
+
601
+ if (!fs.existsSync(productFile)) {
602
+ if (options.verbose) console.warn(`Product file not found: ${productFile}`);
603
+ return;
604
+ }
605
+
606
+ if (options.verbose) console.log(`Processing products from ${versionName}...`);
607
+
608
+ const rl = readline.createInterface({
609
+ input: fs.createReadStream(productFile),
610
+ crlfDelay: Infinity
611
+ });
612
+
613
+ let header = null;
614
+ let lineCount = 0;
615
+ let processedCount = 0;
616
+ const batchSize = 1000;
617
+ let batch = [];
618
+
619
+ // Lookup maps for normalization
620
+ const typesMap = new Map();
621
+ const orgsMap = new Map();
622
+ const doseFormsMap = new Map();
623
+ const routesMap = new Map();
624
+ const codesMap = new Map();
625
+
626
+ const insertProduct = db.prepare(`
627
+ INSERT OR REPLACE INTO NDCProducts
628
+ (NDCKey, Code, LastSeen, Active, Type, TradeName, Suffix, DoseForm, Route, StartDate, EndDate, Category, Company, Generics)
629
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
630
+ `);
631
+
632
+ for await (const line of rl) {
633
+ lineCount++;
634
+
635
+ if (lineCount === 1) {
636
+ header = line.split('\t');
637
+ continue;
638
+ }
639
+
640
+ const cols = line.split('\t');
641
+ if (cols.length < 10) continue;
642
+
643
+ const product = this.#parseProductLine(header, cols, versionName);
644
+ if (!product) continue;
645
+
646
+ // Get or create lookup IDs
647
+ product.typeId = this.#getOrCreateLookupId(typesMap, product.productTypeName);
648
+ product.orgId = this.#getOrCreateLookupId(orgsMap, product.labelerName);
649
+ product.doseFormId = this.#getOrCreateLookupId(doseFormsMap, product.dosageFormName);
650
+ product.routeId = this.#getOrCreateLookupId(routesMap, product.routeName);
651
+
652
+ // Generate unique key
653
+ let productKey = codesMap.get(product.code);
654
+ if (!productKey) {
655
+ productKey = codesMap.size + 1;
656
+ codesMap.set(product.code, productKey);
657
+ }
658
+
659
+ batch.push({
660
+ key: productKey,
661
+ ...product
662
+ });
663
+
664
+ if (batch.length >= batchSize) {
665
+ await this._processBatch(db, insertProduct, batch);
666
+ processedCount += batch.length;
667
+ this.updateProgress(batch.length);
668
+ batch = [];
669
+
670
+ if (processedCount % 10000 === 0 && options.verbose) {
671
+ console.log(` Processed ${processedCount} products...`);
672
+ }
673
+ }
674
+ }
675
+
676
+ // Process remaining batch
677
+ if (batch.length > 0) {
678
+ await this._processBatch(db, insertProduct, batch);
679
+ processedCount += batch.length;
680
+ this.updateProgress(batch.length);
681
+ }
682
+
683
+ insertProduct.finalize();
684
+
685
+ // Store lookup maps for later use
686
+ this.typesMap = typesMap;
687
+ this.orgsMap = orgsMap;
688
+ this.doseFormsMap = doseFormsMap;
689
+ this.routesMap = routesMap;
690
+ this.codesMap = codesMap;
691
+
692
+ if (options.verbose) {
693
+ console.log(` Completed products: ${processedCount} records`);
694
+ }
695
+ }
696
+
697
+ async #processPackages(db, versionDir, versionName, options) {
698
+ const packageFile = path.join(versionDir, 'package.txt');
699
+
700
+ if (!fs.existsSync(packageFile)) {
701
+ if (options.verbose) console.warn(`Package file not found: ${packageFile}`);
702
+ return;
703
+ }
704
+
705
+ if (options.verbose) console.log(`Processing packages from ${versionName}...`);
706
+
707
+ const rl = readline.createInterface({
708
+ input: fs.createReadStream(packageFile),
709
+ crlfDelay: Infinity
710
+ });
711
+
712
+ let header = null;
713
+ let lineCount = 0;
714
+ let processedCount = 0;
715
+ const batchSize = 1000;
716
+ let batch = [];
717
+
718
+ const packageCodesMap = new Map();
719
+
720
+ const insertPackage = db.prepare(`
721
+ INSERT OR REPLACE INTO NDCPackages
722
+ (NDCKey, ProductKey, Code, Code11, LastSeen, Active, Description)
723
+ VALUES (?, ?, ?, ?, ?, ?, ?)
724
+ `);
725
+
726
+ for await (const line of rl) {
727
+ lineCount++;
728
+
729
+ if (lineCount === 1) {
730
+ header = line.split('\t');
731
+ continue;
732
+ }
733
+
734
+ const cols = line.split('\t');
735
+ if (cols.length < 4) continue;
736
+
737
+ const packageData = this.#parsePackageLine(header, cols, versionName);
738
+ if (!packageData) continue;
739
+
740
+ // Get product key
741
+ const productKey = this.codesMap?.get(packageData.productCode);
742
+ if (!productKey) {
743
+ continue; // Skip packages without corresponding products
744
+ }
745
+
746
+ // Generate unique package key
747
+ let packageKey = packageCodesMap.get(packageData.code);
748
+ if (!packageKey) {
749
+ packageKey = packageCodesMap.size + 1;
750
+ packageCodesMap.set(packageData.code, packageKey);
751
+ }
752
+
753
+ batch.push({
754
+ key: packageKey,
755
+ productKey,
756
+ ...packageData
757
+ });
758
+
759
+ if (batch.length >= batchSize) {
760
+ await this._processBatch(db, insertPackage, batch, 'package');
761
+ processedCount += batch.length;
762
+ this.updateProgress(batch.length);
763
+ batch = [];
764
+
765
+ if (processedCount % 10000 === 0 && options.verbose) {
766
+ console.log(` Processed ${processedCount} packages...`);
767
+ }
768
+ }
769
+ }
770
+
771
+ // Process remaining batch
772
+ if (batch.length > 0) {
773
+ await this._processBatch(db, insertPackage, batch, 'package');
774
+ processedCount += batch.length;
775
+ this.updateProgress(batch.length);
776
+ }
777
+
778
+ insertPackage.finalize();
779
+
780
+ if (options.verbose) {
781
+ console.log(` Completed packages: ${processedCount} records`);
782
+ }
783
+ }
784
+
785
+ #parseProductLine(header, cols, versionName) {
786
+ const getField = (fieldName) => {
787
+ const index = header.findIndex(h => h.toUpperCase().includes(fieldName.toUpperCase()));
788
+ return index >= 0 && index < cols.length ? cols[index].trim() : '';
789
+ };
790
+
791
+ const code = getField('PRODUCTNDC');
792
+ if (!code || code.length > 11) return null;
793
+
794
+ const excludeFlag = getField('NDC_EXCLUDE_FLAG');
795
+ let active;
796
+ if (excludeFlag) {
797
+ // If field exists and has data, active when flag is 'Y' or 'N'
798
+ // Other values like 'E', 'U', 'I' mark as inactive
799
+ active = (excludeFlag === 'Y' || excludeFlag === 'N') ? 1 : 0;
800
+ } else {
801
+ // If field doesn't exist or is empty, default to active
802
+ active = 1;
803
+ }
804
+
805
+ return {
806
+ code,
807
+ active,
808
+ productTypeName: getField('PRODUCTTYPENAME') || '',
809
+ proprietaryName: getField('PROPRIETARYNAME') || '',
810
+ proprietaryNameSuffix: getField('PROPRIETARYNAMESUFFIX') || '',
811
+ nonProprietaryName: getField('NONPROPRIETARYNAME') || '',
812
+ dosageFormName: getField('DOSAGEFORMNAME') || '',
813
+ routeName: getField('ROUTENAME') || '',
814
+ startMarketingDate: this.#parseDate(getField('STARTMARKETINGDATE')),
815
+ endMarketingDate: this.#parseDate(this.#fixEndDate(getField('ENDMARKETINGDATE'))),
816
+ marketingCategoryName: getField('MARKETINGCATEGORYNAME') || '',
817
+ labelerName: getField('LABELERNAME') || '',
818
+ lastSeen: versionName
819
+ };
820
+ }
821
+
822
+ #parsePackageLine(header, cols, versionName) {
823
+ const getField = (fieldName) => {
824
+ const index = header.findIndex(h => h.toUpperCase().includes(fieldName.toUpperCase()));
825
+ return index >= 0 && index < cols.length ? cols[index].trim() : '';
826
+ };
827
+
828
+ const code = getField('NDCPACKAGECODE');
829
+ const productCode = getField('PRODUCTNDC');
830
+
831
+ if (!code || !productCode || code.length > 12) return null;
832
+
833
+ const excludeFlag = getField('NDC_EXCLUDE_FLAG');
834
+ let active;
835
+ if (excludeFlag) {
836
+ // If field exists and has data, active when flag is 'Y' or 'N'
837
+ // Other values like 'E', 'U', 'I' mark as inactive
838
+ active = (excludeFlag === 'Y' || excludeFlag === 'N') ? 1 : 0;
839
+ } else {
840
+ // If field doesn't exist or is empty, default to active
841
+ active = 1;
842
+ }
843
+
844
+ return {
845
+ code,
846
+ productCode,
847
+ code11: this.#genCode11(code),
848
+ active,
849
+ description: getField('PACKAGEDESCRIPTION') || '',
850
+ lastSeen: versionName
851
+ };
852
+ }
853
+
854
+ #getOrCreateLookupId(map, value) {
855
+ if (!value) return 1; // Default/unknown entry
856
+
857
+ if (map.has(value)) {
858
+ return map.get(value);
859
+ }
860
+
861
+ const id = map.size + 1;
862
+ map.set(value, id);
863
+ return id;
864
+ }
865
+
866
+ #parseDate(dateStr) {
867
+ if (!dateStr) return null;
868
+
869
+ // Fix known date issues
870
+ let fixed = this.#fixDate(dateStr);
871
+
872
+ // Try different formats
873
+ if (fixed.includes('/')) {
874
+ const parts = fixed.split('/');
875
+ if (parts.length === 3) {
876
+ // MM/DD/YYYY
877
+ return `${parts[2]}-${parts[0].padStart(2, '0')}-${parts[1].padStart(2, '0')}`;
878
+ }
879
+ }
880
+
881
+ if (fixed.includes('-')) {
882
+ return fixed; // Assume already in ISO format
883
+ }
884
+
885
+ return fixed;
886
+ }
887
+
888
+ #fixDate(date) {
889
+ if (date.startsWith('2388')) {
890
+ return '2018' + date.substring(4);
891
+ } else if (date.startsWith('3030')) {
892
+ return '2020' + date.substring(4);
893
+ }
894
+ return date;
895
+ }
896
+
897
+ #fixEndDate(date) {
898
+ if (date.startsWith('3031')) {
899
+ return '2031' + date.substring(4);
900
+ }
901
+ return this.#fixDate(date);
902
+ }
903
+
904
+ #genCode11(code) {
905
+ if (!code) return '';
906
+
907
+ const parts = code.split('-');
908
+ if (parts.length === 3) {
909
+ return parts[0].padStart(5, '0') +
910
+ parts[1].padStart(4, '0') +
911
+ parts[2].padStart(2, '0');
912
+ }
913
+
914
+ return code.replace(/-/g, '').padStart(11, '0');
915
+ }
916
+
917
+ async _processBatch(db, statement, batch, type = 'product') {
918
+ return new Promise((resolve, reject) => {
919
+ db.serialize(() => {
920
+ db.run('BEGIN TRANSACTION');
921
+
922
+ batch.forEach(item => {
923
+ if (type === 'product') {
924
+ statement.run(
925
+ item.key,
926
+ item.code,
927
+ item.lastSeen,
928
+ item.active,
929
+ item.typeId,
930
+ item.proprietaryName.substring(0, 255),
931
+ item.proprietaryNameSuffix.substring(0, 180),
932
+ item.doseFormId,
933
+ item.routeId,
934
+ item.startMarketingDate,
935
+ item.endMarketingDate,
936
+ item.marketingCategoryName.substring(0, 40),
937
+ item.orgId,
938
+ item.nonProprietaryName
939
+ );
940
+ } else {
941
+ statement.run(
942
+ item.key,
943
+ item.productKey,
944
+ item.code,
945
+ item.code11,
946
+ item.lastSeen,
947
+ item.active,
948
+ item.description.substring(0, 255)
949
+ );
950
+ }
951
+ });
952
+
953
+ db.run('COMMIT', (err) => {
954
+ if (err) reject(err);
955
+ else resolve();
956
+ });
957
+ });
958
+ });
959
+ }
960
+
961
+ async #recordVersion(db, versionName) {
962
+ return new Promise((resolve, reject) => {
963
+ db.run('INSERT INTO NDCVersion (Version) VALUES (?)', [versionName], (err) => {
964
+ if (err) reject(err);
965
+ else resolve();
966
+ });
967
+ });
968
+ }
969
+
970
+ async #createLookupTables(db, verbose = true) {
971
+ if (verbose) console.log('Creating lookup tables...');
972
+
973
+ const lookups = [
974
+ { table: 'NDCProductTypes', map: this.typesMap },
975
+ { table: 'NDCOrganizations', map: this.orgsMap },
976
+ { table: 'NDCDoseForms', map: this.doseFormsMap },
977
+ { table: 'NDCRoutes', map: this.routesMap }
978
+ ];
979
+
980
+ return new Promise((resolve) => {
981
+ db.serialize(() => {
982
+ lookups.forEach(lookup => {
983
+ if (lookup.map) {
984
+ for (const [name, id] of lookup.map) {
985
+ db.run(
986
+ `INSERT INTO ${lookup.table} (NDCKey, Name) VALUES (?, ?)`,
987
+ [id, name.substring(0, 500)],
988
+ (err) => {
989
+ if (err && verbose) {
990
+ console.warn(`Warning inserting into ${lookup.table}: ${err.message}`);
991
+ }
992
+ }
993
+ );
994
+ }
995
+ }
996
+ });
997
+
998
+ if (verbose) console.log('Lookup tables created');
999
+ resolve();
1000
+ });
1001
+ });
1002
+ }
1003
+
1004
+ async #closeDatabase(db, verbose = true) {
1005
+ return new Promise((resolve) => {
1006
+ db.close((err) => {
1007
+ if (err && verbose) {
1008
+ console.error('Error closing database:', err);
1009
+ }
1010
+ resolve();
1011
+ });
1012
+ });
1013
+ }
1014
+ }
1015
+
1016
+ // Enhanced migrator with progress reporting
1017
+ class NdcDataMigratorWithProgress {
1018
+ constructor(moduleInstance, verbose = true) {
1019
+ this.module = moduleInstance;
1020
+ this.verbose = verbose;
1021
+ this.totalProgress = 0;
1022
+ }
1023
+
1024
+ async migrate(sourceDir, destFile, version, options) {
1025
+ // Estimate total work by counting lines in all files
1026
+ const versions = await this.countVersions(sourceDir);
1027
+ this.totalProgress = await this.estimateWorkload(sourceDir, versions);
1028
+
1029
+ this.module.logInfo(`Processing ${versions.length} NDC versions (${this.totalProgress.toLocaleString()} estimated records)...`);
1030
+ this.module.createProgressBar();
1031
+ this.module.updateProgress(0, this.totalProgress);
1032
+
1033
+ // Create migrator with progress callback
1034
+ const migratorWithProgress = new NdcDataMigrator((currentProgress) => {
1035
+ this.module.updateProgress(currentProgress);
1036
+ });
1037
+
1038
+ try {
1039
+ await migratorWithProgress.migrate(sourceDir, destFile, version, options);
1040
+ } finally {
1041
+ this.module.stopProgress();
1042
+ }
1043
+ }
1044
+
1045
+ async countVersions(sourceDir) {
1046
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
1047
+ return entries.filter(entry => entry.isDirectory()).map(entry => entry.name);
1048
+ }
1049
+
1050
+ async estimateWorkload(sourceDir, versions) {
1051
+ let totalLines = 0;
1052
+
1053
+ for (const version of versions) {
1054
+ const versionDir = path.join(sourceDir, version);
1055
+ const productFile = path.join(versionDir, 'product.txt');
1056
+ const packageFile = path.join(versionDir, 'package.txt');
1057
+
1058
+ if (fs.existsSync(productFile)) {
1059
+ totalLines += await this.countLines(productFile);
1060
+ }
1061
+
1062
+ if (fs.existsSync(packageFile)) {
1063
+ totalLines += await this.countLines(packageFile);
1064
+ }
1065
+ }
1066
+
1067
+ return Math.max(totalLines - versions.length * 2, 1); // Subtract headers
1068
+ }
1069
+
1070
+ async countLines(filePath) {
1071
+ return new Promise((resolve, reject) => {
1072
+ let lineCount = 0;
1073
+ const rl = require('readline').createInterface({
1074
+ input: fs.createReadStream(filePath),
1075
+ crlfDelay: Infinity
1076
+ });
1077
+
1078
+ rl.on('line', () => lineCount++);
1079
+ rl.on('close', () => resolve(lineCount));
1080
+ rl.on('error', reject);
1081
+ });
1082
+ }
1083
+ }
1084
+
1085
+ module.exports = {
1086
+ NdcModule,
1087
+ NdcDataMigrator
1088
+ };