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,1536 @@
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 LoincModule extends BaseTerminologyModule {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ getName() {
14
+ return 'loinc';
15
+ }
16
+
17
+ getDescription() {
18
+ return 'Logical Observation Identifiers Names and Codes (LOINC) from Regenstrief Institute';
19
+ }
20
+
21
+ getSupportedFormats() {
22
+ return ['csv', 'directory'];
23
+ }
24
+
25
+ getDefaultConfig() {
26
+ return {
27
+ verbose: true,
28
+ overwrite: false,
29
+ createIndexes: true,
30
+ mainOnly: false,
31
+ dest: './data/loinc.db'
32
+ };
33
+ }
34
+
35
+ getEstimatedDuration() {
36
+ return '45-120 minutes (depending on language variants)';
37
+ }
38
+
39
+ registerCommands(terminologyCommand, globalOptions) {
40
+ // Import command
41
+ terminologyCommand
42
+ .command('import')
43
+ .description('Import LOINC data from source directory')
44
+ .option('-s, --source <directory>', 'Source directory containing LOINC files')
45
+ .option('-d, --dest <file>', 'Destination SQLite database')
46
+ .option('-v, --version <version>', 'LOINC version identifier')
47
+ .option('-y, --yes', 'Skip confirmations')
48
+ .option('--no-indexes', 'Skip index creation for faster import')
49
+ .option('--main-only', 'Import only main codes (skip language variants)')
50
+ .action(async (options) => {
51
+ await this.handleImportCommand({...globalOptions, ...options});
52
+ });
53
+
54
+ // Validate command
55
+ terminologyCommand
56
+ .command('validate')
57
+ .description('Validate LOINC source directory structure')
58
+ .option('-s, --source <directory>', 'Source directory to validate')
59
+ .action(async (options) => {
60
+ await this.handleValidateCommand({...globalOptions, ...options});
61
+ });
62
+
63
+ // Status command
64
+ terminologyCommand
65
+ .command('status')
66
+ .description('Show status of LOINC database')
67
+ .option('-d, --dest <file>', 'Database file to check')
68
+ .action(async (options) => {
69
+ await this.handleStatusCommand({...globalOptions, ...options});
70
+ });
71
+ }
72
+
73
+ async handleImportCommand(options) {
74
+ try {
75
+ // Gather configuration with remembered values
76
+ const config = await this.gatherCommonConfig(options);
77
+
78
+ // LOINC-specific configuration
79
+ config.createIndexes = !options.noIndexes;
80
+ config.mainOnly = options.mainOnly || false;
81
+ config.estimatedDuration = this.getEstimatedDuration();
82
+
83
+ if (!options.version) {
84
+ const inquirer = require('inquirer');
85
+ const { version } = await inquirer.prompt({
86
+ type: 'input',
87
+ name: 'version',
88
+ message: 'LOINC version identifier:',
89
+ default: config.version
90
+ });
91
+ config.version = version;
92
+ }
93
+
94
+ // Show confirmation unless --yes is specified
95
+ if (!options.yes) {
96
+ const confirmed = await this.confirmImport(config);
97
+ if (!confirmed) {
98
+ this.logInfo('Import cancelled');
99
+ return;
100
+ }
101
+ }
102
+
103
+ // Save configuration immediately after confirmation
104
+ this.rememberSuccessfulConfig(config);
105
+
106
+ // Run the import
107
+ await this.runImportWithoutConfigSaving(config);
108
+ } catch (error) {
109
+ this.logError(`Import command failed: ${error.message}`);
110
+ if (options.verbose) {
111
+ console.error(error.stack);
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ async confirmImport(config) {
118
+ const inquirer = require('inquirer');
119
+ const chalk = require('chalk');
120
+
121
+ console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
122
+ console.log(` Source: ${chalk.white(config.source)}`);
123
+ console.log(` Destination: ${chalk.white(config.dest)}`);
124
+ console.log(` Version: ${chalk.white(config.version)}`);
125
+ console.log(` Main Only: ${chalk.white(config.mainOnly ? 'Yes' : 'No')}`);
126
+ console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
127
+ console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
128
+ console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
129
+
130
+ if (config.estimatedDuration) {
131
+ console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
132
+ }
133
+
134
+ const { confirmed } = await inquirer.prompt({
135
+ type: 'confirm',
136
+ name: 'confirmed',
137
+ message: 'Proceed with import?',
138
+ default: true
139
+ });
140
+
141
+ return confirmed;
142
+ }
143
+
144
+ async runImportWithoutConfigSaving(config) {
145
+ try {
146
+ console.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
147
+
148
+ // Pre-flight checks
149
+ this.logInfo('Running pre-flight checks...');
150
+ const prerequisitesPassed = await this.validatePrerequisites(config);
151
+
152
+ if (!prerequisitesPassed) {
153
+ throw new Error('Pre-flight checks failed');
154
+ }
155
+
156
+ // Execute the import
157
+ await this.executeImport(config);
158
+
159
+ this.logSuccess(`${this.getName()} import completed successfully!`);
160
+
161
+ } catch (error) {
162
+ this.stopProgress();
163
+ this.logError(`${this.getName()} import failed: ${error.message}`);
164
+ if (config.verbose) {
165
+ console.error(error.stack);
166
+ }
167
+ process.exit(1);
168
+ }
169
+ }
170
+
171
+ async handleValidateCommand(options) {
172
+ if (!options.source) {
173
+ const answers = await require('inquirer').prompt({
174
+ type: 'input',
175
+ name: 'source',
176
+ message: 'Source directory to validate:',
177
+ validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
178
+ });
179
+ options.source = answers.source;
180
+ }
181
+
182
+ this.logInfo(`Validating LOINC directory: ${options.source}`);
183
+
184
+ try {
185
+ const stats = await this.validateLoincDirectory(options.source);
186
+
187
+ this.logSuccess('Directory validation passed');
188
+ console.log(` Main codes file: ${stats.mainCodesFound ? 'Found' : 'Missing'}`);
189
+ console.log(` Parts file: ${stats.partsFound ? 'Found' : 'Missing'}`);
190
+ console.log(` Language variants: ${stats.languageVariants.length} found`);
191
+ console.log(` Estimated main codes: ${stats.estimatedCodes.toLocaleString()}`);
192
+
193
+ if (stats.warnings.length > 0) {
194
+ this.logWarning('Validation warnings:');
195
+ stats.warnings.forEach(warning => console.log(` ${warning}`));
196
+ }
197
+
198
+ } catch (error) {
199
+ this.logError(`Validation failed: ${error.message}`);
200
+ }
201
+ }
202
+
203
+ async handleStatusCommand(options) {
204
+ const dbPath = options.dest || './data/loinc.db';
205
+
206
+ if (!fs.existsSync(dbPath)) {
207
+ this.logError(`Database not found: ${dbPath}`);
208
+ return;
209
+ }
210
+
211
+ this.logInfo(`Checking LOINC database: ${dbPath}`);
212
+
213
+ try {
214
+ const stats = await this.getDatabaseStats(dbPath);
215
+
216
+ this.logSuccess('Database status:');
217
+ console.log(` Version: ${stats.version}`);
218
+ console.log(` Total Codes: ${stats.codeCount.toLocaleString()}`);
219
+ console.log(` Main LOINC Codes: ${stats.mainCodeCount.toLocaleString()}`);
220
+ console.log(` Parts: ${stats.partCount.toLocaleString()}`);
221
+ console.log(` Answer Lists: ${stats.answerListCount.toLocaleString()}`);
222
+ console.log(` Languages: ${stats.languageCount.toLocaleString()}`);
223
+ console.log(` Relationships: ${stats.relationshipCount.toLocaleString()}`);
224
+ console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
225
+ console.log(` Last Modified: ${stats.lastModified}`);
226
+
227
+ } catch (error) {
228
+ this.logError(`Status check failed: ${error.message}`);
229
+ }
230
+ }
231
+
232
+ async validatePrerequisites(config) {
233
+ const baseValid = await super.validatePrerequisites(config);
234
+
235
+ try {
236
+ this.logInfo('Validating LOINC directory structure...');
237
+ await this.validateLoincDirectory(config.source);
238
+ this.logSuccess('LOINC directory structure valid');
239
+ } catch (error) {
240
+ this.logError(`LOINC directory validation failed: ${error.message}`);
241
+ return false;
242
+ }
243
+
244
+ return baseValid;
245
+ }
246
+
247
+ async executeImport(config) {
248
+ this.logInfo('Starting LOINC data migration...');
249
+
250
+ const enhancedMigrator = new LoincDataMigratorWithProgress(
251
+ this,
252
+ config.verbose
253
+ );
254
+
255
+ await enhancedMigrator.migrate(
256
+ config.source,
257
+ config.dest,
258
+ config.version,
259
+ {
260
+ verbose: config.verbose,
261
+ mainOnly: config.mainOnly
262
+ }
263
+ );
264
+
265
+ if (config.createIndexes) {
266
+ this.logInfo('Creating database indexes...');
267
+ await this.createIndexes(config.dest);
268
+ this.logSuccess('Indexes created');
269
+ }
270
+ }
271
+
272
+ async validateLoincDirectory(sourceDir) {
273
+ if (!fs.existsSync(sourceDir)) {
274
+ throw new Error(`Source directory not found: ${sourceDir}`);
275
+ }
276
+
277
+ const requiredFiles = [
278
+ 'LoincTable/Loinc.csv',
279
+ 'AccessoryFiles/PartFile/Part.csv'
280
+ ];
281
+
282
+ const optionalFiles = [
283
+ 'AccessoryFiles/ConsumerName/ConsumerName.csv',
284
+ 'AccessoryFiles/AnswerFile/AnswerList.csv',
285
+ 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
286
+ 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
287
+ 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
288
+ 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv'
289
+ ];
290
+
291
+ const warnings = [];
292
+ let mainCodesFound = false;
293
+ let partsFound = false;
294
+ let estimatedCodes = 0;
295
+
296
+ // Check required files
297
+ for (const file of requiredFiles) {
298
+ const filePath = path.join(sourceDir, file);
299
+ if (!fs.existsSync(filePath)) {
300
+ throw new Error(`Required file missing: ${file}`);
301
+ }
302
+
303
+ if (file.includes('Loinc.csv')) {
304
+ mainCodesFound = true;
305
+ estimatedCodes = await this.countLines(filePath) - 1;
306
+ } else if (file.includes('Part.csv')) {
307
+ partsFound = true;
308
+ }
309
+ }
310
+
311
+ // Check optional files
312
+ for (const file of optionalFiles) {
313
+ const filePath = path.join(sourceDir, file);
314
+ if (!fs.existsSync(filePath)) {
315
+ warnings.push(`Optional file missing: ${file}`);
316
+ }
317
+ }
318
+
319
+ // Check for language variants
320
+ const languageVariants = [];
321
+ const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
322
+ if (fs.existsSync(linguisticVariantsDir)) {
323
+ const files = fs.readdirSync(linguisticVariantsDir);
324
+ for (const file of files) {
325
+ if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
326
+ languageVariants.push(file);
327
+ }
328
+ }
329
+ }
330
+
331
+ return {
332
+ mainCodesFound,
333
+ partsFound,
334
+ estimatedCodes,
335
+ languageVariants,
336
+ warnings
337
+ };
338
+ }
339
+
340
+ async countLines(filePath) {
341
+ return new Promise((resolve, reject) => {
342
+ let lineCount = 0;
343
+ const rl = readline.createInterface({
344
+ input: fs.createReadStream(filePath),
345
+ crlfDelay: Infinity
346
+ });
347
+
348
+ rl.on('line', () => lineCount++);
349
+ rl.on('close', () => resolve(lineCount));
350
+ rl.on('error', reject);
351
+ });
352
+ }
353
+
354
+ async getDatabaseStats(dbPath) {
355
+ const sqlite3 = require('sqlite3').verbose();
356
+ const db = new sqlite3.Database(dbPath);
357
+
358
+ return new Promise((resolve, reject) => {
359
+ const stats = {};
360
+
361
+ db.get('SELECT Value FROM Config WHERE ConfigKey = 2', (err, row) => {
362
+ if (err) return reject(err);
363
+ stats.version = row ? row.Value : 'Unknown';
364
+
365
+ const queries = [
366
+ { name: 'codeCount', sql: 'SELECT COUNT(*) as count FROM Codes' },
367
+ { name: 'mainCodeCount', sql: 'SELECT COUNT(*) as count FROM Codes WHERE Type = 1' },
368
+ { name: 'partCount', sql: 'SELECT COUNT(*) as count FROM Codes WHERE Type = 2' },
369
+ { name: 'answerListCount', sql: 'SELECT COUNT(*) as count FROM Codes WHERE Type = 3' },
370
+ { name: 'languageCount', sql: 'SELECT COUNT(*) as count FROM Languages' },
371
+ { name: 'relationshipCount', sql: 'SELECT COUNT(*) as count FROM Relationships' }
372
+ ];
373
+
374
+ let completed = 0;
375
+
376
+ queries.forEach(query => {
377
+ db.get(query.sql, (err, row) => {
378
+ if (err) return reject(err);
379
+ stats[query.name] = row.count;
380
+ completed++;
381
+
382
+ if (completed === queries.length) {
383
+ const fileStat = fs.statSync(dbPath);
384
+ stats.sizeGB = fileStat.size / (1024 * 1024 * 1024);
385
+ stats.lastModified = fileStat.mtime.toISOString();
386
+
387
+ db.close();
388
+ resolve(stats);
389
+ }
390
+ });
391
+ });
392
+ });
393
+ });
394
+ }
395
+
396
+ async createIndexes(dbPath) {
397
+ const sqlite3 = require('sqlite3').verbose();
398
+ const db = new sqlite3.Database(dbPath);
399
+
400
+ const indexes = [
401
+ // Codes table indexes
402
+ 'CREATE INDEX IF NOT EXISTS idx_codes_code ON Codes(Code)',
403
+ 'CREATE INDEX IF NOT EXISTS idx_codes_type ON Codes(Type)',
404
+ 'CREATE INDEX IF NOT EXISTS idx_codes_status ON Codes(StatusKey)',
405
+ 'CREATE INDEX IF NOT EXISTS idx_codes_type_status ON Codes(Type, StatusKey)',
406
+
407
+ // Relationships table indexes
408
+ 'CREATE INDEX IF NOT EXISTS idx_relationships_source ON Relationships(RelationshipTypeKey, SourceKey)',
409
+ 'CREATE INDEX IF NOT EXISTS idx_relationships_target ON Relationships(RelationshipTypeKey, TargetKey)',
410
+ 'CREATE INDEX IF NOT EXISTS idx_relationships_source_type ON Relationships(SourceKey, RelationshipTypeKey)',
411
+
412
+ // Properties table indexes
413
+ 'CREATE INDEX IF NOT EXISTS idx_properties_code ON Properties(PropertyTypeKey, CodeKey)',
414
+ 'CREATE INDEX IF NOT EXISTS idx_properties_code2 ON Properties(CodeKey, PropertyTypeKey)',
415
+ 'CREATE INDEX IF NOT EXISTS idx_properties_valuekey ON Properties(PropertyValueKey)',
416
+
417
+ // PropertyValues table indexes - for value lookups in filters
418
+ 'CREATE INDEX IF NOT EXISTS idx_propertyvalues_value ON PropertyValues(Value COLLATE NOCASE)',
419
+
420
+ // Descriptions table indexes
421
+ 'CREATE INDEX IF NOT EXISTS idx_descriptions_code ON Descriptions(CodeKey, LanguageKey)',
422
+ 'CREATE INDEX IF NOT EXISTS idx_descriptions_type ON Descriptions(DescriptionTypeKey)',
423
+ 'CREATE INDEX IF NOT EXISTS idx_descriptions_code_type ON Descriptions(CodeKey, DescriptionTypeKey)',
424
+
425
+ // Closure table indexes
426
+ 'CREATE INDEX IF NOT EXISTS idx_closure_ancestor ON Closure(AncestorKey)',
427
+ 'CREATE INDEX IF NOT EXISTS idx_closure_descendent ON Closure(DescendentKey)'
428
+ ];
429
+
430
+ return new Promise((resolve, reject) => {
431
+ db.serialize(() => {
432
+ indexes.forEach(sql => {
433
+ db.run(sql, (err) => {
434
+ if (err) console.warn(`Index creation warning: ${err.message}`);
435
+ });
436
+ });
437
+ });
438
+
439
+ db.close((err) => {
440
+ if (err) reject(err);
441
+ else resolve();
442
+ });
443
+ });
444
+ }
445
+ }
446
+
447
+ // Enhanced migrator with progress reporting
448
+ class LoincDataMigratorWithProgress {
449
+ constructor(moduleInstance, verbose = true) {
450
+ this.module = moduleInstance;
451
+ this.verbose = verbose;
452
+ this.totalProgress = 0;
453
+ this.currentOperation = 'Starting';
454
+ }
455
+
456
+ async migrate(sourceDir, destFile, version, options) {
457
+ // Estimate total work
458
+ this.totalProgress = await this.estimateWorkload(sourceDir, options);
459
+
460
+ this.module.logInfo(`Processing LOINC data (${this.totalProgress.toLocaleString()} estimated records)...`);
461
+
462
+ // Create progress bar with operation display
463
+ const progressFormat = '{operation} |{bar}| {percentage}% | {value}/{total} | ETA: {eta}s';
464
+ this.module.createProgressBar(progressFormat);
465
+
466
+ // Start the progress bar
467
+ this.module.progressBar.start(this.totalProgress, 0, {
468
+ operation: chalk.cyan('Starting'.padEnd(20).substring(0, 20))
469
+ });
470
+
471
+ // Create migrator with progress callback that properly updates both progress and operation
472
+ const migratorWithProgress = new LoincDataMigrator((currentProgress, operation) => {
473
+ if (operation) {
474
+ this.currentOperation = operation;
475
+ }
476
+
477
+ if (this.module.progressBar) {
478
+ this.module.progressBar.update(currentProgress, {
479
+ operation: chalk.cyan(this.currentOperation.padEnd(20).substring(0, 20))
480
+ });
481
+ }
482
+ });
483
+
484
+ try {
485
+ await migratorWithProgress.migrate(sourceDir, destFile, version, options);
486
+ } finally {
487
+ this.module.stopProgress();
488
+ }
489
+ }
490
+
491
+ async estimateWorkload(sourceDir, options) {
492
+ let totalLines = 0;
493
+ const files = [
494
+ 'LoincTable/Loinc.csv',
495
+ 'AccessoryFiles/PartFile/Part.csv'
496
+ ];
497
+
498
+ const optionalFiles = [
499
+ 'AccessoryFiles/ConsumerName/ConsumerName.csv',
500
+ 'AccessoryFiles/AnswerFile/AnswerList.csv',
501
+ 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
502
+ 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
503
+ 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv'
504
+ ];
505
+
506
+ // Count main files
507
+ for (const file of [...files, ...optionalFiles]) {
508
+ const filePath = path.join(sourceDir, file);
509
+ if (fs.existsSync(filePath)) {
510
+ totalLines += await this.countLines(filePath);
511
+ }
512
+ }
513
+
514
+ // Count language variant files if not main-only
515
+ if (!options.mainOnly) {
516
+ const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
517
+ if (fs.existsSync(linguisticVariantsDir)) {
518
+ const files = fs.readdirSync(linguisticVariantsDir);
519
+ for (const file of files) {
520
+ if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
521
+ const filePath = path.join(linguisticVariantsDir, file);
522
+ totalLines += await this.countLines(filePath);
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ return Math.max(totalLines - 20, 1);
529
+ }
530
+
531
+ async countLines(filePath) {
532
+ return new Promise((resolve, reject) => {
533
+ let lineCount = 0;
534
+ const rl = readline.createInterface({
535
+ input: fs.createReadStream(filePath),
536
+ crlfDelay: Infinity
537
+ });
538
+
539
+ rl.on('line', () => lineCount++);
540
+ rl.on('close', () => resolve(lineCount));
541
+ rl.on('error', reject);
542
+ });
543
+ }
544
+ }
545
+
546
+ class LoincDataMigrator {
547
+ constructor(progressCallback = null) {
548
+ this.progressCallback = progressCallback;
549
+ this.currentProgress = 0;
550
+ this.stepCount = 16;
551
+ this.currentOperation = 'Initializing';
552
+ }
553
+
554
+ updateProgress(amount = 1, operation = null) {
555
+ this.currentProgress += amount;
556
+ if (operation) {
557
+ this.currentOperation = operation;
558
+ }
559
+ if (this.progressCallback) {
560
+ this.progressCallback(this.currentProgress, this.currentOperation);
561
+ }
562
+ }
563
+
564
+ async migrate(sourceDir, destFile, version = 'unknown', options = {}) {
565
+ if (options.verbose) console.log('Starting LOINC data migration...');
566
+
567
+ const destDir = path.dirname(destFile);
568
+ if (!fs.existsSync(destDir)) {
569
+ fs.mkdirSync(destDir, { recursive: true });
570
+ }
571
+
572
+ if (fs.existsSync(destFile)) {
573
+ fs.unlinkSync(destFile);
574
+ }
575
+
576
+ const db = new sqlite3.Database(destFile);
577
+
578
+ try {
579
+ // Initialize tracking variables
580
+ this.codeKey = 0;
581
+ this.relKey = 0;
582
+ this.descKey = 0;
583
+ this.propKey = 0;
584
+ this.propValueKey = 0;
585
+ this.langKey = 1;
586
+
587
+ this.codes = new Map();
588
+ this.codeList = [];
589
+ this.statii = new Map();
590
+ this.langs = new Map();
591
+ this.rels = new Map();
592
+ this.dTypes = new Map();
593
+ this.props = new Map();
594
+ this.propValues = new Map();
595
+ this.partNames = new Map();
596
+
597
+ // Create tables and initial data
598
+ await this.createTables(db, version, options.verbose);
599
+
600
+ // Discover language variants first
601
+ const languageVariants = await this.discoverLanguageVariants(sourceDir);
602
+ if (!options.mainOnly) {
603
+ this.stepCount = 12 + languageVariants.length;
604
+ }
605
+
606
+ // Process all data with proper operation names
607
+ this.updateProgress(0, 'Language Variants');
608
+ await this.processLanguageVariants(db, sourceDir, 2, options);
609
+
610
+ this.updateProgress(0, 'Parts');
611
+ await this.processParts(db, sourceDir, 3, options);
612
+
613
+ this.updateProgress(0, 'Main Codes');
614
+ await this.processCodes(db, sourceDir, 4, options);
615
+
616
+ this.updateProgress(0, 'Consumer Names');
617
+ await this.processConsumerNames(db, sourceDir, 5, options);
618
+
619
+ this.updateProgress(0, 'Answer Lists');
620
+ await this.processLists(db, sourceDir, 6, options);
621
+
622
+ this.updateProgress(0, 'Part Links');
623
+ await this.processPartLinks(db, sourceDir, 7, options);
624
+
625
+ this.updateProgress(0, 'List Links');
626
+ await this.processListLinks(db, sourceDir, 8, options);
627
+
628
+ this.updateProgress(0, 'Hierarchy');
629
+ await this.processHierarchy(db, sourceDir, 9, options);
630
+
631
+ this.updateProgress(0, 'Property Values');
632
+ await this.processPropertyValues(db, 10, options);
633
+
634
+ this.updateProgress(0, 'Closure Table');
635
+ await this.storeClosureTable(db, 11, options);
636
+
637
+ // Process individual language variants
638
+ if (!options.mainOnly) {
639
+ for (let i = 0; i < languageVariants.length; i++) {
640
+ this.updateProgress(0, `Lang: ${languageVariants[i]}`);
641
+ await this.processLanguage(db, sourceDir, 12 + i, languageVariants[i], options);
642
+ }
643
+ }
644
+
645
+ if (options.verbose) console.log('LOINC data migration completed successfully');
646
+ } finally {
647
+ await this.closeDatabase(db, options.verbose);
648
+ }
649
+ }
650
+
651
+ async createTables(db, version, verbose = true) {
652
+ if (verbose) console.log('Creating database tables...');
653
+
654
+ const tableSQL = [
655
+ `CREATE TABLE Config (
656
+ ConfigKey INTEGER NOT NULL PRIMARY KEY,
657
+ Value TEXT NOT NULL
658
+ )`,
659
+ `CREATE TABLE Types (
660
+ TypeKey INTEGER NOT NULL PRIMARY KEY,
661
+ Code TEXT NOT NULL
662
+ )`,
663
+ `CREATE TABLE Languages (
664
+ LanguageKey INTEGER NOT NULL PRIMARY KEY,
665
+ Code TEXT NOT NULL,
666
+ Description TEXT NOT NULL
667
+ )`,
668
+ `CREATE TABLE StatusCodes (
669
+ StatusKey INTEGER NOT NULL PRIMARY KEY,
670
+ Description TEXT NOT NULL
671
+ )`,
672
+ `CREATE TABLE RelationshipTypes (
673
+ RelationshipTypeKey INTEGER NOT NULL PRIMARY KEY,
674
+ Description TEXT NOT NULL
675
+ )`,
676
+ `CREATE TABLE DescriptionTypes (
677
+ DescriptionTypeKey INTEGER NOT NULL PRIMARY KEY,
678
+ Description TEXT NOT NULL
679
+ )`,
680
+ `CREATE TABLE PropertyTypes (
681
+ PropertyTypeKey INTEGER NOT NULL PRIMARY KEY,
682
+ Description TEXT NOT NULL
683
+ )`,
684
+ `CREATE TABLE Codes (
685
+ CodeKey INTEGER NOT NULL PRIMARY KEY,
686
+ Code TEXT NOT NULL,
687
+ Type INTEGER NOT NULL,
688
+ RelationshipKey INTEGER NULL,
689
+ StatusKey INTEGER NOT NULL,
690
+ Description TEXT NOT NULL
691
+ )`,
692
+ `CREATE TABLE Relationships (
693
+ RelationshipKey INTEGER NOT NULL PRIMARY KEY,
694
+ RelationshipTypeKey INTEGER NOT NULL,
695
+ SourceKey INTEGER NOT NULL,
696
+ TargetKey INTEGER NOT NULL,
697
+ StatusKey INTEGER NOT NULL
698
+ )`,
699
+ `CREATE TABLE PropertyValues (
700
+ PropertyValueKey INTEGER NOT NULL PRIMARY KEY,
701
+ Value TEXT NOT NULL
702
+ )`,
703
+ `CREATE TABLE Properties (
704
+ PropertyKey INTEGER NOT NULL PRIMARY KEY,
705
+ PropertyTypeKey INTEGER NOT NULL,
706
+ CodeKey INTEGER NOT NULL,
707
+ PropertyValueKey INTEGER NOT NULL
708
+ )`,
709
+ `CREATE TABLE Descriptions (
710
+ DescriptionKey INTEGER NOT NULL PRIMARY KEY,
711
+ CodeKey INTEGER NOT NULL,
712
+ LanguageKey INTEGER NOT NULL,
713
+ DescriptionTypeKey INTEGER NOT NULL,
714
+ Value TEXT NOT NULL
715
+ )`,
716
+ `CREATE TABLE Closure (
717
+ AncestorKey INTEGER NOT NULL,
718
+ DescendentKey INTEGER NOT NULL,
719
+ PRIMARY KEY (AncestorKey, DescendentKey)
720
+ )`,
721
+ `CREATE VIRTUAL TABLE TextIndex USING fts5(
722
+ codekey UNINDEXED,
723
+ type UNINDEXED,
724
+ lang UNINDEXED,
725
+ text
726
+ )`
727
+ ];
728
+
729
+ return new Promise((resolve, reject) => {
730
+ db.serialize(() => {
731
+ tableSQL.forEach(sql => {
732
+ db.run(sql, (err) => {
733
+ if (err) return reject(err);
734
+ });
735
+ });
736
+
737
+ this.insertInitialData(db, version);
738
+ if (verbose) console.log('Database tables created');
739
+ resolve();
740
+ });
741
+ });
742
+ }
743
+
744
+ insertInitialData(db, version) {
745
+ // Config
746
+ db.run('INSERT INTO Config (ConfigKey, Value) VALUES (1, "c3c89b66-5930-4aa2-8962-124561a5f8c1")');
747
+ db.run('INSERT INTO Config (ConfigKey, Value) VALUES (2, ?)', [version]);
748
+
749
+ // Types
750
+ db.run('INSERT INTO Types (TypeKey, Code) VALUES (1, "Code")');
751
+ db.run('INSERT INTO Types (TypeKey, Code) VALUES (2, "Part")');
752
+ db.run('INSERT INTO Types (TypeKey, Code) VALUES (3, "AnswerList")');
753
+ db.run('INSERT INTO Types (TypeKey, Code) VALUES (4, "Answer")');
754
+
755
+ // Status codes
756
+ const statusCodes = [
757
+ [0, 'NotStated'], [1, 'ACTIVE'], [2, 'DEPRECATED'], [3, 'TRIAL'],
758
+ [4, 'DISCOURAGED'], [5, 'EXAMPLE'], [6, 'PREFERRED'], [7, 'Primary'],
759
+ [8, 'DocumentOntology'], [9, 'Radiology'], [10, 'NORMATIVE']
760
+ ];
761
+ statusCodes.forEach(([key, desc]) => {
762
+ db.run('INSERT INTO StatusCodes (StatusKey, Description) VALUES (?, ?)', [key, desc]);
763
+ this.statii.set(desc, key);
764
+ });
765
+
766
+ // Relationship types
767
+ const relationshipTypes = [
768
+ [0, 'N/A'], [1, 'adjustment'], [2, 'challenge'], [3, 'CLASS'], [4, 'COMPONENT'],
769
+ [5, 'count'], [6, 'analyte-divisor'], [7, 'document-kind'], [8, 'document-role'],
770
+ [9, 'document-setting'], [10, 'document-subject-matter-domain'], [11, 'document-type-of-service'],
771
+ [12, 'analyte-gene'], [13, 'METHOD_TYP'], [14, 'PROPERTY'], [15, 'rad-anatomic-location-imaging-focus'],
772
+ [16, 'rad-guidance-for-action'], [17, 'rad-guidance-for-approach'], [18, 'rad-guidance-for-object'],
773
+ [19, 'rad-guidance-for-presence'], [20, 'rad-maneuver-maneuver-type'], [21, 'rad-modality-modality-subtype'],
774
+ [22, 'rad-modality-modality-type'], [23, 'rad-pharmaceutical-route'], [24, 'rad-pharmaceutical-substance-given'],
775
+ [25, 'rad-reason-for-exam'], [26, 'rad-subject'], [27, 'rad-timing'], [28, 'rad-view-aggregation'],
776
+ [29, 'rad-view-view-type'], [30, 'SCALE_TYP'], [31, 'analyte-suffix'], [32, 'super-system'],
777
+ [33, 'SYSTEM'], [34, 'TIME_ASPCT'], [35, 'time-modifier'], [36, 'rad-anatomic-location-laterality'],
778
+ [37, 'rad-anatomic-location-laterality-presence'], [38, 'rad-anatomic-location-region-imaged'],
779
+ [39, 'AnswerList'], [40, 'Answer'], [41, 'answers-for'], [42, 'parent'], [43, 'child']
780
+ ];
781
+ relationshipTypes.forEach(([key, desc]) => {
782
+ db.run('INSERT INTO RelationshipTypes (RelationshipTypeKey, Description) VALUES (?, ?)', [key, desc]);
783
+ this.rels.set(desc, key);
784
+ });
785
+
786
+ // Description types
787
+ const descriptionTypes = [
788
+ [1, 'LONG_COMMON_NAME'], [2, 'SHORTNAME'], [3, 'ConsumerName'],
789
+ [4, 'RELATEDNAMES2'], [5, 'DisplayName'], [6, 'LinguisticVariantDisplayName']
790
+ ];
791
+ descriptionTypes.forEach(([key, desc]) => {
792
+ db.run('INSERT INTO DescriptionTypes (DescriptionTypeKey, Description) VALUES (?, ?)', [key, desc]);
793
+ this.dTypes.set(desc, key);
794
+ });
795
+
796
+ // Property types
797
+ const propertyTypes = [
798
+ [1, 'CLASSTYPE'], [2, 'ORDER_OBS'], [3, 'EXAMPLE_UNITS'], [4, 'EXAMPLE_UCUM_UNITS'],
799
+ [5, 'PanelType'], [6, 'AskAtOrderEntry'], [7, 'UNITSREQUIRED'], [9, 'Copyright'],
800
+ [10, 'ValidHL7AttachmentRequest']
801
+ ];
802
+ propertyTypes.forEach(([key, desc]) => {
803
+ db.run('INSERT INTO PropertyTypes (PropertyTypeKey, Description) VALUES (?, ?)', [key, desc]);
804
+ this.props.set(desc, key);
805
+ });
806
+
807
+ // Languages (English US is default)
808
+ db.run('INSERT INTO Languages (LanguageKey, Code, Description) VALUES (1, "en-US", "English (United States)")');
809
+ this.langs.set('en-US', 1);
810
+ }
811
+
812
+ async discoverLanguageVariants(sourceDir) {
813
+ const languageVariants = [];
814
+ const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
815
+
816
+ if (fs.existsSync(linguisticVariantsDir)) {
817
+ const files = fs.readdirSync(linguisticVariantsDir);
818
+ for (const file of files) {
819
+ if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
820
+ const match = file.match(/^([a-z]{2}[A-Z]{2})/);
821
+ if (match) {
822
+ const langCode = match[1].substring(0, 2) + '-' + match[1].substring(2);
823
+ languageVariants.push(langCode);
824
+ }
825
+ }
826
+ }
827
+ }
828
+ return languageVariants;
829
+ }
830
+
831
+ async processLanguageVariants(db, sourceDir, step, options) {
832
+ if (options.verbose) console.log('Processing Language Variants...');
833
+
834
+ const filePath = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv');
835
+ if (!fs.existsSync(filePath)) {
836
+ if (options.verbose) console.warn(`Language variants file not found: ${filePath}`);
837
+ return;
838
+ }
839
+
840
+ const rl = readline.createInterface({
841
+ input: fs.createReadStream(filePath),
842
+ crlfDelay: Infinity
843
+ });
844
+
845
+ let lineCount = 0;
846
+ for await (const line of rl) {
847
+ lineCount++;
848
+ if (lineCount === 1) continue;
849
+
850
+ const items = csvSplit(line, 4);
851
+ if (items.length < 4) continue;
852
+
853
+ const key = parseInt(items[0]);
854
+ const langCode = items[1] + '-' + items[2];
855
+ const description = items[3];
856
+
857
+ db.run('INSERT INTO Languages (LanguageKey, Code, Description) VALUES (?, ?, ?)',
858
+ [key, langCode, description]);
859
+ this.langs.set(langCode, key);
860
+
861
+ if (key > this.langKey) this.langKey = key;
862
+ }
863
+ }
864
+
865
+ async processParts(db, sourceDir, step, options) {
866
+ if (options.verbose) console.log('Processing Parts...');
867
+
868
+ const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/Part.csv');
869
+ const rl = readline.createInterface({
870
+ input: fs.createReadStream(filePath),
871
+ crlfDelay: Infinity
872
+ });
873
+
874
+ let lineCount = 0;
875
+ let processedCount = 0;
876
+
877
+ for await (const line of rl) {
878
+ lineCount++;
879
+ if (lineCount === 1) continue;
880
+
881
+ const items = csvSplit(line, 5);
882
+ if (items.length < 5) continue;
883
+
884
+ await this.processPartItem(db, items);
885
+ processedCount++;
886
+
887
+ if (processedCount % 100 === 0) {
888
+ this.updateProgress(100);
889
+ }
890
+ }
891
+
892
+ const remaining = processedCount % 100;
893
+ if (remaining > 0) {
894
+ this.updateProgress(remaining);
895
+ }
896
+
897
+ if (options.verbose) console.log(` Processed ${processedCount} parts`);
898
+ }
899
+
900
+ async processPartItem(db, items) {
901
+ this.codeKey++;
902
+ const codeKey = this.codeKey;
903
+ const code = items[0];
904
+ const type = 2;
905
+ const relKey = this.rels.get(adjustPropName(items[1]));
906
+ const description = items[2];
907
+ const statusKey = this.statii.get(items[4]) || 0;
908
+
909
+ db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
910
+ [codeKey, code, type, relKey, statusKey, description]);
911
+
912
+ const codeInfo = { key: codeKey, children: new Set() };
913
+ this.codes.set(code, codeInfo);
914
+ this.codeList.push(codeInfo);
915
+ this.partNames.set(items[1] + '.' + items[2], items[0]);
916
+
917
+ this.addDescription(db, codeKey, 1, this.dTypes.get('DisplayName'), items[3]);
918
+ }
919
+
920
+ async processCodes(db, sourceDir, step, options) {
921
+ if (options.verbose) console.log('Processing Main Codes...');
922
+
923
+ const filePath = path.join(sourceDir, 'LoincTable/Loinc.csv');
924
+ const rl = readline.createInterface({
925
+ input: fs.createReadStream(filePath),
926
+ crlfDelay: Infinity
927
+ });
928
+
929
+ let lineCount = 0;
930
+ let processedCount = 0;
931
+
932
+ for await (const line of rl) {
933
+ lineCount++;
934
+ if (lineCount === 1) continue;
935
+
936
+ const items = csvSplit(line, 39);
937
+ if (items.length < 39) continue;
938
+
939
+ await this.processCodeItem(db, items);
940
+ processedCount++;
941
+
942
+ if (processedCount % 100 === 0) {
943
+ this.updateProgress(100);
944
+ }
945
+ }
946
+
947
+ const remaining = processedCount % 100;
948
+ if (remaining > 0) {
949
+ this.updateProgress(remaining);
950
+ }
951
+
952
+ if (options.verbose) console.log(` Processed ${processedCount} main codes`);
953
+ }
954
+
955
+ async processCodeItem(db, items) {
956
+ this.codeKey++;
957
+ const codeKey = this.codeKey;
958
+ const code = removeQuotes(items[0]);
959
+ const type = 1;
960
+ const description = removeQuotes(items[25]);
961
+ const statusKey = this.statii.get(items[11]) || 1;
962
+
963
+ db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
964
+ [codeKey, code, type, null, statusKey, description]);
965
+
966
+ const codeInfo = { key: codeKey, children: new Set() };
967
+ this.codes.set(code, codeInfo);
968
+ this.codeList.push(codeInfo);
969
+
970
+ // Add CLASS relationship
971
+ const clsCode = this.partNames.get('CLASS.' + items[7]);
972
+ if (clsCode && this.codes.has(clsCode)) {
973
+ this.addRelationship(db, codeKey, this.codes.get(clsCode).key, this.rels.get('CLASS'));
974
+ }
975
+
976
+ // Add properties
977
+ this.addProperty(db, codeKey, this.props.get('CLASSTYPE'), descClassType(items[13]));
978
+ this.addProperty(db, codeKey, this.props.get('ORDER_OBS'), items[21]);
979
+ this.addProperty(db, codeKey, this.props.get('EXAMPLE_UNITS'), items[24]);
980
+ this.addProperty(db, codeKey, this.props.get('EXAMPLE_UCUM_UNITS'), items[26]);
981
+ this.addProperty(db, codeKey, this.props.get('PanelType'), items[34]);
982
+ this.addProperty(db, codeKey, this.props.get('AskAtOrderEntry'), items[35]);
983
+ this.addProperty(db, codeKey, this.props.get('UNITSREQUIRED'), items[18]);
984
+ this.addProperty(db, codeKey, this.props.get('Copyright'), items[23]);
985
+ this.addProperty(db, codeKey, this.props.get('ValidHL7AttachmentRequest'), items[38]);
986
+
987
+ // Add descriptions
988
+ this.addDescription(db, codeKey, 1, this.dTypes.get('LONG_COMMON_NAME'), description);
989
+ this.addDescription(db, codeKey, 1, this.dTypes.get('ConsumerName'), items[12]);
990
+ this.addDescription(db, codeKey, 1, this.dTypes.get('RELATEDNAMES2'), items[19]);
991
+ this.addDescription(db, codeKey, 1, this.dTypes.get('SHORTNAME'), items[20]);
992
+ this.addDescription(db, codeKey, 1, this.dTypes.get('DisplayName'), items[39]);
993
+ }
994
+
995
+ async processConsumerNames(db, sourceDir, step, options) {
996
+ const filePath = path.join(sourceDir, 'AccessoryFiles/ConsumerName/ConsumerName.csv');
997
+ if (!fs.existsSync(filePath)) {
998
+ if (options.verbose) console.warn(`Consumer names file not found: ${filePath}`);
999
+ return;
1000
+ }
1001
+
1002
+ if (options.verbose) console.log('Processing Consumer Names...');
1003
+
1004
+ const rl = readline.createInterface({
1005
+ input: fs.createReadStream(filePath),
1006
+ crlfDelay: Infinity
1007
+ });
1008
+
1009
+ let lineCount = 0;
1010
+ let processedCount = 0;
1011
+ for await (const line of rl) {
1012
+ lineCount++;
1013
+ if (lineCount === 1) continue;
1014
+
1015
+ const items = csvSplit(line, 2);
1016
+ if (items.length < 2 || !this.codes.has(items[0])) continue;
1017
+
1018
+ this.addDescription(db, this.codes.get(items[0]).key, 1,
1019
+ this.dTypes.get('ConsumerName'), items[1]);
1020
+ processedCount++;
1021
+
1022
+ if (processedCount % 100 === 0) {
1023
+ this.updateProgress(100);
1024
+ }
1025
+ }
1026
+
1027
+ const remaining = processedCount % 100;
1028
+ if (remaining > 0) {
1029
+ this.updateProgress(remaining);
1030
+ }
1031
+ }
1032
+
1033
+ async processLists(db, sourceDir, step, options) {
1034
+ const filePath = path.join(sourceDir, 'AccessoryFiles/AnswerFile/AnswerList.csv');
1035
+ if (!fs.existsSync(filePath)) {
1036
+ if (options.verbose) console.warn(`Answer lists file not found: ${filePath}`);
1037
+ return;
1038
+ }
1039
+
1040
+ if (options.verbose) console.log('Processing Answer Lists...');
1041
+
1042
+ const rl = readline.createInterface({
1043
+ input: fs.createReadStream(filePath),
1044
+ crlfDelay: Infinity
1045
+ });
1046
+
1047
+ let lineCount = 0;
1048
+ let processedCount = 0;
1049
+ let currentList = '';
1050
+
1051
+ for await (const line of rl) {
1052
+ lineCount++;
1053
+ if (lineCount === 1) continue;
1054
+
1055
+ const items = csvSplit(line, 11);
1056
+ if (items.length < 11) continue;
1057
+
1058
+ await this.processListItem(db, items, currentList);
1059
+ currentList = items[0];
1060
+ processedCount++;
1061
+
1062
+ if (processedCount % 100 === 0) {
1063
+ this.updateProgress(100);
1064
+ }
1065
+ }
1066
+
1067
+ const remaining = processedCount % 100;
1068
+ if (remaining > 0) {
1069
+ this.updateProgress(remaining);
1070
+ }
1071
+ }
1072
+
1073
+ async processListItem(db, items, currentList) {
1074
+ const listCode = removeQuotes(items[0]);
1075
+ let listCodeKey;
1076
+
1077
+ if (listCode !== currentList) {
1078
+ this.codeKey++;
1079
+ listCodeKey = this.codeKey;
1080
+ const description = removeQuotes(items[1]);
1081
+
1082
+ db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
1083
+ [listCodeKey, listCode, 3, null, 0, description]);
1084
+
1085
+ this.codes.set(listCode, { key: listCodeKey, children: new Set() });
1086
+ } else {
1087
+ listCodeKey = this.codes.get(listCode).key;
1088
+ }
1089
+
1090
+ const answerCode = removeQuotes(items[6]);
1091
+ let answerCodeKey;
1092
+
1093
+ if (this.codes.has(answerCode)) {
1094
+ answerCodeKey = this.codes.get(answerCode).key;
1095
+ } else {
1096
+ this.codeKey++;
1097
+ answerCodeKey = this.codeKey;
1098
+ const description = removeQuotes(items[10]);
1099
+
1100
+ db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
1101
+ [answerCodeKey, answerCode, 4, null, 0, description]);
1102
+
1103
+ this.codes.set(answerCode, { key: answerCodeKey, children: new Set() });
1104
+ }
1105
+
1106
+ this.addRelationship(db, listCodeKey, answerCodeKey, this.rels.get('Answer'));
1107
+ this.addRelationship(db, answerCodeKey, listCodeKey, this.rels.get('AnswerList'));
1108
+ }
1109
+
1110
+ async processPartLinks(db, sourceDir, step, options) {
1111
+ const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv');
1112
+ if (!fs.existsSync(filePath)) {
1113
+ if (options.verbose) console.warn(`Part links file not found: ${filePath}`);
1114
+ return;
1115
+ }
1116
+
1117
+ if (options.verbose) console.log('Processing Part Links...');
1118
+
1119
+ const rl = readline.createInterface({
1120
+ input: fs.createReadStream(filePath),
1121
+ crlfDelay: Infinity
1122
+ });
1123
+
1124
+ let lineCount = 0;
1125
+ let processedCount = 0;
1126
+ for await (const line of rl) {
1127
+ lineCount++;
1128
+ if (lineCount === 1) continue;
1129
+
1130
+ const items = csvSplit(line, 7);
1131
+ if (items.length < 7) continue;
1132
+
1133
+ const sourceCode = items[0];
1134
+ const targetCode = items[2];
1135
+ const relType = adjustPropName(items[5]);
1136
+ const status = items[6];
1137
+
1138
+ if (this.codes.has(sourceCode) && this.codes.has(targetCode)) {
1139
+ this.addRelationship(db,
1140
+ this.codes.get(sourceCode).key,
1141
+ this.codes.get(targetCode).key,
1142
+ this.rels.get(relType),
1143
+ this.statii.get(status) || 0
1144
+ );
1145
+ }
1146
+ processedCount++;
1147
+
1148
+ if (processedCount % 100 === 0) {
1149
+ this.updateProgress(100);
1150
+ }
1151
+ }
1152
+
1153
+ const remaining = processedCount % 100;
1154
+ if (remaining > 0) {
1155
+ this.updateProgress(remaining);
1156
+ }
1157
+ }
1158
+
1159
+ async processListLinks(db, sourceDir, step, options) {
1160
+ const filePath = path.join(sourceDir, 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv');
1161
+ if (!fs.existsSync(filePath)) {
1162
+ if (options.verbose) console.warn(`List links file not found: ${filePath}`);
1163
+ return;
1164
+ }
1165
+
1166
+ if (options.verbose) console.log('Processing List Links...');
1167
+
1168
+ const rl = readline.createInterface({
1169
+ input: fs.createReadStream(filePath),
1170
+ crlfDelay: Infinity
1171
+ });
1172
+
1173
+ let lineCount = 0;
1174
+ let processedCount = 0;
1175
+ for await (const line of rl) {
1176
+ lineCount++;
1177
+ if (lineCount === 1) continue;
1178
+
1179
+ const items = csvSplit(line, 5);
1180
+ if (items.length < 5) continue;
1181
+
1182
+ const sourceCode = items[0];
1183
+ const targetCode = items[2];
1184
+ const status = items[4];
1185
+
1186
+ if (this.codes.has(sourceCode) && this.codes.has(targetCode)) {
1187
+ const statusKey = this.statii.get(status) || 0;
1188
+ this.addRelationship(db,
1189
+ this.codes.get(sourceCode).key,
1190
+ this.codes.get(targetCode).key,
1191
+ this.rels.get('AnswerList'),
1192
+ statusKey
1193
+ );
1194
+ this.addRelationship(db,
1195
+ this.codes.get(targetCode).key,
1196
+ this.codes.get(sourceCode).key,
1197
+ this.rels.get('answers-for'),
1198
+ statusKey
1199
+ );
1200
+ }
1201
+ processedCount++;
1202
+
1203
+ if (processedCount % 100 === 0) {
1204
+ this.updateProgress(100);
1205
+ }
1206
+ }
1207
+
1208
+ const remaining = processedCount % 100;
1209
+ if (remaining > 0) {
1210
+ this.updateProgress(remaining);
1211
+ }
1212
+ }
1213
+
1214
+ async processHierarchy(db, sourceDir, step, options) {
1215
+ const filePath = path.join(sourceDir, 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv');
1216
+ if (!fs.existsSync(filePath)) {
1217
+ if (options.verbose) console.warn(`Hierarchy file not found: ${filePath}`);
1218
+ return;
1219
+ }
1220
+
1221
+ if (options.verbose) console.log('Processing Hierarchy...');
1222
+
1223
+ const rl = readline.createInterface({
1224
+ input: fs.createReadStream(filePath),
1225
+ crlfDelay: Infinity
1226
+ });
1227
+
1228
+ let lineCount = 0;
1229
+ let processedCount = 0;
1230
+ for await (const line of rl) {
1231
+ lineCount++;
1232
+ if (lineCount === 1) continue;
1233
+
1234
+ const items = csvSplit(line, 12);
1235
+ if (items.length < 5) continue;
1236
+
1237
+ await this.processHierarchyItem(db, items);
1238
+ processedCount++;
1239
+
1240
+ if (processedCount % 100 === 0) {
1241
+ this.updateProgress(100);
1242
+ }
1243
+ }
1244
+
1245
+ const remaining = processedCount % 100;
1246
+ if (remaining > 0) {
1247
+ this.updateProgress(remaining);
1248
+ }
1249
+ }
1250
+
1251
+ async processHierarchyItem(db, items) {
1252
+ const pathCode = items[3];
1253
+ const parentPath = items[2];
1254
+ const description = items[4];
1255
+
1256
+ if (!this.codes.has(pathCode)) {
1257
+ this.codeKey++;
1258
+ const codeKey = this.codeKey;
1259
+
1260
+ db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
1261
+ [codeKey, pathCode, 2, 0, 0, description]);
1262
+
1263
+ const codeInfo = { key: codeKey, children: new Set() };
1264
+ this.codes.set(pathCode, codeInfo);
1265
+ this.codeList.push(codeInfo);
1266
+ }
1267
+
1268
+ if (!parentPath) {
1269
+ db.run('INSERT INTO Config (ConfigKey, Value) VALUES (3, ?)', [pathCode]);
1270
+ } else if (this.codes.has(parentPath)) {
1271
+ const childKey = this.codes.get(pathCode).key;
1272
+ const parentKey = this.codes.get(parentPath).key;
1273
+
1274
+ this.addRelationship(db, childKey, parentKey, this.rels.get('parent'));
1275
+ this.addRelationship(db, parentKey, childKey, this.rels.get('child'));
1276
+
1277
+ const pathParts = items[0].split('.');
1278
+ for (const ancestorCode of pathParts) {
1279
+ if (this.codes.has(ancestorCode)) {
1280
+ const ancestorInfo = this.codes.get(ancestorCode);
1281
+ ancestorInfo.children.add(childKey);
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ async processPropertyValues(db, step, options) {
1288
+ if (options.verbose) console.log('Processing Property Values...');
1289
+
1290
+ for (const [value, key] of this.propValues) {
1291
+ db.run('INSERT INTO PropertyValues (PropertyValueKey, Value) VALUES (?, ?)', [key, value]);
1292
+ }
1293
+ }
1294
+
1295
+ async storeClosureTable(db, step, options) {
1296
+ if (options.verbose) console.log('Storing Closure Table...');
1297
+
1298
+ let count = 0;
1299
+ for (const codeInfo of this.codeList) {
1300
+ count++;
1301
+
1302
+ if (codeInfo.children.size > 0) {
1303
+ for (const descendentKey of codeInfo.children) {
1304
+ db.run('INSERT INTO Closure (AncestorKey, DescendentKey) VALUES (?, ?)',
1305
+ [codeInfo.key, descendentKey]);
1306
+ }
1307
+ }
1308
+
1309
+ if (count % 100 === 0) {
1310
+ this.updateProgress(100);
1311
+ }
1312
+ }
1313
+
1314
+ const remaining = count % 100;
1315
+ if (remaining > 0) {
1316
+ this.updateProgress(remaining);
1317
+ }
1318
+ }
1319
+
1320
+ async processLanguage(db, sourceDir, step, langCode, options) {
1321
+ if (options.verbose) console.log(`Processing Language ${langCode}...`);
1322
+
1323
+ const langKey = this.langs.get(langCode);
1324
+ if (!langKey) return;
1325
+
1326
+ const baseCode = langCode.replace('-', '');
1327
+ const fileName = `${baseCode}${langKey}LinguisticVariant.csv`;
1328
+ const filePath = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants', fileName);
1329
+
1330
+ if (!fs.existsSync(filePath)) {
1331
+ if (options.verbose) console.warn(`Language file not found: ${filePath}`);
1332
+ return;
1333
+ }
1334
+
1335
+ const rl = readline.createInterface({
1336
+ input: fs.createReadStream(filePath),
1337
+ crlfDelay: Infinity
1338
+ });
1339
+
1340
+ let lineCount = 0;
1341
+ let processedCount = 0;
1342
+ for await (const line of rl) {
1343
+ lineCount++;
1344
+ if (lineCount === 1) continue;
1345
+
1346
+ const items = csvSplit(line, 12);
1347
+ if (items.length < 12 || !this.codes.has(items[0])) continue;
1348
+
1349
+ const codeKey = this.codes.get(items[0]).key;
1350
+
1351
+ this.addDescription(db, codeKey, langKey, this.dTypes.get('LONG_COMMON_NAME'), items[9]);
1352
+ this.addDescription(db, codeKey, langKey, this.dTypes.get('RELATEDNAMES2'), items[10]);
1353
+ this.addDescription(db, codeKey, langKey, this.dTypes.get('SHORTNAME'), items[8]);
1354
+ this.addDescription(db, codeKey, langKey, this.dTypes.get('LinguisticVariantDisplayName'), items[11]);
1355
+
1356
+ processedCount++;
1357
+
1358
+ if (processedCount % 100 === 0) {
1359
+ this.updateProgress(100);
1360
+ }
1361
+ }
1362
+
1363
+ const remaining = processedCount % 100;
1364
+ if (remaining > 0) {
1365
+ this.updateProgress(remaining);
1366
+ }
1367
+ }
1368
+
1369
+ addDescription(db, codeKey, languageKey, descriptionType, value) {
1370
+ if (!value) return;
1371
+
1372
+ this.descKey++;
1373
+ db.run('INSERT INTO Descriptions (DescriptionKey, CodeKey, LanguageKey, DescriptionTypeKey, Value) VALUES (?, ?, ?, ?, ?)',
1374
+ [this.descKey, codeKey, languageKey, descriptionType, value]);
1375
+
1376
+ db.run('INSERT INTO TextIndex (codekey, type, lang, text) VALUES (?, ?, ?, ?)',
1377
+ [codeKey, descriptionType, languageKey, value]);
1378
+ }
1379
+
1380
+ addProperty(db, codeKey, propertyType, value) {
1381
+ if (!value || !propertyType) return;
1382
+
1383
+ this.propKey++;
1384
+ const propertyValueKey = this.getPropertyValueKey(value);
1385
+
1386
+ db.run('INSERT INTO Properties (PropertyKey, PropertyTypeKey, CodeKey, PropertyValueKey) VALUES (?, ?, ?, ?)',
1387
+ [this.propKey, propertyType, codeKey, propertyValueKey]);
1388
+ }
1389
+
1390
+ addRelationship(db, sourceKey, targetKey, relationshipType, statusKey = 0) {
1391
+ if (!relationshipType) return;
1392
+
1393
+ this.relKey++;
1394
+ db.run('INSERT INTO Relationships (RelationshipKey, RelationshipTypeKey, SourceKey, TargetKey, StatusKey) VALUES (?, ?, ?, ?, ?)',
1395
+ [this.relKey, relationshipType, sourceKey, targetKey, statusKey]);
1396
+ }
1397
+
1398
+ getPropertyValueKey(value) {
1399
+ if (this.propValues.has(value)) {
1400
+ return this.propValues.get(value);
1401
+ }
1402
+
1403
+ this.propValueKey++;
1404
+ this.propValues.set(value, this.propValueKey);
1405
+ return this.propValueKey;
1406
+ }
1407
+
1408
+ async closeDatabase(db, verbose = true) {
1409
+ return new Promise((resolve) => {
1410
+ db.close((err) => {
1411
+ if (err && verbose) {
1412
+ console.error('Error closing database:', err);
1413
+ }
1414
+ resolve();
1415
+ });
1416
+ });
1417
+ }
1418
+ }
1419
+
1420
+ // Property name mappings from Pascal code
1421
+ const KNOWN_PROPERTY_NAMES = [
1422
+ 'AskAtOrderEntry', 'AssociatedObservations', 'CHANGE_REASON_PUBLIC', 'CHNG_TYPE', 'CLASS', 'CLASSTYPE', 'COMMON_ORDER_RANK', 'COMMON_TEST_RANK', 'COMPONENT', 'CONSUMER_NAME',
1423
+ 'DefinitionDescription', 'DisplayName', 'EXAMPLE_UCUM_UNITS', 'EXAMPLE_UNITS', 'EXMPL_ANSWERS', 'EXTERNAL_COPYRIGHT_LINK', 'EXTERNAL_COPYRIGHT_NOTICE', 'FORMULA',
1424
+ 'HL7_ATTACHMENT_STRUCTURE', 'HL7_FIELD_SUBFIELD_ID', 'LONG_COMMON_NAME', 'MAP_TO', 'METHOD_TYP', 'ORDER_OBS', 'PROPERTY', 'PanelType', 'RELATEDNAMES2', 'SCALE_TYP',
1425
+ 'SHORTNAME', 'STATUS', 'STATUS_REASON', 'STATUS_TEXT', 'SURVEY_QUEST_SRC', 'SURVEY_QUEST_TEXT', 'SYSTEM', 'TIME_ASPCT', 'UNITSREQUIRED', 'ValidHL7AttachmentRequest',
1426
+ 'VersionFirstReleased', 'VersionLastChanged', 'adjustment', 'analyte', 'analyte-core', 'analyte-divisor', 'analyte-divisor-suffix', 'analyte-gene', 'analyte-numerator',
1427
+ 'analyte-suffix', 'answer-list', 'answers-for', 'category', 'challenge', 'child', 'count', 'document-kind', 'document-role', 'document-setting', 'document-subject-matter-domain',
1428
+ 'document-type-of-service', 'parent', 'rad-anatomic-location-imaging-focus', 'rad-anatomic-location-laterality', 'rad-anatomic-location-laterality-presence', 'rad-anatomic-location-region-imaged',
1429
+ 'rad-guidance-for-action', 'rad-guidance-for-approach', 'rad-guidance-for-object', 'rad-guidance-for-presence', 'rad-maneuver-maneuver-type', 'rad-modality-modality-subtype',
1430
+ 'rad-modality-modality-type', 'rad-pharmaceutical-route', 'rad-pharmaceutical-substance-given', 'rad-reason-for-exam', 'rad-subject', 'rad-timing', 'rad-view-aggregation',
1431
+ 'rad-view-view-type', 'search', 'super-system', 'system-core', 'time-core', 'time-modifier',
1432
+ 'Answer', 'AnswerList'
1433
+ ];
1434
+
1435
+ function adjustPropName(s) {
1436
+ if (KNOWN_PROPERTY_NAMES.includes(s)) {
1437
+ return s;
1438
+ }
1439
+
1440
+ const mappings = {
1441
+ 'ADJUSTMENT': 'adjustment',
1442
+ 'CHALLENGE': 'challenge',
1443
+ 'COUNT': 'count',
1444
+ 'DIVISORS': 'analyte-divisor',
1445
+ 'Document.Kind': 'document-kind',
1446
+ 'Document.Role': 'document-role',
1447
+ 'Document.Setting': 'document-setting',
1448
+ 'Document.SubjectMatterDomain': 'document-subject-matter-domain',
1449
+ 'Document.TypeOfService': 'document-type-of-service',
1450
+ 'GENE': 'analyte-gene',
1451
+ 'METHOD': 'METHOD_TYP',
1452
+ 'Rad.Anatomic Location.Imaging Focus': 'rad-anatomic-location-imaging-focus',
1453
+ 'Rad.Anatomic Location.Laterality': 'rad-anatomic-location-laterality',
1454
+ 'Rad.Anatomic Location.Laterality.Presence': 'rad-anatomic-location-laterality-presence',
1455
+ 'Rad.Anatomic Location.Region Imaged': 'rad-anatomic-location-region-imaged',
1456
+ 'Rad.Guidance for.Action': 'rad-guidance-for-action',
1457
+ 'Rad.Guidance for.Approach': 'rad-guidance-for-approach',
1458
+ 'Rad.Guidance for.Object': 'rad-guidance-for-object',
1459
+ 'Rad.Guidance for.Presence': 'rad-guidance-for-presence',
1460
+ 'Rad.Maneuver.Maneuver Type': 'rad-maneuver-maneuver-type',
1461
+ 'Rad.Modality.Modality Subtype': 'rad-modality-modality-subtype',
1462
+ 'Rad.Modality.Modality Type': 'rad-modality-modality-type',
1463
+ 'Rad.Pharmaceutical.Route': 'rad-pharmaceutical-route',
1464
+ 'Rad.Pharmaceutical.Substance Given': 'rad-pharmaceutical-substance-given',
1465
+ 'Rad.Reason for Exam': 'rad-reason-for-exam',
1466
+ 'Rad.Subject': 'rad-subject',
1467
+ 'Rad.Timing': 'rad-timing',
1468
+ 'Rad.View.Aggregation': 'rad-view-aggregation',
1469
+ 'Rad.View.View Type': 'rad-view-view-type',
1470
+ 'SCALE': 'SCALE_TYP',
1471
+ 'SUFFIX': 'analyte-suffix',
1472
+ 'SUPER SYSTEM': 'super-system',
1473
+ 'TIME': 'TIME_ASPCT',
1474
+ 'TIME MODIFIER': 'time-modifier'
1475
+ };
1476
+
1477
+ if (mappings[s]) {
1478
+ return mappings[s];
1479
+ }
1480
+
1481
+ throw new Error(`Unknown Property Name: ${s}`);
1482
+ }
1483
+
1484
+ function descClassType(s) {
1485
+ const types = {
1486
+ '1': 'Laboratory class',
1487
+ '2': 'Clinical class',
1488
+ '3': 'Claims attachment',
1489
+ '4': 'Surveys'
1490
+ };
1491
+ return types[s] || s;
1492
+ }
1493
+
1494
+ // CSV parsing utility
1495
+ function csvSplit(line, expectedCount) {
1496
+ const result = new Array(expectedCount).fill('');
1497
+ let inQuoted = false;
1498
+ let currentField = 0;
1499
+ let fieldStart = 0;
1500
+ let i = 0;
1501
+
1502
+ while (i < line.length && currentField < expectedCount) {
1503
+ const ch = line[i];
1504
+
1505
+ if (!inQuoted && ch === ',') {
1506
+ if (currentField < expectedCount) {
1507
+ result[currentField] = line.substring(fieldStart, i).replace(/^"|"$/g, '').replace(/""/g, '"');
1508
+ currentField++;
1509
+ fieldStart = i + 1;
1510
+ }
1511
+ } else if (ch === '"') {
1512
+ if (inQuoted && i + 1 < line.length && line[i + 1] === '"') {
1513
+ i++;
1514
+ } else {
1515
+ inQuoted = !inQuoted;
1516
+ }
1517
+ }
1518
+ i++;
1519
+ }
1520
+
1521
+ if (currentField < expectedCount) {
1522
+ result[currentField] = line.substring(fieldStart).replace(/^"|"$/g, '').replace(/""/g, '"');
1523
+ }
1524
+
1525
+ return result;
1526
+ }
1527
+
1528
+ function removeQuotes(str) {
1529
+ if (!str) return '';
1530
+ return str.replace(/^"|"$/g, '');
1531
+ }
1532
+
1533
+ module.exports = {
1534
+ LoincModule,
1535
+ LoincDataMigrator
1536
+ };