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.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const sqlite3 = require('sqlite3').verbose();
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const terminal = require('./terminal');
|
|
6
7
|
|
|
7
8
|
class NdcModule extends BaseTerminologyModule {
|
|
8
9
|
getName() {
|
|
@@ -94,18 +95,18 @@ class NdcModule extends BaseTerminologyModule {
|
|
|
94
95
|
const inquirer = require('inquirer');
|
|
95
96
|
const chalk = require('chalk');
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
|
|
99
|
+
terminal.log(` Source: ${chalk.white(config.source)}`);
|
|
100
|
+
terminal.log(` Destination: ${chalk.white(config.dest)}`);
|
|
101
|
+
terminal.log(` Version: ${chalk.white(config.version)}`);
|
|
102
|
+
terminal.log(` Products Only: ${chalk.white(config.productsOnly ? 'Yes' : 'No')}`);
|
|
103
|
+
terminal.log(` Packages Only: ${chalk.white(config.packagesOnly ? 'Yes' : 'No')}`);
|
|
104
|
+
terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
|
|
105
|
+
terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
106
|
+
terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
|
|
106
107
|
|
|
107
108
|
if (config.estimatedDuration) {
|
|
108
|
-
|
|
109
|
+
terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
const { confirmed } = await inquirer.prompt({
|
|
@@ -135,14 +136,14 @@ class NdcModule extends BaseTerminologyModule {
|
|
|
135
136
|
const stats = await this.validateNdcDirectory(options.source, parseInt(options.sample));
|
|
136
137
|
|
|
137
138
|
this.logSuccess('Directory validation passed');
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
terminal.log(` Versions found: ${stats.versions.length}`);
|
|
140
|
+
terminal.log(` Total products estimated: ${stats.totalProducts.toLocaleString()}`);
|
|
141
|
+
terminal.log(` Total packages estimated: ${stats.totalPackages.toLocaleString()}`);
|
|
142
|
+
terminal.log(` Versions: ${stats.versions.join(', ')}`);
|
|
142
143
|
|
|
143
144
|
if (stats.warnings.length > 0) {
|
|
144
145
|
this.logWarning('Validation warnings:');
|
|
145
|
-
stats.warnings.forEach(warning =>
|
|
146
|
+
stats.warnings.forEach(warning => terminal.log(` ${warning}`));
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
} catch (error) {
|
|
@@ -166,7 +167,7 @@ class NdcModule extends BaseTerminologyModule {
|
|
|
166
167
|
|
|
167
168
|
this.logSuccess(`Found ${versions.length} NDC versions:`);
|
|
168
169
|
versions.forEach((version, index) => {
|
|
169
|
-
|
|
170
|
+
terminal.log(` ${index + 1}. ${version}`);
|
|
170
171
|
});
|
|
171
172
|
|
|
172
173
|
} catch (error) {
|
|
@@ -188,13 +189,13 @@ class NdcModule extends BaseTerminologyModule {
|
|
|
188
189
|
const stats = await this.getDatabaseStats(dbPath);
|
|
189
190
|
|
|
190
191
|
this.logSuccess('Database status:');
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
terminal.log(` Versions: ${stats.versions.join(', ')}`);
|
|
193
|
+
terminal.log(` Products: ${stats.productCount.toLocaleString()}`);
|
|
194
|
+
terminal.log(` Packages: ${stats.packageCount.toLocaleString()}`);
|
|
195
|
+
terminal.log(` Organizations: ${stats.orgCount.toLocaleString()}`);
|
|
196
|
+
terminal.log(` Product Types: ${stats.typeCount.toLocaleString()}`);
|
|
197
|
+
terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
|
|
198
|
+
terminal.log(` Last Modified: ${stats.lastModified}`);
|
|
198
199
|
|
|
199
200
|
} catch (error) {
|
|
200
201
|
this.logError(`Status check failed: ${error.message}`);
|
|
@@ -427,7 +428,7 @@ class NdcModule extends BaseTerminologyModule {
|
|
|
427
428
|
db.serialize(() => {
|
|
428
429
|
indexes.forEach(sql => {
|
|
429
430
|
db.run(sql, (err) => {
|
|
430
|
-
if (err)
|
|
431
|
+
if (err) terminal.warn(`Index creation warning: ${err.message}`);
|
|
431
432
|
});
|
|
432
433
|
});
|
|
433
434
|
});
|
|
@@ -447,7 +448,7 @@ class NdcDataMigrator {
|
|
|
447
448
|
}
|
|
448
449
|
|
|
449
450
|
async migrate(sourceDir, destFile = 'unknown', options = {}) {
|
|
450
|
-
if (options.verbose)
|
|
451
|
+
if (options.verbose) terminal.log('Starting NDC data migration...');
|
|
451
452
|
|
|
452
453
|
// Ensure destination directory exists
|
|
453
454
|
const destDir = path.dirname(destFile);
|
|
@@ -471,12 +472,12 @@ class NdcDataMigrator {
|
|
|
471
472
|
const versions = await this.#findVersions(sourceDir);
|
|
472
473
|
|
|
473
474
|
if (options.verbose) {
|
|
474
|
-
|
|
475
|
+
terminal.log(`Found ${versions.length} versions: ${versions.join(', ')}`);
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
// Process each version
|
|
478
479
|
for (const versionName of versions) {
|
|
479
|
-
if (options.verbose)
|
|
480
|
+
if (options.verbose) terminal.log(`Processing version: ${versionName}`);
|
|
480
481
|
|
|
481
482
|
await this.#processVersion(db, sourceDir, versionName, options);
|
|
482
483
|
}
|
|
@@ -489,7 +490,7 @@ class NdcDataMigrator {
|
|
|
489
490
|
// Create lookup tables
|
|
490
491
|
await this.#createLookupTables(db, options.verbose);
|
|
491
492
|
|
|
492
|
-
if (options.verbose)
|
|
493
|
+
if (options.verbose) terminal.log('NDC data migration completed successfully');
|
|
493
494
|
} finally {
|
|
494
495
|
await this.#closeDatabase(db, options.verbose);
|
|
495
496
|
}
|
|
@@ -511,7 +512,7 @@ class NdcDataMigrator {
|
|
|
511
512
|
}
|
|
512
513
|
|
|
513
514
|
async #createTables(db, verbose = true) {
|
|
514
|
-
if (verbose)
|
|
515
|
+
if (verbose) terminal.log('Creating database tables...');
|
|
515
516
|
|
|
516
517
|
const tableSQL = [
|
|
517
518
|
`CREATE TABLE NDCProducts (
|
|
@@ -574,7 +575,7 @@ class NdcDataMigrator {
|
|
|
574
575
|
});
|
|
575
576
|
});
|
|
576
577
|
|
|
577
|
-
if (verbose)
|
|
578
|
+
if (verbose) terminal.log('Database tables created');
|
|
578
579
|
resolve();
|
|
579
580
|
});
|
|
580
581
|
});
|
|
@@ -599,11 +600,11 @@ class NdcDataMigrator {
|
|
|
599
600
|
const productFile = path.join(versionDir, 'product.txt');
|
|
600
601
|
|
|
601
602
|
if (!fs.existsSync(productFile)) {
|
|
602
|
-
if (options.verbose)
|
|
603
|
+
if (options.verbose) terminal.warn(`Product file not found: ${productFile}`);
|
|
603
604
|
return;
|
|
604
605
|
}
|
|
605
606
|
|
|
606
|
-
if (options.verbose)
|
|
607
|
+
if (options.verbose) terminal.log(`Processing products from ${versionName}...`);
|
|
607
608
|
|
|
608
609
|
const rl = readline.createInterface({
|
|
609
610
|
input: fs.createReadStream(productFile),
|
|
@@ -668,7 +669,7 @@ class NdcDataMigrator {
|
|
|
668
669
|
batch = [];
|
|
669
670
|
|
|
670
671
|
if (processedCount % 10000 === 0 && options.verbose) {
|
|
671
|
-
|
|
672
|
+
terminal.log(` Processed ${processedCount} products...`);
|
|
672
673
|
}
|
|
673
674
|
}
|
|
674
675
|
}
|
|
@@ -690,7 +691,7 @@ class NdcDataMigrator {
|
|
|
690
691
|
this.codesMap = codesMap;
|
|
691
692
|
|
|
692
693
|
if (options.verbose) {
|
|
693
|
-
|
|
694
|
+
terminal.log(` Completed products: ${processedCount} records`);
|
|
694
695
|
}
|
|
695
696
|
}
|
|
696
697
|
|
|
@@ -698,11 +699,11 @@ class NdcDataMigrator {
|
|
|
698
699
|
const packageFile = path.join(versionDir, 'package.txt');
|
|
699
700
|
|
|
700
701
|
if (!fs.existsSync(packageFile)) {
|
|
701
|
-
if (options.verbose)
|
|
702
|
+
if (options.verbose) terminal.warn(`Package file not found: ${packageFile}`);
|
|
702
703
|
return;
|
|
703
704
|
}
|
|
704
705
|
|
|
705
|
-
if (options.verbose)
|
|
706
|
+
if (options.verbose) terminal.log(`Processing packages from ${versionName}...`);
|
|
706
707
|
|
|
707
708
|
const rl = readline.createInterface({
|
|
708
709
|
input: fs.createReadStream(packageFile),
|
|
@@ -763,7 +764,7 @@ class NdcDataMigrator {
|
|
|
763
764
|
batch = [];
|
|
764
765
|
|
|
765
766
|
if (processedCount % 10000 === 0 && options.verbose) {
|
|
766
|
-
|
|
767
|
+
terminal.log(` Processed ${processedCount} packages...`);
|
|
767
768
|
}
|
|
768
769
|
}
|
|
769
770
|
}
|
|
@@ -778,7 +779,7 @@ class NdcDataMigrator {
|
|
|
778
779
|
insertPackage.finalize();
|
|
779
780
|
|
|
780
781
|
if (options.verbose) {
|
|
781
|
-
|
|
782
|
+
terminal.log(` Completed packages: ${processedCount} records`);
|
|
782
783
|
}
|
|
783
784
|
}
|
|
784
785
|
|
|
@@ -968,7 +969,7 @@ class NdcDataMigrator {
|
|
|
968
969
|
}
|
|
969
970
|
|
|
970
971
|
async #createLookupTables(db, verbose = true) {
|
|
971
|
-
if (verbose)
|
|
972
|
+
if (verbose) terminal.log('Creating lookup tables...');
|
|
972
973
|
|
|
973
974
|
const lookups = [
|
|
974
975
|
{ table: 'NDCProductTypes', map: this.typesMap },
|
|
@@ -977,8 +978,12 @@ class NdcDataMigrator {
|
|
|
977
978
|
{ table: 'NDCRoutes', map: this.routesMap }
|
|
978
979
|
];
|
|
979
980
|
|
|
980
|
-
|
|
981
|
+
// One transaction for all of them: otherwise each row commits (and waits on a disk sync)
|
|
982
|
+
// on its own, and there are thousands of organisations. A row that fails is warned about
|
|
983
|
+
// and skipped, as before: in SQLite a failed INSERT doesn't end the transaction
|
|
984
|
+
return new Promise((resolve, reject) => {
|
|
981
985
|
db.serialize(() => {
|
|
986
|
+
db.run('BEGIN TRANSACTION');
|
|
982
987
|
lookups.forEach(lookup => {
|
|
983
988
|
if (lookup.map) {
|
|
984
989
|
for (const [name, id] of lookup.map) {
|
|
@@ -987,7 +992,7 @@ class NdcDataMigrator {
|
|
|
987
992
|
[id, name.substring(0, 500)],
|
|
988
993
|
(err) => {
|
|
989
994
|
if (err && verbose) {
|
|
990
|
-
|
|
995
|
+
terminal.warn(`Warning inserting into ${lookup.table}: ${err.message}`);
|
|
991
996
|
}
|
|
992
997
|
}
|
|
993
998
|
);
|
|
@@ -995,8 +1000,15 @@ class NdcDataMigrator {
|
|
|
995
1000
|
}
|
|
996
1001
|
});
|
|
997
1002
|
|
|
998
|
-
|
|
999
|
-
|
|
1003
|
+
// resolves once the rows are in, not when they have only been queued
|
|
1004
|
+
db.run('COMMIT', (err) => {
|
|
1005
|
+
if (err) {
|
|
1006
|
+
reject(err);
|
|
1007
|
+
} else {
|
|
1008
|
+
if (verbose) terminal.log('Lookup tables created');
|
|
1009
|
+
resolve();
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1000
1012
|
});
|
|
1001
1013
|
});
|
|
1002
1014
|
}
|
|
@@ -1005,7 +1017,7 @@ class NdcDataMigrator {
|
|
|
1005
1017
|
return new Promise((resolve) => {
|
|
1006
1018
|
db.close((err) => {
|
|
1007
1019
|
if (err && verbose) {
|
|
1008
|
-
|
|
1020
|
+
terminal.error('Error closing database:', err);
|
|
1009
1021
|
}
|
|
1010
1022
|
resolve();
|
|
1011
1023
|
});
|
|
@@ -4,6 +4,7 @@ const sqlite3 = require('sqlite3').verbose();
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
|
+
const terminal = require('./terminal');
|
|
7
8
|
|
|
8
9
|
class RxNormModule extends BaseTerminologyModule {
|
|
9
10
|
constructor() {
|
|
@@ -98,7 +99,7 @@ class RxNormModule extends BaseTerminologyModule {
|
|
|
98
99
|
} catch (error) {
|
|
99
100
|
this.logError(`Import command failed: ${error.message}`);
|
|
100
101
|
if (options.verbose) {
|
|
101
|
-
|
|
102
|
+
terminal.error(error.stack);
|
|
102
103
|
}
|
|
103
104
|
throw error;
|
|
104
105
|
}
|
|
@@ -121,17 +122,17 @@ class RxNormModule extends BaseTerminologyModule {
|
|
|
121
122
|
async confirmImport(config) {
|
|
122
123
|
const inquirer = require('inquirer');
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
|
|
126
|
+
terminal.log(` Source: ${chalk.white(config.source)}`);
|
|
127
|
+
terminal.log(` Destination: ${chalk.white(config.dest)}`);
|
|
128
|
+
terminal.log(` Version: ${chalk.white(config.version || 'Auto-detect')}`);
|
|
129
|
+
terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
|
|
130
|
+
terminal.log(` Create Stems: ${chalk.white(config.createStems ? 'Yes' : 'No')}`);
|
|
131
|
+
terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
132
|
+
terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
|
|
132
133
|
|
|
133
134
|
if (config.estimatedDuration) {
|
|
134
|
-
|
|
135
|
+
terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
const { confirmed } = await inquirer.prompt({
|
|
@@ -161,14 +162,14 @@ class RxNormModule extends BaseTerminologyModule {
|
|
|
161
162
|
const stats = await this.validateRxNormDirectory(options.source);
|
|
162
163
|
|
|
163
164
|
this.logSuccess('Directory validation passed');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
terminal.log(` Required files found: ${stats.requiredFiles.length}/3`);
|
|
166
|
+
terminal.log(` Optional files found: ${stats.optionalFiles.length}/3`);
|
|
167
|
+
terminal.log(` Estimated concepts: ${stats.estimatedConcepts.toLocaleString()}`);
|
|
168
|
+
terminal.log(` Estimated relationships: ${stats.estimatedRelationships.toLocaleString()}`);
|
|
168
169
|
|
|
169
170
|
if (stats.warnings.length > 0) {
|
|
170
171
|
this.logWarning('Validation warnings:');
|
|
171
|
-
stats.warnings.forEach(warning =>
|
|
172
|
+
stats.warnings.forEach(warning => terminal.log(` ${warning}`));
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
} catch (error) {
|
|
@@ -190,13 +191,13 @@ class RxNormModule extends BaseTerminologyModule {
|
|
|
190
191
|
const stats = await this.getDatabaseStats(dbPath);
|
|
191
192
|
|
|
192
193
|
this.logSuccess('Database status:');
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
terminal.log(` Version: ${stats.version}`);
|
|
195
|
+
terminal.log(` RXNCONSO Records: ${stats.rxnconsoCount.toLocaleString()}`);
|
|
196
|
+
terminal.log(` RXNREL Records: ${stats.rxnrelCount.toLocaleString()}`);
|
|
197
|
+
terminal.log(` RXNSTY Records: ${stats.rxnstyCount.toLocaleString()}`);
|
|
198
|
+
terminal.log(` Stem Records: ${stats.stemCount.toLocaleString()}`);
|
|
199
|
+
terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
|
|
200
|
+
terminal.log(` Last Modified: ${stats.lastModified}`);
|
|
200
201
|
|
|
201
202
|
} catch (error) {
|
|
202
203
|
this.logError(`Status check failed: ${error.message}`);
|
|
@@ -428,15 +429,20 @@ class RxNormModule extends BaseTerminologyModule {
|
|
|
428
429
|
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rel ON RXNREL(REL)',
|
|
429
430
|
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela ON RXNREL(RELA)',
|
|
430
431
|
'CREATE INDEX IF NOT EXISTS X_RXNSTY_2 ON RXNSTY(TUI)',
|
|
432
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnsty_rxcui ON RXNSTY(RXCUI)',
|
|
433
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rxcui2 ON RXNREL(RXCUI2, SAB)',
|
|
434
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela_rxcui1 ON RXNREL(RELA, RXCUI1)',
|
|
435
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela_rxaui1 ON RXNREL(RELA, RXAUI1)',
|
|
431
436
|
'CREATE INDEX IF NOT EXISTS idx_rxnstems_stem_cui ON RXNSTEMS(stem, CUI)',
|
|
432
|
-
'CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)'
|
|
437
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)',
|
|
438
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnatomarchive_rxcui_sab ON RXNATOMARCHIVE(RXCUI, SAB)'
|
|
433
439
|
];
|
|
434
440
|
|
|
435
441
|
return new Promise((resolve, reject) => {
|
|
436
442
|
db.serialize(() => {
|
|
437
443
|
indexes.forEach(sql => {
|
|
438
444
|
db.run(sql, (err) => {
|
|
439
|
-
if (err)
|
|
445
|
+
if (err) terminal.warn(`Index creation warning: ${err.message}`);
|
|
440
446
|
});
|
|
441
447
|
});
|
|
442
448
|
});
|
|
@@ -472,7 +478,7 @@ class RxNormImporter {
|
|
|
472
478
|
}
|
|
473
479
|
|
|
474
480
|
async import() {
|
|
475
|
-
if (this.options.verbose)
|
|
481
|
+
if (this.options.verbose) terminal.log('Starting RxNorm import...');
|
|
476
482
|
|
|
477
483
|
// Ensure destination directory exists
|
|
478
484
|
const destDir = path.dirname(this.destFile);
|
|
@@ -501,7 +507,7 @@ class RxNormImporter {
|
|
|
501
507
|
await this.makeStems(db);
|
|
502
508
|
}
|
|
503
509
|
|
|
504
|
-
if (this.options.verbose)
|
|
510
|
+
if (this.options.verbose) terminal.log('RxNorm import completed successfully');
|
|
505
511
|
} finally {
|
|
506
512
|
await this.closeDatabase(db);
|
|
507
513
|
}
|
|
@@ -634,12 +640,12 @@ class RxNormImporter {
|
|
|
634
640
|
tableSQL.forEach(sql => {
|
|
635
641
|
db.run(sql, (err) => {
|
|
636
642
|
if (err && this.options.verbose) {
|
|
637
|
-
|
|
643
|
+
terminal.warn(`Table creation warning: ${err.message}`);
|
|
638
644
|
}
|
|
639
645
|
});
|
|
640
646
|
});
|
|
641
647
|
|
|
642
|
-
if (this.options.verbose)
|
|
648
|
+
if (this.options.verbose) terminal.log('Database tables created');
|
|
643
649
|
resolve();
|
|
644
650
|
});
|
|
645
651
|
});
|
|
@@ -701,83 +707,116 @@ class RxNormImporter {
|
|
|
701
707
|
async loadRRFFile(db, fileName, insertSQL, extractValues) {
|
|
702
708
|
const filePath = path.join(this.sourceDir, fileName);
|
|
703
709
|
if (!fs.existsSync(filePath)) {
|
|
704
|
-
if (this.options.verbose)
|
|
710
|
+
if (this.options.verbose) terminal.warn(`File not found: ${fileName}`);
|
|
705
711
|
return;
|
|
706
712
|
}
|
|
707
713
|
|
|
708
714
|
this.updateProgress(0, `Loading ${fileName}`);
|
|
709
715
|
|
|
716
|
+
// Rows go in as multi-row INSERTs, as many rows per statement as fit in SQLite's 999
|
|
717
|
+
// parameters: one statement (and one callback) per row made the import slow, and slower
|
|
718
|
+
// still when the process is busy collecting a large heap - the test suite runs it in a
|
|
719
|
+
// jest process already holding GBs. OR IGNORE keeps the old per-row behaviour, where a row
|
|
720
|
+
// that broke a constraint was dropped and the rest of the file went in
|
|
721
|
+
const m = insertSQL.match(/^\s*INSERT\s+(?:OR\s+IGNORE\s+)?INTO([\s\S]*?VALUES\s*)(\([^)]*\))\s*$/i);
|
|
722
|
+
if (!m) {
|
|
723
|
+
throw new Error(`Unexpected insert statement for ${fileName}: ${insertSQL}`);
|
|
724
|
+
}
|
|
725
|
+
const head = 'INSERT OR IGNORE INTO' + m[1];
|
|
726
|
+
const group = m[2];
|
|
727
|
+
const cols = (group.match(/\?/g) || []).length;
|
|
728
|
+
const batchRows = Math.max(1, Math.floor(999 / cols));
|
|
729
|
+
const sqlFor = (n) => head + new Array(n).fill(group).join(', ');
|
|
730
|
+
const fullSql = sqlFor(batchRows);
|
|
731
|
+
|
|
710
732
|
const rl = readline.createInterface({
|
|
711
733
|
input: fs.createReadStream(filePath),
|
|
712
734
|
crlfDelay: Infinity
|
|
713
735
|
});
|
|
714
736
|
|
|
715
737
|
return new Promise((resolve, reject) => {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const stmt = db.prepare(insertSQL);
|
|
720
|
-
let lineCount = 0;
|
|
721
|
-
let processedCount = 0;
|
|
722
|
-
let errorCount = 0;
|
|
723
|
-
|
|
724
|
-
rl.on('line', (line) => {
|
|
725
|
-
lineCount++;
|
|
726
|
-
|
|
727
|
-
const items = line.split('|');
|
|
728
|
-
if (items.length === 0) return;
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
const values = extractValues(items);
|
|
732
|
-
stmt.run(values, (err) => {
|
|
733
|
-
if (err && err.code !== 'SQLITE_CONSTRAINT') {
|
|
734
|
-
errorCount++;
|
|
735
|
-
if (this.options.verbose && errorCount <= 10) {
|
|
736
|
-
console.warn(`Error processing line ${lineCount} in ${fileName}: ${err.message}`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
processedCount++;
|
|
738
|
+
// serialized: BEGIN, every batch, and COMMIT run in the order they are queued
|
|
739
|
+
db.serialize();
|
|
740
|
+
db.run('BEGIN TRANSACTION');
|
|
741
741
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
742
|
+
let lineCount = 0;
|
|
743
|
+
let processedCount = 0;
|
|
744
|
+
let errorCount = 0;
|
|
745
|
+
let pending = [];
|
|
746
|
+
let pendingRows = 0;
|
|
747
|
+
|
|
748
|
+
const flush = () => {
|
|
749
|
+
if (pendingRows === 0) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const rows = pendingRows;
|
|
753
|
+
db.run(rows === batchRows ? fullSql : sqlFor(rows), pending, (err) => {
|
|
754
|
+
if (err) {
|
|
746
755
|
errorCount++;
|
|
747
756
|
if (this.options.verbose && errorCount <= 10) {
|
|
748
|
-
|
|
757
|
+
terminal.warn(`Error loading ${rows} rows of ${fileName}: ${err.message}`);
|
|
749
758
|
}
|
|
750
759
|
}
|
|
751
760
|
});
|
|
761
|
+
pending = [];
|
|
762
|
+
pendingRows = 0;
|
|
763
|
+
};
|
|
752
764
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
db.run('COMMIT', (err) => {
|
|
756
|
-
if (err) reject(err);
|
|
757
|
-
else {
|
|
758
|
-
// Update progress for remaining records
|
|
759
|
-
const remaining = processedCount % 1000;
|
|
760
|
-
if (remaining > 0) {
|
|
761
|
-
this.updateProgress(remaining);
|
|
762
|
-
}
|
|
765
|
+
rl.on('line', (line) => {
|
|
766
|
+
lineCount++;
|
|
763
767
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
+
const items = line.split('|');
|
|
769
|
+
if (items.length === 0) return;
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
const values = extractValues(items);
|
|
773
|
+
for (let i = 0; i < cols; i++) {
|
|
774
|
+
pending.push(values[i] === undefined ? null : values[i]);
|
|
775
|
+
}
|
|
776
|
+
pendingRows++;
|
|
777
|
+
if (pendingRows === batchRows) {
|
|
778
|
+
flush();
|
|
779
|
+
}
|
|
780
|
+
processedCount++;
|
|
781
|
+
|
|
782
|
+
if (processedCount % 1000 === 0) {
|
|
783
|
+
this.updateProgress(1000);
|
|
784
|
+
}
|
|
785
|
+
} catch (error) {
|
|
786
|
+
errorCount++;
|
|
787
|
+
if (this.options.verbose && errorCount <= 10) {
|
|
788
|
+
terminal.warn(`Error processing line ${lineCount} in ${fileName}: ${error.message}`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
rl.on('close', () => {
|
|
794
|
+
flush();
|
|
795
|
+
db.run('COMMIT', (err) => {
|
|
796
|
+
if (err) reject(err);
|
|
797
|
+
else {
|
|
798
|
+
// Update progress for remaining records
|
|
799
|
+
const remaining = processedCount % 1000;
|
|
800
|
+
if (remaining > 0) {
|
|
801
|
+
this.updateProgress(remaining);
|
|
768
802
|
}
|
|
769
|
-
});
|
|
770
|
-
});
|
|
771
803
|
|
|
772
|
-
|
|
804
|
+
if (this.options.verbose) {
|
|
805
|
+
terminal.log(` Loaded ${processedCount} records from ${fileName}${errorCount > 0 ? ` (${errorCount} errors)` : ''}`);
|
|
806
|
+
}
|
|
807
|
+
resolve();
|
|
808
|
+
}
|
|
809
|
+
});
|
|
773
810
|
});
|
|
811
|
+
|
|
812
|
+
rl.on('error', reject);
|
|
774
813
|
});
|
|
775
814
|
}
|
|
776
815
|
|
|
777
816
|
async makeStems(db) {
|
|
778
817
|
this.updateProgress(0, 'Generating Stems');
|
|
779
818
|
|
|
780
|
-
if (this.options.verbose)
|
|
819
|
+
if (this.options.verbose) terminal.log('Generating word stems...');
|
|
781
820
|
|
|
782
821
|
// Simple English stemmer implementation
|
|
783
822
|
var natural = require('natural/lib/natural/stemmers'); // just the stemmers: the package index also loads its storage backends, and dotenv with them
|
|
@@ -833,7 +872,7 @@ class RxNormImporter {
|
|
|
833
872
|
}
|
|
834
873
|
|
|
835
874
|
if (this.options.verbose) {
|
|
836
|
-
|
|
875
|
+
terminal.log(` Generated ${totalInserts} stem entries from ${processedStems} unique stems`);
|
|
837
876
|
}
|
|
838
877
|
resolve();
|
|
839
878
|
}
|
|
@@ -857,7 +896,7 @@ class RxNormImporter {
|
|
|
857
896
|
return new Promise((resolve) => {
|
|
858
897
|
db.close((err) => {
|
|
859
898
|
if (err && this.options.verbose) {
|
|
860
|
-
|
|
899
|
+
terminal.error('Error closing database:', err);
|
|
861
900
|
}
|
|
862
901
|
resolve();
|
|
863
902
|
});
|