fhirsmith 0.18.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -0,0 +1,25 @@
1
+ {%- comment -%}
2
+ The background tasks under the graphs (stats.taskSummary): one line naming those at work, those
3
+ whose last run failed (each is a problem in the status line, with its state) and the idle ones,
4
+ then a row for each at work. Styles are in status-line.liquid; every task is under "More".
5
+ {%- endcomment -%}
6
+ <section class="fs-status fs-status-jobs" aria-labelledby="fs-status-jobs-title">
7
+ <div class="fs-status-jobs-head">
8
+ <h{{ level }} id="fs-status-jobs-title">Background tasks</h{{ level }}>
9
+ <span class="fs-status-idle">
10
+ {%- if tasks.working.size == 0 %}none at work{% else %}{{ tasks.working.size }} at work{% endif -%}
11
+ {%- if tasks.failed.size > 0 %} · {{ tasks.failed.size }} failed, under Problems: {{ tasks.failed | join: ', ' | escape }}{% endif -%}
12
+ {%- if tasks.idle.size > 0 %} · {{ tasks.idle.size }} idle: {{ tasks.idle | join: ', ' | escape }}{% endif -%}
13
+ </span>
14
+ </div>
15
+ {%- if tasks.working.size > 0 %}
16
+ <table>
17
+ <thead><tr><th>Task</th><th>Status</th><th>Frequency</th><th>Last seen</th></tr></thead>
18
+ <tbody>
19
+ {%- for t in tasks.working %}
20
+ <tr><td>{{ t.name | escape }}</td><td>{{ t.state | escape }}</td><td>{{ t.frequency | escape }}</td><td>{{ t.ago | escape }}</td></tr>
21
+ {%- endfor %}
22
+ </tbody>
23
+ </table>
24
+ {%- endif %}
25
+ </section>
@@ -83,8 +83,6 @@
83
83
  <div id="stripe"> </div>
84
84
  <div class="container">
85
85
  <div style="background-color: #ad1f2f; padding: 6px; color: white;">
86
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> [%fhir-version%] &copy; HL7.org &nbsp;|&nbsp;
87
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &nbsp;|&nbsp;
88
86
  <a href="/" style="color: gold"> Server Home</a> &nbsp;|&nbsp;
89
87
  <a href="[%endpoint-path%]" style="color: gold">TX Home</a> &nbsp;|&nbsp;
90
88
  <a href="[%endpoint-path%]/metadata" style="color: gold">Capability Statement</a> &nbsp;|&nbsp;
@@ -105,7 +103,7 @@
105
103
  <div class="inner-wrapper">
106
104
  <div class="col-12">
107
105
 
108
- <h2><img border="0" src="/FHIRsmith32.png" style="vertical-align: text-bottom"/> TX: [%title%] </h2>
106
+ <h2><a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" title="FHIRsmith [%ver%]"><img border="0" src="/FHIRsmith32.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/></a> TX: [%title%] </h2>
109
107
 
110
108
  [%content%]
111
109
 
@@ -121,8 +119,7 @@
121
119
  <div class="container">
122
120
  <div class="inner-wrapper">
123
121
  <p>
124
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> v[%fhir-version%] &copy; HL7.org 2011+. &nbsp;|&nbsp;
125
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
122
+ <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
126
123
  &nbsp; ([%ms%] ms)
127
124
  [%sponsorMessage%]
128
125
  </p>
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const { XMLParser } = require('fast-xml-parser');
3
+ const terminal = require('./terminal');
3
4
 
4
5
  // Configuration
5
6
  const ATC_FILE = process.argv[2] || '2025_ATC.xml';
@@ -147,21 +148,21 @@ function buildConcept(atcRow, dddMap) {
147
148
 
148
149
  // Main conversion function
149
150
  function convertATCtoFHIR(atcFile, dddFile) {
150
- console.log(`Parsing ${atcFile}...`);
151
+ terminal.log(`Parsing ${atcFile}...`);
151
152
  const atcParsed = parseXML(atcFile);
152
153
  const atcRows = extractRows(atcParsed);
153
- console.log(` Found ${atcRows.length} ATC codes`);
154
+ terminal.log(` Found ${atcRows.length} ATC codes`);
154
155
 
155
- console.log(`Parsing ${dddFile}...`);
156
+ terminal.log(`Parsing ${dddFile}...`);
156
157
  const dddParsed = parseXML(dddFile);
157
158
  const dddRows = extractRows(dddParsed);
158
- console.log(` Found ${dddRows.length} DDD entries`);
159
+ terminal.log(` Found ${dddRows.length} DDD entries`);
159
160
 
160
- console.log('Building DDD lookup map...');
161
+ terminal.log('Building DDD lookup map...');
161
162
  const dddMap = buildDDDMap(dddRows);
162
- console.log(` ${dddMap.size} codes have DDD data`);
163
+ terminal.log(` ${dddMap.size} codes have DDD data`);
163
164
 
164
- console.log('Building FHIR CodeSystem...');
165
+ terminal.log('Building FHIR CodeSystem...');
165
166
 
166
167
  const codeSystem = {
167
168
  resourceType: 'CodeSystem',
@@ -256,7 +257,7 @@ function convertATCtoFHIR(atcFile, dddFile) {
256
257
  parent.concept.push(concept);
257
258
  } else {
258
259
  // Parent not found, add to root (shouldn't happen with valid data)
259
- console.warn(`Warning: Parent ${parentCode} not found for ${code}`);
260
+ terminal.warn(`Warning: Parent ${parentCode} not found for ${code}`);
260
261
  rootConcepts.push(concept);
261
262
  }
262
263
  }
@@ -265,7 +266,7 @@ function convertATCtoFHIR(atcFile, dddFile) {
265
266
  codeSystem.concept = rootConcepts;
266
267
  codeSystem.count = conceptMap.size;
267
268
 
268
- console.log(` Generated ${conceptMap.size} concepts in nested hierarchy`);
269
+ terminal.log(` Generated ${conceptMap.size} concepts in nested hierarchy`);
269
270
 
270
271
  return codeSystem;
271
272
  }
@@ -274,19 +275,19 @@ function convertATCtoFHIR(atcFile, dddFile) {
274
275
  try {
275
276
  const codeSystem = convertATCtoFHIR(ATC_FILE, DDD_FILE);
276
277
 
277
- console.log(`Writing to ${OUTPUT_FILE}...`);
278
+ terminal.log(`Writing to ${OUTPUT_FILE}...`);
278
279
  fs.writeFileSync(OUTPUT_FILE, JSON.stringify(codeSystem, null, 2));
279
- console.log('Done!');
280
+ terminal.log('Done!');
280
281
 
281
282
  const totalConcepts = countConcepts(codeSystem.concept);
282
283
  const withDDD = countWithDDD(codeSystem.concept);
283
- console.log(`\nStatistics:`);
284
- console.log(` Total concepts: ${totalConcepts}`);
285
- console.log(` Top-level concepts: ${codeSystem.concept.length}`);
286
- console.log(` Concepts with DDD data: ${withDDD}`);
284
+ terminal.log(`\nStatistics:`);
285
+ terminal.log(` Total concepts: ${totalConcepts}`);
286
+ terminal.log(` Top-level concepts: ${codeSystem.concept.length}`);
287
+ terminal.log(` Concepts with DDD data: ${withDDD}`);
287
288
 
288
289
  } catch (error) {
289
- console.error('Error:', error.message);
290
+ terminal.error('Error:', error.message);
290
291
  process.exit(1);
291
292
  }
292
293
 
@@ -24,6 +24,7 @@ const crypto = require('crypto');
24
24
  const Database = require('better-sqlite3');
25
25
  const { Command } = require('commander');
26
26
  const { axisLabel } = require('../cs/icd11-expressions');
27
+ const terminal = require('./terminal');
27
28
 
28
29
  const TOOL_VERSION = '1.0.0';
29
30
  const SCHEMA_NS = 'http://id.who.int/icd/schema';
@@ -141,7 +142,7 @@ class CodeSystemExporter {
141
142
  constructor(opts) {
142
143
  this.opts = opts;
143
144
  this.db = new Database(opts.src, { readonly: true });
144
- this.log = (...a) => console.log(...a);
145
+ this.log = (...a) => terminal.log(...a);
145
146
  }
146
147
 
147
148
  meta(key) {
@@ -861,7 +862,7 @@ function main(argv) {
861
862
 
862
863
  if (require.main === module) {
863
864
  main(process.argv).catch(e => {
864
- console.error(`\nexport failed: ${e && e.stack || e}`);
865
+ terminal.error(`\nexport failed: ${e && e.stack || e}`);
865
866
  process.exit(1);
866
867
  });
867
868
  }
@@ -26,6 +26,7 @@ const fs = require('fs');
26
26
  const path = require('path');
27
27
  const Database = require('better-sqlite3');
28
28
  const { Command } = require('commander');
29
+ const terminal = require('./terminal');
29
30
 
30
31
  const TOOL_VERSION = '1.0.0';
31
32
  const WHO = 'http://id.who.int';
@@ -341,8 +342,8 @@ class Icd11Importer {
341
342
  this.native = stripTrailingSlash(opts.native);
342
343
  this.fhir = opts.fhir ? stripTrailingSlash(opts.fhir) : null;
343
344
  this.fetcher = new Fetcher(opts);
344
- this.log = (...a) => console.log(...a);
345
- this.warn = (...a) => console.warn(...a);
345
+ this.log = (...a) => terminal.log(...a);
346
+ this.warn = (...a) => terminal.warn(...a);
346
347
  }
347
348
 
348
349
  headers(lang) {
@@ -892,7 +893,7 @@ function main(argv) {
892
893
 
893
894
  if (require.main === module) {
894
895
  main(process.argv).catch(e => {
895
- console.error(`\nimport failed: ${e && e.stack || e}`);
896
+ terminal.error(`\nimport failed: ${e && e.stack || e}`);
896
897
  process.exit(1);
897
898
  });
898
899
  }
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { csvRecords } = require('./csv-records');
7
7
  const chalk = require('chalk');
8
+ const terminal = require('./terminal');
8
9
 
9
10
  class LoincModule extends BaseTerminologyModule {
10
11
  constructor() {
@@ -109,7 +110,7 @@ class LoincModule extends BaseTerminologyModule {
109
110
  } catch (error) {
110
111
  this.logError(`Import command failed: ${error.message}`);
111
112
  if (options.verbose) {
112
- console.error(error.stack);
113
+ terminal.error(error.stack);
113
114
  }
114
115
  throw error;
115
116
  }
@@ -119,17 +120,17 @@ class LoincModule extends BaseTerminologyModule {
119
120
  const inquirer = require('inquirer');
120
121
  const chalk = require('chalk');
121
122
 
122
- console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
123
- console.log(` Source: ${chalk.white(config.source)}`);
124
- console.log(` Destination: ${chalk.white(config.dest)}`);
125
- console.log(` Version: ${chalk.white(config.version)}`);
126
- console.log(` Main Only: ${chalk.white(config.mainOnly ? 'Yes' : 'No')}`);
127
- console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
128
- console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
129
- console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
123
+ terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
124
+ terminal.log(` Source: ${chalk.white(config.source)}`);
125
+ terminal.log(` Destination: ${chalk.white(config.dest)}`);
126
+ terminal.log(` Version: ${chalk.white(config.version)}`);
127
+ terminal.log(` Main Only: ${chalk.white(config.mainOnly ? 'Yes' : 'No')}`);
128
+ terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
129
+ terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
130
+ terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
130
131
 
131
132
  if (config.estimatedDuration) {
132
- console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
133
+ terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
133
134
  }
134
135
 
135
136
  const { confirmed } = await inquirer.prompt({
@@ -144,7 +145,7 @@ class LoincModule extends BaseTerminologyModule {
144
145
 
145
146
  async runImportWithoutConfigSaving(config) {
146
147
  try {
147
- console.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
148
+ terminal.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
148
149
 
149
150
  // Pre-flight checks
150
151
  this.logInfo('Running pre-flight checks...');
@@ -163,7 +164,7 @@ class LoincModule extends BaseTerminologyModule {
163
164
  this.stopProgress();
164
165
  this.logError(`${this.getName()} import failed: ${error.message}`);
165
166
  if (config.verbose) {
166
- console.error(error.stack);
167
+ terminal.error(error.stack);
167
168
  }
168
169
  process.exit(1);
169
170
  }
@@ -186,14 +187,14 @@ class LoincModule extends BaseTerminologyModule {
186
187
  const stats = await this.validateLoincDirectory(options.source);
187
188
 
188
189
  this.logSuccess('Directory validation passed');
189
- console.log(` Main codes file: ${stats.mainCodesFound ? 'Found' : 'Missing'}`);
190
- console.log(` Parts file: ${stats.partsFound ? 'Found' : 'Missing'}`);
191
- console.log(` Language variants: ${stats.languageVariants.length} found`);
192
- console.log(` Estimated main codes: ${stats.estimatedCodes.toLocaleString()}`);
190
+ terminal.log(` Main codes file: ${stats.mainCodesFound ? 'Found' : 'Missing'}`);
191
+ terminal.log(` Parts file: ${stats.partsFound ? 'Found' : 'Missing'}`);
192
+ terminal.log(` Language variants: ${stats.languageVariants.length} found`);
193
+ terminal.log(` Estimated main codes: ${stats.estimatedCodes.toLocaleString()}`);
193
194
 
194
195
  if (stats.warnings.length > 0) {
195
196
  this.logWarning('Validation warnings:');
196
- stats.warnings.forEach(warning => console.log(` ${warning}`));
197
+ stats.warnings.forEach(warning => terminal.log(` ${warning}`));
197
198
  }
198
199
 
199
200
  } catch (error) {
@@ -215,15 +216,15 @@ class LoincModule extends BaseTerminologyModule {
215
216
  const stats = await this.getDatabaseStats(dbPath);
216
217
 
217
218
  this.logSuccess('Database status:');
218
- console.log(` Version: ${stats.version}`);
219
- console.log(` Total Codes: ${stats.codeCount.toLocaleString()}`);
220
- console.log(` Main LOINC Codes: ${stats.mainCodeCount.toLocaleString()}`);
221
- console.log(` Parts: ${stats.partCount.toLocaleString()}`);
222
- console.log(` Answer Lists: ${stats.answerListCount.toLocaleString()}`);
223
- console.log(` Languages: ${stats.languageCount.toLocaleString()}`);
224
- console.log(` Relationships: ${stats.relationshipCount.toLocaleString()}`);
225
- console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
226
- console.log(` Last Modified: ${stats.lastModified}`);
219
+ terminal.log(` Version: ${stats.version}`);
220
+ terminal.log(` Total Codes: ${stats.codeCount.toLocaleString()}`);
221
+ terminal.log(` Main LOINC Codes: ${stats.mainCodeCount.toLocaleString()}`);
222
+ terminal.log(` Parts: ${stats.partCount.toLocaleString()}`);
223
+ terminal.log(` Answer Lists: ${stats.answerListCount.toLocaleString()}`);
224
+ terminal.log(` Languages: ${stats.languageCount.toLocaleString()}`);
225
+ terminal.log(` Relationships: ${stats.relationshipCount.toLocaleString()}`);
226
+ terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
227
+ terminal.log(` Last Modified: ${stats.lastModified}`);
227
228
 
228
229
  } catch (error) {
229
230
  this.logError(`Status check failed: ${error.message}`);
@@ -446,7 +447,7 @@ class LoincModule extends BaseTerminologyModule {
446
447
  db.serialize(() => {
447
448
  indexes.forEach(sql => {
448
449
  db.run(sql, (err) => {
449
- if (err) console.warn(`Index creation warning: ${err.message}`);
450
+ if (err) terminal.warn(`Index creation warning: ${err.message}`);
450
451
  });
451
452
  });
452
453
  });
@@ -579,7 +580,7 @@ class LoincDataMigrator {
579
580
  }
580
581
 
581
582
  async migrate(sourceDir, destFile, version = 'unknown', options = {}) {
582
- if (options.verbose) console.log('Starting LOINC data migration...');
583
+ if (options.verbose) terminal.log('Starting LOINC data migration...');
583
584
 
584
585
  const destDir = path.dirname(destFile);
585
586
  if (!fs.existsSync(destDir)) {
@@ -591,11 +592,21 @@ class LoincDataMigrator {
591
592
  }
592
593
 
593
594
  const db = new sqlite3.Database(destFile);
595
+ // Run every statement in submission order, and the whole import as one transaction:
596
+ // without it each INSERT autocommits and waits on its own fsync (~10 ms each), which
597
+ // made even the small test fixture take minutes
598
+ db.serialize();
599
+ const exec = (sql) => new Promise((resolve, reject) => db.exec(sql, (err) => err ? reject(err) : resolve()));
594
600
 
595
601
  try {
602
+ await exec('BEGIN');
603
+
596
604
  // Initialize tracking variables
597
605
  this.codeKey = 0;
598
606
  this.relKey = 0;
607
+ this.relSeen = new Set();
608
+ this.axisParts = new Map(); // term key -> { COMPONENT: part key, ... } (see processLanguage)
609
+ this.partNamesSeen = new Set();
599
610
  this.descKey = 0;
600
611
  this.propKey = 0;
601
612
  this.propValueKey = 0;
@@ -667,14 +678,15 @@ class LoincDataMigrator {
667
678
  }
668
679
  }
669
680
 
670
- if (options.verbose) console.log('LOINC data migration completed successfully');
681
+ await exec('COMMIT');
682
+ if (options.verbose) terminal.log('LOINC data migration completed successfully');
671
683
  } finally {
672
684
  await this.closeDatabase(db, options.verbose);
673
685
  }
674
686
  }
675
687
 
676
688
  async createTables(db, version, verbose = true) {
677
- if (verbose) console.log('Creating database tables...');
689
+ if (verbose) terminal.log('Creating database tables...');
678
690
 
679
691
  const tableSQL = [
680
692
  `CREATE TABLE Config (
@@ -775,7 +787,7 @@ class LoincDataMigrator {
775
787
  });
776
788
 
777
789
  this.insertInitialData(db, version);
778
- if (verbose) console.log('Database tables created');
790
+ if (verbose) terminal.log('Database tables created');
779
791
  resolve();
780
792
  });
781
793
  });
@@ -826,7 +838,8 @@ class LoincDataMigrator {
826
838
  // Description types
827
839
  const descriptionTypes = [
828
840
  [1, 'LONG_COMMON_NAME'], [2, 'SHORTNAME'], [3, 'ConsumerName'],
829
- [4, 'RELATEDNAMES2'], [5, 'DisplayName'], [6, 'LinguisticVariantDisplayName']
841
+ [4, 'RELATEDNAMES2'], [5, 'DisplayName'], [6, 'LinguisticVariantDisplayName'],
842
+ [7, 'PartName']
830
843
  ];
831
844
  descriptionTypes.forEach(([key, desc]) => {
832
845
  db.run('INSERT INTO DescriptionTypes (DescriptionTypeKey, Description) VALUES (?, ?)', [key, desc]);
@@ -869,11 +882,11 @@ class LoincDataMigrator {
869
882
  }
870
883
 
871
884
  async processLanguageVariants(db, sourceDir, step, options) {
872
- if (options.verbose) console.log('Processing Language Variants...');
885
+ if (options.verbose) terminal.log('Processing Language Variants...');
873
886
 
874
887
  const filePath = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv');
875
888
  if (!fs.existsSync(filePath)) {
876
- if (options.verbose) console.warn(`Language variants file not found: ${filePath}`);
889
+ if (options.verbose) terminal.warn(`Language variants file not found: ${filePath}`);
877
890
  return;
878
891
  }
879
892
 
@@ -900,7 +913,7 @@ class LoincDataMigrator {
900
913
  }
901
914
 
902
915
  async processParts(db, sourceDir, step, options) {
903
- if (options.verbose) console.log('Processing Parts...');
916
+ if (options.verbose) terminal.log('Processing Parts...');
904
917
 
905
918
  const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/Part.csv');
906
919
  const rl = csvRecords(filePath);
@@ -928,7 +941,7 @@ class LoincDataMigrator {
928
941
  this.updateProgress(remaining);
929
942
  }
930
943
 
931
- if (options.verbose) console.log(` Processed ${processedCount} parts`);
944
+ if (options.verbose) terminal.log(` Processed ${processedCount} parts`);
932
945
  }
933
946
 
934
947
  async processPartItem(db, items) {
@@ -952,7 +965,7 @@ class LoincDataMigrator {
952
965
  }
953
966
 
954
967
  async processCodes(db, sourceDir, step, options) {
955
- if (options.verbose) console.log('Processing Main Codes...');
968
+ if (options.verbose) terminal.log('Processing Main Codes...');
956
969
 
957
970
  const filePath = path.join(sourceDir, 'LoincTable/Loinc.csv');
958
971
  const rl = csvRecords(filePath);
@@ -980,7 +993,7 @@ class LoincDataMigrator {
980
993
  this.updateProgress(remaining);
981
994
  }
982
995
 
983
- if (options.verbose) console.log(` Processed ${processedCount} main codes`);
996
+ if (options.verbose) terminal.log(` Processed ${processedCount} main codes`);
984
997
  }
985
998
 
986
999
  async processCodeItem(db, items) {
@@ -1002,6 +1015,7 @@ class LoincDataMigrator {
1002
1015
  const clsCode = this.partNames.get('CLASS.' + items[7]);
1003
1016
  if (clsCode && this.codes.has(clsCode)) {
1004
1017
  this.addRelationship(db, codeKey, this.codes.get(clsCode).key, this.rels.get('CLASS'));
1018
+ this.#noteAxisPart(codeKey, 'CLASS', this.codes.get(clsCode).key);
1005
1019
  }
1006
1020
 
1007
1021
  // Add properties
@@ -1026,11 +1040,11 @@ class LoincDataMigrator {
1026
1040
  async processConsumerNames(db, sourceDir, step, options) {
1027
1041
  const filePath = path.join(sourceDir, 'AccessoryFiles/ConsumerName/ConsumerName.csv');
1028
1042
  if (!fs.existsSync(filePath)) {
1029
- if (options.verbose) console.warn(`Consumer names file not found: ${filePath}`);
1043
+ if (options.verbose) terminal.warn(`Consumer names file not found: ${filePath}`);
1030
1044
  return;
1031
1045
  }
1032
1046
 
1033
- if (options.verbose) console.log('Processing Consumer Names...');
1047
+ if (options.verbose) terminal.log('Processing Consumer Names...');
1034
1048
 
1035
1049
  const rl = csvRecords(filePath);
1036
1050
 
@@ -1061,11 +1075,11 @@ class LoincDataMigrator {
1061
1075
  async processLists(db, sourceDir, step, options) {
1062
1076
  const filePath = path.join(sourceDir, 'AccessoryFiles/AnswerFile/AnswerList.csv');
1063
1077
  if (!fs.existsSync(filePath)) {
1064
- if (options.verbose) console.warn(`Answer lists file not found: ${filePath}`);
1078
+ if (options.verbose) terminal.warn(`Answer lists file not found: ${filePath}`);
1065
1079
  return;
1066
1080
  }
1067
1081
 
1068
- if (options.verbose) console.log('Processing Answer Lists...');
1082
+ if (options.verbose) terminal.log('Processing Answer Lists...');
1069
1083
 
1070
1084
  const rl = csvRecords(filePath);
1071
1085
 
@@ -1113,6 +1127,12 @@ class LoincDataMigrator {
1113
1127
  }
1114
1128
 
1115
1129
  const answerCode = removeQuotes(items[6]);
1130
+ if (!answerCode) {
1131
+ // an externally defined answer list (ExtDefinedYN = Y: ICD-10, RxNorm, ...) has one row with
1132
+ // no AnswerStringId. The list is a code; the row names no answer, so there is nothing to add
1133
+ // (it used to become a single Answer code '' shared by all 65 such lists)
1134
+ return;
1135
+ }
1116
1136
  let answerCodeKey;
1117
1137
 
1118
1138
  if (this.codes.has(answerCode)) {
@@ -1141,11 +1161,11 @@ class LoincDataMigrator {
1141
1161
  async processPartMappings(db, sourceDir, step, options) {
1142
1162
  const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/PartRelatedCodeMapping.csv');
1143
1163
  if (!fs.existsSync(filePath)) {
1144
- if (options.verbose) console.warn(`Part mapping file not found: ${filePath}`);
1164
+ if (options.verbose) terminal.warn(`Part mapping file not found: ${filePath}`);
1145
1165
  return;
1146
1166
  }
1147
1167
 
1148
- if (options.verbose) console.log('Processing Part Mappings...');
1168
+ if (options.verbose) terminal.log('Processing Part Mappings...');
1149
1169
 
1150
1170
  const rl = csvRecords(filePath);
1151
1171
 
@@ -1180,11 +1200,11 @@ class LoincDataMigrator {
1180
1200
  async processIeeeMappings(db, sourceDir, step, options) {
1181
1201
  const filePath = path.join(sourceDir, 'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv');
1182
1202
  if (!fs.existsSync(filePath)) {
1183
- if (options.verbose) console.warn(`IEEE mapping file not found: ${filePath}`);
1203
+ if (options.verbose) terminal.warn(`IEEE mapping file not found: ${filePath}`);
1184
1204
  return;
1185
1205
  }
1186
1206
 
1187
- if (options.verbose) console.log('Processing IEEE Mappings...');
1207
+ if (options.verbose) terminal.log('Processing IEEE Mappings...');
1188
1208
 
1189
1209
  const rl = csvRecords(filePath);
1190
1210
 
@@ -1240,11 +1260,11 @@ class LoincDataMigrator {
1240
1260
  async processPartLinks(db, sourceDir, step, options) {
1241
1261
  const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv');
1242
1262
  if (!fs.existsSync(filePath)) {
1243
- if (options.verbose) console.warn(`Part links file not found: ${filePath}`);
1263
+ if (options.verbose) terminal.warn(`Part links file not found: ${filePath}`);
1244
1264
  return;
1245
1265
  }
1246
1266
 
1247
- if (options.verbose) console.log('Processing Part Links...');
1267
+ if (options.verbose) terminal.log('Processing Part Links...');
1248
1268
 
1249
1269
  const rl = csvRecords(filePath);
1250
1270
 
@@ -1269,6 +1289,9 @@ class LoincDataMigrator {
1269
1289
  this.rels.get(relType),
1270
1290
  this.statii.get(status) || 0
1271
1291
  );
1292
+ if (LINGUISTIC_VARIANT_AXES.includes(relType)) {
1293
+ this.#noteAxisPart(this.codes.get(sourceCode).key, relType, this.codes.get(targetCode).key);
1294
+ }
1272
1295
  }
1273
1296
  processedCount++;
1274
1297
 
@@ -1286,11 +1309,11 @@ class LoincDataMigrator {
1286
1309
  async processListLinks(db, sourceDir, step, options) {
1287
1310
  const filePath = path.join(sourceDir, 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv');
1288
1311
  if (!fs.existsSync(filePath)) {
1289
- if (options.verbose) console.warn(`List links file not found: ${filePath}`);
1312
+ if (options.verbose) terminal.warn(`List links file not found: ${filePath}`);
1290
1313
  return;
1291
1314
  }
1292
1315
 
1293
- if (options.verbose) console.log('Processing List Links...');
1316
+ if (options.verbose) terminal.log('Processing List Links...');
1294
1317
 
1295
1318
  const rl = csvRecords(filePath);
1296
1319
 
@@ -1343,7 +1366,7 @@ class LoincDataMigrator {
1343
1366
  throw new Error(`Required file missing: ${filePath}`);
1344
1367
  }
1345
1368
 
1346
- if (options.verbose) console.log('Processing Hierarchy...');
1369
+ if (options.verbose) terminal.log('Processing Hierarchy...');
1347
1370
 
1348
1371
  const rl = csvRecords(filePath);
1349
1372
 
@@ -1407,7 +1430,7 @@ class LoincDataMigrator {
1407
1430
  }
1408
1431
 
1409
1432
  async processPropertyValues(db, step, options) {
1410
- if (options.verbose) console.log('Processing Property Values...');
1433
+ if (options.verbose) terminal.log('Processing Property Values...');
1411
1434
 
1412
1435
  for (const [value, key] of this.propValues) {
1413
1436
  db.run('INSERT INTO PropertyValues (PropertyValueKey, Value) VALUES (?, ?)', [key, value]);
@@ -1415,7 +1438,7 @@ class LoincDataMigrator {
1415
1438
  }
1416
1439
 
1417
1440
  async storeClosureTable(db, step, options) {
1418
- if (options.verbose) console.log('Storing Closure Table...');
1441
+ if (options.verbose) terminal.log('Storing Closure Table...');
1419
1442
 
1420
1443
  let count = 0;
1421
1444
  for (const codeInfo of this.codeList) {
@@ -1440,7 +1463,7 @@ class LoincDataMigrator {
1440
1463
  }
1441
1464
 
1442
1465
  async processLanguage(db, sourceDir, step, langCode, options) {
1443
- if (options.verbose) console.log(`Processing Language ${langCode}...`);
1466
+ if (options.verbose) terminal.log(`Processing Language ${langCode}...`);
1444
1467
 
1445
1468
  const langKey = this.langs.get(langCode);
1446
1469
  if (!langKey) return;
@@ -1450,7 +1473,7 @@ class LoincDataMigrator {
1450
1473
  const filePath = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants', fileName);
1451
1474
 
1452
1475
  if (!fs.existsSync(filePath)) {
1453
- if (options.verbose) console.warn(`Language file not found: ${filePath}`);
1476
+ if (options.verbose) terminal.warn(`Language file not found: ${filePath}`);
1454
1477
  return;
1455
1478
  }
1456
1479
 
@@ -1472,6 +1495,24 @@ class LoincDataMigrator {
1472
1495
  this.addDescription(db, codeKey, langKey, this.dTypes.get('SHORTNAME'), items[8]);
1473
1496
  this.addDescription(db, codeKey, langKey, this.dTypes.get('LinguisticVariantDisplayName'), items[11]);
1474
1497
 
1498
+ // The axis columns (COMPONENT ... CLASS) translate the term's Parts. Each becomes a
1499
+ // PartName designation on the Part the term links to for that axis - for some languages
1500
+ // (ko-KR, most of fr-CA) these are the only translations LOINC has. Each distinct name once
1501
+ const axes = this.axisParts.get(codeKey);
1502
+ if (axes) {
1503
+ LINGUISTIC_VARIANT_AXES.forEach((axis, i) => {
1504
+ const value = items[i + 1] ? items[i + 1].trim() : '';
1505
+ const partKey = axes[axis];
1506
+ if (value && partKey) {
1507
+ const id = partKey + '|' + langKey + '|' + value;
1508
+ if (!this.partNamesSeen.has(id)) {
1509
+ this.partNamesSeen.add(id);
1510
+ this.addDescription(db, partKey, langKey, this.dTypes.get('PartName'), value);
1511
+ }
1512
+ }
1513
+ });
1514
+ }
1515
+
1475
1516
  processedCount++;
1476
1517
 
1477
1518
  if (processedCount % 100 === 0) {
@@ -1485,6 +1526,15 @@ class LoincDataMigrator {
1485
1526
  }
1486
1527
  }
1487
1528
 
1529
+ #noteAxisPart(termKey, axis, partKey) {
1530
+ let axes = this.axisParts.get(termKey);
1531
+ if (!axes) {
1532
+ axes = {};
1533
+ this.axisParts.set(termKey, axes);
1534
+ }
1535
+ axes[axis] = partKey;
1536
+ }
1537
+
1488
1538
  addDescription(db, codeKey, languageKey, descriptionType, value) {
1489
1539
  if (!value) return;
1490
1540
 
@@ -1508,6 +1558,14 @@ class LoincDataMigrator {
1508
1558
 
1509
1559
  addRelationship(db, sourceKey, targetKey, relationshipType, statusKey = 0) {
1510
1560
  if (!relationshipType) return;
1561
+ // each relationship once: the source files repeat some (a part under the same parent on
1562
+ // two paths in ComponentHierarchyBySystem, the same answer list linked more than once)
1563
+ if (!this.relSeen) {
1564
+ this.relSeen = new Set();
1565
+ }
1566
+ const id = relationshipType + '|' + sourceKey + '|' + targetKey;
1567
+ if (this.relSeen.has(id)) return;
1568
+ this.relSeen.add(id);
1511
1569
 
1512
1570
  this.relKey++;
1513
1571
  db.run('INSERT INTO Relationships (RelationshipKey, RelationshipTypeKey, SourceKey, TargetKey, StatusKey) VALUES (?, ?, ?, ?, ?)',
@@ -1528,7 +1586,7 @@ class LoincDataMigrator {
1528
1586
  return new Promise((resolve) => {
1529
1587
  db.close((err) => {
1530
1588
  if (err && verbose) {
1531
- console.error('Error closing database:', err);
1589
+ terminal.error('Error closing database:', err);
1532
1590
  }
1533
1591
  resolve();
1534
1592
  });
@@ -1537,6 +1595,10 @@ class LoincDataMigrator {
1537
1595
  }
1538
1596
 
1539
1597
  // Property name mappings from Pascal code
1598
+ // The axis columns of a LinguisticVariant file, in file order (columns 1 to 7), named as the
1599
+ // term's relationships to its Parts are
1600
+ const LINGUISTIC_VARIANT_AXES = ['COMPONENT', 'PROPERTY', 'TIME_ASPCT', 'SYSTEM', 'SCALE_TYP', 'METHOD_TYP', 'CLASS'];
1601
+
1540
1602
  const KNOWN_PROPERTY_NAMES = [
1541
1603
  'AskAtOrderEntry', 'AssociatedObservations', 'CHANGE_REASON_PUBLIC', 'CHNG_TYPE', 'CLASS', 'CLASSTYPE', 'COMMON_ORDER_RANK', 'COMMON_TEST_RANK', 'COMPONENT', 'CONSUMER_NAME',
1542
1604
  'DefinitionDescription', 'DisplayName', 'EXAMPLE_UCUM_UNITS', 'EXAMPLE_UNITS', 'EXMPL_ANSWERS', 'EXTERNAL_COPYRIGHT_LINK', 'EXTERNAL_COPYRIGHT_NOTICE', 'FORMULA',