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.
- package/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
|
@@ -0,0 +1,898 @@
|
|
|
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 RxNormModule extends BaseTerminologyModule {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getName() {
|
|
14
|
+
return 'rxnorm';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getDescription() {
|
|
18
|
+
return 'RxNorm prescribable drug nomenclature from the National Library of Medicine';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getSupportedFormats() {
|
|
22
|
+
return ['rrf', 'directory'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getDefaultConfig() {
|
|
26
|
+
return {
|
|
27
|
+
...super.getDefaultConfig(),
|
|
28
|
+
createIndexes: true,
|
|
29
|
+
createStems: true,
|
|
30
|
+
dest: './data/rxnorm.db'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getEstimatedDuration() {
|
|
35
|
+
return '15-45 minutes (depending on stem generation)';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
registerCommands(terminologyCommand, globalOptions) {
|
|
39
|
+
// Import command
|
|
40
|
+
terminologyCommand
|
|
41
|
+
.command('import')
|
|
42
|
+
.description('Import RxNorm data from source directory')
|
|
43
|
+
.option('-s, --source <directory>', 'Source directory containing RRF files')
|
|
44
|
+
.option('-d, --dest <file>', 'Destination SQLite database')
|
|
45
|
+
.option('-v, --version <version>', 'RxNorm version identifier')
|
|
46
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
47
|
+
.option('--no-indexes', 'Skip index creation for faster import')
|
|
48
|
+
.option('--no-stems', 'Skip stem generation for faster import')
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
await this.handleImportCommand({...globalOptions, ...options});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Validate command
|
|
54
|
+
terminologyCommand
|
|
55
|
+
.command('validate')
|
|
56
|
+
.description('Validate RxNorm source directory structure')
|
|
57
|
+
.option('-s, --source <directory>', 'Source directory to validate')
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
await this.handleValidateCommand({...globalOptions, ...options});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Status command
|
|
63
|
+
terminologyCommand
|
|
64
|
+
.command('status')
|
|
65
|
+
.description('Show status of RxNorm database')
|
|
66
|
+
.option('-d, --dest <file>', 'Database file to check')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
await this.handleStatusCommand({...globalOptions, ...options});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async handleImportCommand(options) {
|
|
73
|
+
try {
|
|
74
|
+
// Gather configuration with remembered values
|
|
75
|
+
const config = await this.gatherCommonConfig(options);
|
|
76
|
+
|
|
77
|
+
// RxNorm-specific configuration
|
|
78
|
+
config.createIndexes = !options.noIndexes;
|
|
79
|
+
config.createStems = !options.noStems;
|
|
80
|
+
config.estimatedDuration = this.getEstimatedDuration();
|
|
81
|
+
|
|
82
|
+
// Auto-detect version from path if not provided
|
|
83
|
+
if (!config.version) {
|
|
84
|
+
config.version = this.detectVersionFromPath(config.source);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show confirmation unless --yes is specified
|
|
88
|
+
if (!options.yes) {
|
|
89
|
+
const confirmed = await this.confirmImport(config);
|
|
90
|
+
if (!confirmed) {
|
|
91
|
+
this.logInfo('Import cancelled');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Run the import
|
|
97
|
+
await this.runImport(config);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.logError(`Import command failed: ${error.message}`);
|
|
100
|
+
if (options.verbose) {
|
|
101
|
+
console.error(error.stack);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
detectVersionFromPath(sourcePath) {
|
|
108
|
+
// Try to extract version from path like "RxNorm_full_08042025"
|
|
109
|
+
const pathMatch = sourcePath.match(/RxNorm_full_(\d{8})/);
|
|
110
|
+
if (pathMatch) {
|
|
111
|
+
const dateStr = pathMatch[1];
|
|
112
|
+
// Convert MMDDYYYY to YYYY-MM-DD format
|
|
113
|
+
const month = dateStr.substring(0, 2);
|
|
114
|
+
const day = dateStr.substring(2, 4);
|
|
115
|
+
const year = dateStr.substring(4, 8);
|
|
116
|
+
return `RXNORM-${year}-${month}-${day}`;
|
|
117
|
+
}
|
|
118
|
+
return 'RXNORM-UNKNOWN';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async confirmImport(config) {
|
|
122
|
+
const inquirer = require('inquirer');
|
|
123
|
+
|
|
124
|
+
console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
|
|
125
|
+
console.log(` Source: ${chalk.white(config.source)}`);
|
|
126
|
+
console.log(` Destination: ${chalk.white(config.dest)}`);
|
|
127
|
+
console.log(` Version: ${chalk.white(config.version || 'Auto-detect')}`);
|
|
128
|
+
console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
|
|
129
|
+
console.log(` Create Stems: ${chalk.white(config.createStems ? 'Yes' : 'No')}`);
|
|
130
|
+
console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
131
|
+
console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
|
|
132
|
+
|
|
133
|
+
if (config.estimatedDuration) {
|
|
134
|
+
console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { confirmed } = await inquirer.prompt({
|
|
138
|
+
type: 'confirm',
|
|
139
|
+
name: 'confirmed',
|
|
140
|
+
message: 'Proceed with import?',
|
|
141
|
+
default: true
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return confirmed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async handleValidateCommand(options) {
|
|
148
|
+
if (!options.source) {
|
|
149
|
+
const answers = await require('inquirer').prompt({
|
|
150
|
+
type: 'input',
|
|
151
|
+
name: 'source',
|
|
152
|
+
message: 'Source directory to validate:',
|
|
153
|
+
validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
|
|
154
|
+
});
|
|
155
|
+
options.source = answers.source;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.logInfo(`Validating RxNorm directory: ${options.source}`);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const stats = await this.validateRxNormDirectory(options.source);
|
|
162
|
+
|
|
163
|
+
this.logSuccess('Directory validation passed');
|
|
164
|
+
console.log(` Required files found: ${stats.requiredFiles.length}/3`);
|
|
165
|
+
console.log(` Optional files found: ${stats.optionalFiles.length}/3`);
|
|
166
|
+
console.log(` Estimated concepts: ${stats.estimatedConcepts.toLocaleString()}`);
|
|
167
|
+
console.log(` Estimated relationships: ${stats.estimatedRelationships.toLocaleString()}`);
|
|
168
|
+
|
|
169
|
+
if (stats.warnings.length > 0) {
|
|
170
|
+
this.logWarning('Validation warnings:');
|
|
171
|
+
stats.warnings.forEach(warning => console.log(` ${warning}`));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.logError(`Validation failed: ${error.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async handleStatusCommand(options) {
|
|
180
|
+
const dbPath = options.dest || './data/rxnorm.db';
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(dbPath)) {
|
|
183
|
+
this.logError(`Database not found: ${dbPath}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.logInfo(`Checking RxNorm database: ${dbPath}`);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const stats = await this.getDatabaseStats(dbPath);
|
|
191
|
+
|
|
192
|
+
this.logSuccess('Database status:');
|
|
193
|
+
console.log(` Version: ${stats.version}`);
|
|
194
|
+
console.log(` RXNCONSO Records: ${stats.rxnconsoCount.toLocaleString()}`);
|
|
195
|
+
console.log(` RXNREL Records: ${stats.rxnrelCount.toLocaleString()}`);
|
|
196
|
+
console.log(` RXNSTY Records: ${stats.rxnstyCount.toLocaleString()}`);
|
|
197
|
+
console.log(` Stem Records: ${stats.stemCount.toLocaleString()}`);
|
|
198
|
+
console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
|
|
199
|
+
console.log(` Last Modified: ${stats.lastModified}`);
|
|
200
|
+
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.logError(`Status check failed: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async validatePrerequisites(config) {
|
|
207
|
+
const baseValid = await super.validatePrerequisites(config);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
this.logInfo('Validating RxNorm directory structure...');
|
|
211
|
+
await this.validateRxNormDirectory(config.source);
|
|
212
|
+
this.logSuccess('RxNorm directory structure valid');
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.logError(`RxNorm directory validation failed: ${error.message}`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return baseValid;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async executeImport(config) {
|
|
222
|
+
this.logInfo('Starting RxNorm data import...');
|
|
223
|
+
|
|
224
|
+
const importer = new RxNormImporter(
|
|
225
|
+
config.source,
|
|
226
|
+
config.dest,
|
|
227
|
+
config.version,
|
|
228
|
+
{
|
|
229
|
+
verbose: config.verbose,
|
|
230
|
+
createStems: config.createStems,
|
|
231
|
+
progressCallback: (current, operation) => {
|
|
232
|
+
if (operation && this.progressBar) {
|
|
233
|
+
// Update operation display
|
|
234
|
+
this.progressBar.update(current, {
|
|
235
|
+
operation: chalk.cyan(operation.padEnd(20).substring(0, 20))
|
|
236
|
+
});
|
|
237
|
+
} else if (this.progressBar) {
|
|
238
|
+
this.progressBar.update(current);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Estimate total work for progress bar
|
|
245
|
+
const totalWork = await this.estimateWorkload(config.source);
|
|
246
|
+
|
|
247
|
+
const progressFormat = '{operation} |{bar}| {percentage}% | {value}/{total} | ETA: {eta}s';
|
|
248
|
+
this.createProgressBar(progressFormat);
|
|
249
|
+
this.progressBar.start(totalWork, 0, {
|
|
250
|
+
operation: chalk.cyan('Starting'.padEnd(20).substring(0, 20))
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await importer.import();
|
|
255
|
+
} finally {
|
|
256
|
+
this.stopProgress();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (config.createIndexes) {
|
|
260
|
+
this.logInfo('Creating database indexes...');
|
|
261
|
+
await this.createIndexes(config.dest);
|
|
262
|
+
this.logSuccess('Indexes created');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async validateRxNormDirectory(sourceDir) {
|
|
267
|
+
if (!fs.existsSync(sourceDir)) {
|
|
268
|
+
throw new Error(`Source directory not found: ${sourceDir}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const requiredFiles = [
|
|
272
|
+
'RXNCONSO.RRF',
|
|
273
|
+
'RXNREL.RRF',
|
|
274
|
+
'RXNSTY.RRF'
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
const optionalFiles = [
|
|
278
|
+
'RXNSAB.RRF',
|
|
279
|
+
'RXNATOMARCHIVE.RRF',
|
|
280
|
+
'RXNCUI.RRF'
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const warnings = [];
|
|
284
|
+
let requiredFound = [];
|
|
285
|
+
let optionalFound = [];
|
|
286
|
+
let estimatedConcepts = 0;
|
|
287
|
+
let estimatedRelationships = 0;
|
|
288
|
+
|
|
289
|
+
// Check required files
|
|
290
|
+
for (const file of requiredFiles) {
|
|
291
|
+
const filePath = path.join(sourceDir, file);
|
|
292
|
+
if (!fs.existsSync(filePath)) {
|
|
293
|
+
throw new Error(`Required file missing: ${file}`);
|
|
294
|
+
}
|
|
295
|
+
requiredFound.push(file);
|
|
296
|
+
|
|
297
|
+
// Estimate record counts
|
|
298
|
+
if (file === 'RXNCONSO.RRF') {
|
|
299
|
+
estimatedConcepts = await this.countLines(filePath) - 1;
|
|
300
|
+
} else if (file === 'RXNREL.RRF') {
|
|
301
|
+
estimatedRelationships = await this.countLines(filePath) - 1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check optional files
|
|
306
|
+
for (const file of optionalFiles) {
|
|
307
|
+
const filePath = path.join(sourceDir, file);
|
|
308
|
+
if (!fs.existsSync(filePath)) {
|
|
309
|
+
warnings.push(`Optional file missing: ${file}`);
|
|
310
|
+
} else {
|
|
311
|
+
optionalFound.push(file);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
requiredFiles: requiredFound,
|
|
317
|
+
optionalFiles: optionalFound,
|
|
318
|
+
estimatedConcepts,
|
|
319
|
+
estimatedRelationships,
|
|
320
|
+
warnings
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async countLines(filePath) {
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
let lineCount = 0;
|
|
327
|
+
const rl = readline.createInterface({
|
|
328
|
+
input: fs.createReadStream(filePath),
|
|
329
|
+
crlfDelay: Infinity
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
rl.on('line', () => lineCount++);
|
|
333
|
+
rl.on('close', () => resolve(lineCount));
|
|
334
|
+
rl.on('error', reject);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async estimateWorkload(sourceDir) {
|
|
339
|
+
let totalWork = 0;
|
|
340
|
+
const files = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF', 'RXNSAB.RRF', 'RXNATOMARCHIVE.RRF', 'RXNCUI.RRF'];
|
|
341
|
+
|
|
342
|
+
for (const file of files) {
|
|
343
|
+
const filePath = path.join(sourceDir, file);
|
|
344
|
+
if (fs.existsSync(filePath)) {
|
|
345
|
+
const lines = await this.countLines(filePath);
|
|
346
|
+
totalWork += Math.max(lines - 1, 0);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Add estimated work for stem generation (roughly equal to concept count)
|
|
351
|
+
const conceptsPath = path.join(sourceDir, 'RXNCONSO.RRF');
|
|
352
|
+
if (fs.existsSync(conceptsPath)) {
|
|
353
|
+
const concepts = await this.countLines(conceptsPath) - 1;
|
|
354
|
+
totalWork += concepts; // Stem generation work
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return Math.max(totalWork, 1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getDatabaseStats(dbPath) {
|
|
361
|
+
const db = new sqlite3.Database(dbPath);
|
|
362
|
+
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
const stats = {};
|
|
365
|
+
|
|
366
|
+
// Try to get version from a version table or derive from path
|
|
367
|
+
db.get('SELECT name FROM sqlite_master WHERE type="table" AND name="RXNVer"', (err, row) => {
|
|
368
|
+
if (row) {
|
|
369
|
+
db.get('SELECT version FROM RXNVer LIMIT 1', (err, versionRow) => {
|
|
370
|
+
stats.version = versionRow ? versionRow.version : 'Unknown';
|
|
371
|
+
this.getTableCounts(db, stats, resolve, reject);
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
// Derive version from path or set as unknown
|
|
375
|
+
const pathVersion = path.basename(dbPath, '.db').match(/\d+$/);
|
|
376
|
+
stats.version = pathVersion ? pathVersion[0] : 'Unknown';
|
|
377
|
+
this.getTableCounts(db, stats, resolve, reject);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getTableCounts(db, stats, resolve) {
|
|
384
|
+
const queries = [
|
|
385
|
+
{ name: 'rxnconsoCount', sql: 'SELECT COUNT(*) as count FROM RXNCONSO' },
|
|
386
|
+
{ name: 'rxnrelCount', sql: 'SELECT COUNT(*) as count FROM RXNREL' },
|
|
387
|
+
{ name: 'rxnstyCount', sql: 'SELECT COUNT(*) as count FROM RXNSTY' },
|
|
388
|
+
{ name: 'stemCount', sql: 'SELECT COUNT(*) as count FROM RXNSTEMS' }
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
let completed = 0;
|
|
392
|
+
|
|
393
|
+
queries.forEach(query => {
|
|
394
|
+
db.get(query.sql, (err, row) => {
|
|
395
|
+
if (err) {
|
|
396
|
+
stats[query.name] = 0;
|
|
397
|
+
} else {
|
|
398
|
+
stats[query.name] = row ? row.count : 0;
|
|
399
|
+
}
|
|
400
|
+
completed++;
|
|
401
|
+
|
|
402
|
+
if (completed === queries.length) {
|
|
403
|
+
const fileStat = fs.statSync(this.dbPath || './data/rxnorm.db');
|
|
404
|
+
stats.sizeGB = fileStat.size / (1024 * 1024 * 1024);
|
|
405
|
+
stats.lastModified = fileStat.mtime.toISOString();
|
|
406
|
+
|
|
407
|
+
db.close();
|
|
408
|
+
resolve(stats);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async createIndexes(dbPath) {
|
|
415
|
+
const db = new sqlite3.Database(dbPath);
|
|
416
|
+
|
|
417
|
+
const indexes = [
|
|
418
|
+
'CREATE INDEX IF NOT EXISTS X_RXNCONSO_1 ON RXNCONSO(RXCUI)',
|
|
419
|
+
'CREATE INDEX IF NOT EXISTS X_RXNCONSO_2 ON RXNCONSO(SAB, TTY)',
|
|
420
|
+
'CREATE INDEX IF NOT EXISTS X_RXNCONSO_3 ON RXNCONSO(CODE, SAB, TTY)',
|
|
421
|
+
'CREATE INDEX IF NOT EXISTS X_RXNCONSO_4 ON RXNCONSO(TTY, SAB)',
|
|
422
|
+
'CREATE INDEX IF NOT EXISTS X_RXNCONSO_6 ON RXNCONSO(RXAUI)',
|
|
423
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnconso_sab_tty_rxcui ON RXNCONSO(SAB, TTY, RXCUI)',
|
|
424
|
+
'CREATE INDEX IF NOT EXISTS X_RXNREL_2 ON RXNREL(REL, RXAUI1)',
|
|
425
|
+
'CREATE INDEX IF NOT EXISTS X_RXNREL_3 ON RXNREL(REL, RXCUI1)',
|
|
426
|
+
'CREATE INDEX IF NOT EXISTS X_RXNREL_4 ON RXNREL(RELA, RXAUI2)',
|
|
427
|
+
'CREATE INDEX IF NOT EXISTS X_RXNREL_5 ON RXNREL(RELA, RXCUI2)',
|
|
428
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rel ON RXNREL(REL)',
|
|
429
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela ON RXNREL(RELA)',
|
|
430
|
+
'CREATE INDEX IF NOT EXISTS X_RXNSTY_2 ON RXNSTY(TUI)',
|
|
431
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnstems_stem_cui ON RXNSTEMS(stem, CUI)'
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
db.serialize(() => {
|
|
436
|
+
indexes.forEach(sql => {
|
|
437
|
+
db.run(sql, (err) => {
|
|
438
|
+
if (err) console.warn(`Index creation warning: ${err.message}`);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
db.close((err) => {
|
|
444
|
+
if (err) reject(err);
|
|
445
|
+
else resolve();
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// RxNorm data importer
|
|
452
|
+
class RxNormImporter {
|
|
453
|
+
constructor(sourceDir, destFile, version, options = {}) {
|
|
454
|
+
this.sourceDir = sourceDir;
|
|
455
|
+
this.destFile = destFile;
|
|
456
|
+
this.version = version;
|
|
457
|
+
this.options = {
|
|
458
|
+
verbose: true,
|
|
459
|
+
createStems: true,
|
|
460
|
+
progressCallback: null,
|
|
461
|
+
...options
|
|
462
|
+
};
|
|
463
|
+
this.currentProgress = 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
updateProgress(amount = 1, operation = null) {
|
|
467
|
+
this.currentProgress += amount;
|
|
468
|
+
if (this.options.progressCallback) {
|
|
469
|
+
this.options.progressCallback(this.currentProgress, operation);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async import() {
|
|
474
|
+
if (this.options.verbose) console.log('Starting RxNorm import...');
|
|
475
|
+
|
|
476
|
+
// Ensure destination directory exists
|
|
477
|
+
const destDir = path.dirname(this.destFile);
|
|
478
|
+
if (!fs.existsSync(destDir)) {
|
|
479
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Remove existing database
|
|
483
|
+
if (fs.existsSync(this.destFile)) {
|
|
484
|
+
fs.unlinkSync(this.destFile);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const db = new sqlite3.Database(this.destFile);
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
await this.checkFiles();
|
|
491
|
+
await this.createTables(db);
|
|
492
|
+
await this.loadRXNSAB(db);
|
|
493
|
+
await this.loadRXNATOMARCHIVE(db);
|
|
494
|
+
await this.loadRXNCUI(db);
|
|
495
|
+
await this.loadRXNCONSO(db);
|
|
496
|
+
await this.loadRXNREL(db);
|
|
497
|
+
await this.loadRXNSTY(db);
|
|
498
|
+
|
|
499
|
+
if (this.options.createStems) {
|
|
500
|
+
await this.makeStems(db);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (this.options.verbose) console.log('RxNorm import completed successfully');
|
|
504
|
+
} finally {
|
|
505
|
+
await this.closeDatabase(db);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async checkFiles() {
|
|
510
|
+
this.updateProgress(0, 'Checking Files');
|
|
511
|
+
|
|
512
|
+
const requiredFiles = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF'];
|
|
513
|
+
|
|
514
|
+
for (const file of requiredFiles) {
|
|
515
|
+
const filePath = path.join(this.sourceDir, file);
|
|
516
|
+
if (!fs.existsSync(filePath)) {
|
|
517
|
+
throw new Error(`Required file not found: ${file}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async createTables(db) {
|
|
523
|
+
this.updateProgress(0, 'Creating Tables');
|
|
524
|
+
|
|
525
|
+
const tableSQL = [
|
|
526
|
+
// Drop existing tables
|
|
527
|
+
'DROP TABLE IF EXISTS RXNCONSO',
|
|
528
|
+
'DROP TABLE IF EXISTS RXNREL',
|
|
529
|
+
'DROP TABLE IF EXISTS RXNSTY',
|
|
530
|
+
'DROP TABLE IF EXISTS RXNSTEMS',
|
|
531
|
+
'DROP TABLE IF EXISTS RXNATOMARCHIVE',
|
|
532
|
+
'DROP TABLE IF EXISTS RXNCUI',
|
|
533
|
+
'DROP TABLE IF EXISTS RXNSAB',
|
|
534
|
+
|
|
535
|
+
// Create RXNCONSO table
|
|
536
|
+
`CREATE TABLE RXNCONSO (
|
|
537
|
+
RXCUI varchar(8) NOT NULL,
|
|
538
|
+
RXAUI varchar(8) NOT NULL,
|
|
539
|
+
SAB varchar(20) NOT NULL,
|
|
540
|
+
TTY varchar(20) NOT NULL,
|
|
541
|
+
CODE varchar(50) NOT NULL,
|
|
542
|
+
STR varchar(3000) NOT NULL,
|
|
543
|
+
SUPPRESS varchar(1)
|
|
544
|
+
)`,
|
|
545
|
+
|
|
546
|
+
// Create RXNREL table
|
|
547
|
+
`CREATE TABLE RXNREL (
|
|
548
|
+
RXCUI1 varchar(8),
|
|
549
|
+
RXAUI1 varchar(8),
|
|
550
|
+
REL varchar(4),
|
|
551
|
+
RXCUI2 varchar(8),
|
|
552
|
+
RXAUI2 varchar(8),
|
|
553
|
+
RELA varchar(100),
|
|
554
|
+
SAB varchar(20) NOT NULL
|
|
555
|
+
)`,
|
|
556
|
+
|
|
557
|
+
// Create RXNSTY table
|
|
558
|
+
`CREATE TABLE RXNSTY (
|
|
559
|
+
RXCUI varchar(8) NOT NULL,
|
|
560
|
+
TUI varchar(4)
|
|
561
|
+
)`,
|
|
562
|
+
|
|
563
|
+
// Create RXNATOMARCHIVE table
|
|
564
|
+
`CREATE TABLE RXNATOMARCHIVE (
|
|
565
|
+
RXAUI varchar(8) NOT NULL PRIMARY KEY,
|
|
566
|
+
AUI varchar(10),
|
|
567
|
+
STR varchar(4000) NOT NULL,
|
|
568
|
+
ARCHIVE_TIMESTAMP varchar(280) NOT NULL,
|
|
569
|
+
CREATED_TIMESTAMP varchar(280) NOT NULL,
|
|
570
|
+
UPDATED_TIMESTAMP varchar(280) NOT NULL,
|
|
571
|
+
CODE varchar(50),
|
|
572
|
+
IS_BRAND varchar(1),
|
|
573
|
+
LAT varchar(3),
|
|
574
|
+
LAST_RELEASED varchar(30),
|
|
575
|
+
SAUI varchar(50),
|
|
576
|
+
VSAB varchar(40),
|
|
577
|
+
RXCUI varchar(8),
|
|
578
|
+
SAB varchar(20),
|
|
579
|
+
TTY varchar(20),
|
|
580
|
+
MERGED_TO_RXCUI varchar(8)
|
|
581
|
+
)`,
|
|
582
|
+
|
|
583
|
+
// Create RXNSTEMS table
|
|
584
|
+
`CREATE TABLE RXNSTEMS (
|
|
585
|
+
stem CHAR(20) NOT NULL,
|
|
586
|
+
CUI VARCHAR(8) NOT NULL,
|
|
587
|
+
PRIMARY KEY (stem, CUI)
|
|
588
|
+
)`,
|
|
589
|
+
|
|
590
|
+
// Create RXNSAB table
|
|
591
|
+
`CREATE TABLE RXNSAB (
|
|
592
|
+
VCUI varchar(8),
|
|
593
|
+
RCUI varchar(8),
|
|
594
|
+
VSAB varchar(40),
|
|
595
|
+
RSAB varchar(20) NOT NULL,
|
|
596
|
+
SON varchar(3000),
|
|
597
|
+
SF varchar(20),
|
|
598
|
+
SVER varchar(20),
|
|
599
|
+
VSTART varchar(10),
|
|
600
|
+
VEND varchar(10),
|
|
601
|
+
IMETA varchar(10),
|
|
602
|
+
RMETA varchar(10),
|
|
603
|
+
SLC varchar(1000),
|
|
604
|
+
SCC varchar(1000),
|
|
605
|
+
SRL integer,
|
|
606
|
+
TFR integer,
|
|
607
|
+
CFR integer,
|
|
608
|
+
CXTY varchar(50),
|
|
609
|
+
TTYL varchar(300),
|
|
610
|
+
ATNL varchar(1000),
|
|
611
|
+
LAT varchar(3),
|
|
612
|
+
CENC varchar(20),
|
|
613
|
+
CURVER varchar(1),
|
|
614
|
+
SABIN varchar(1),
|
|
615
|
+
SSN varchar(3000),
|
|
616
|
+
SCIT varchar(4000),
|
|
617
|
+
PRIMARY KEY (VCUI)
|
|
618
|
+
)`,
|
|
619
|
+
|
|
620
|
+
// Create RXNCUI table
|
|
621
|
+
`CREATE TABLE RXNCUI (
|
|
622
|
+
cui1 VARCHAR(8),
|
|
623
|
+
ver_start VARCHAR(40),
|
|
624
|
+
ver_end VARCHAR(40),
|
|
625
|
+
cardinality VARCHAR(8),
|
|
626
|
+
cui2 VARCHAR(8),
|
|
627
|
+
PRIMARY KEY (cui1)
|
|
628
|
+
)`
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
return new Promise((resolve) => {
|
|
632
|
+
db.serialize(() => {
|
|
633
|
+
tableSQL.forEach(sql => {
|
|
634
|
+
db.run(sql, (err) => {
|
|
635
|
+
if (err && this.options.verbose) {
|
|
636
|
+
console.warn(`Table creation warning: ${err.message}`);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
if (this.options.verbose) console.log('Database tables created');
|
|
642
|
+
resolve();
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async loadRXNCONSO(db) {
|
|
648
|
+
await this.loadRRFFile(db, 'RXNCONSO.RRF',
|
|
649
|
+
'INSERT INTO RXNCONSO (RXCUI, RXAUI, SAB, TTY, CODE, STR, SUPPRESS) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
650
|
+
(items) => [items[0], items[7], items[11], items[12], items[13], items[14], items[16]]
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async loadRXNSAB(db) {
|
|
655
|
+
const filePath = path.join(this.sourceDir, 'RXNSAB.RRF');
|
|
656
|
+
if (!fs.existsSync(filePath)) return;
|
|
657
|
+
|
|
658
|
+
await this.loadRRFFile(db, 'RXNSAB.RRF',
|
|
659
|
+
`INSERT OR IGNORE INTO RXNSAB (VCUI, RCUI, VSAB, RSAB, SON, SF, SVER, VSTART, VEND, IMETA, RMETA, SLC, SCC, SRL, TFR, CFR, CXTY, TTYL, ATNL, LAT, CENC, CURVER, SABIN, SSN, SCIT)
|
|
660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
661
|
+
(items) => items.slice(0, 25)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async loadRXNCUI(db) {
|
|
666
|
+
const filePath = path.join(this.sourceDir, 'RXNCUI.RRF');
|
|
667
|
+
if (!fs.existsSync(filePath)) return;
|
|
668
|
+
|
|
669
|
+
await this.loadRRFFile(db, 'RXNCUI.RRF',
|
|
670
|
+
'INSERT OR IGNORE INTO RXNCUI (cui1, ver_start, ver_end, cardinality, cui2) VALUES (?, ?, ?, ?, ?)',
|
|
671
|
+
(items) => items.slice(0, 5)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async loadRXNATOMARCHIVE(db) {
|
|
676
|
+
const filePath = path.join(this.sourceDir, 'RXNATOMARCHIVE.RRF');
|
|
677
|
+
if (!fs.existsSync(filePath)) return;
|
|
678
|
+
|
|
679
|
+
await this.loadRRFFile(db, 'RXNATOMARCHIVE.RRF',
|
|
680
|
+
`INSERT OR IGNORE INTO RXNATOMARCHIVE (RXAUI, AUI, STR, ARCHIVE_TIMESTAMP, CREATED_TIMESTAMP, UPDATED_TIMESTAMP, CODE, IS_BRAND, LAT, LAST_RELEASED, SAUI, VSAB, RXCUI, SAB, TTY, MERGED_TO_RXCUI)
|
|
681
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
682
|
+
(items) => items.slice(0, 16)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async loadRXNREL(db) {
|
|
687
|
+
await this.loadRRFFile(db, 'RXNREL.RRF',
|
|
688
|
+
'INSERT INTO RXNREL (RXCUI1, RXAUI1, REL, RXCUI2, RXAUI2, RELA, SAB) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
689
|
+
(items) => [items[0], items[1], items[3], items[4], items[5], items[7], items[10]]
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async loadRXNSTY(db) {
|
|
694
|
+
await this.loadRRFFile(db, 'RXNSTY.RRF',
|
|
695
|
+
'INSERT INTO RXNSTY (RXCUI, TUI) VALUES (?, ?)',
|
|
696
|
+
(items) => [items[0], items[1]]
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async loadRRFFile(db, fileName, insertSQL, extractValues) {
|
|
701
|
+
const filePath = path.join(this.sourceDir, fileName);
|
|
702
|
+
if (!fs.existsSync(filePath)) {
|
|
703
|
+
if (this.options.verbose) console.warn(`File not found: ${fileName}`);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this.updateProgress(0, `Loading ${fileName}`);
|
|
708
|
+
|
|
709
|
+
const rl = readline.createInterface({
|
|
710
|
+
input: fs.createReadStream(filePath),
|
|
711
|
+
crlfDelay: Infinity
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
return new Promise((resolve, reject) => {
|
|
715
|
+
db.serialize(() => {
|
|
716
|
+
db.run('BEGIN TRANSACTION');
|
|
717
|
+
|
|
718
|
+
const stmt = db.prepare(insertSQL);
|
|
719
|
+
let lineCount = 0;
|
|
720
|
+
let processedCount = 0;
|
|
721
|
+
let errorCount = 0;
|
|
722
|
+
|
|
723
|
+
rl.on('line', (line) => {
|
|
724
|
+
lineCount++;
|
|
725
|
+
|
|
726
|
+
const items = line.split('|');
|
|
727
|
+
if (items.length === 0) return;
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const values = extractValues(items);
|
|
731
|
+
stmt.run(values, (err) => {
|
|
732
|
+
if (err && err.code !== 'SQLITE_CONSTRAINT') {
|
|
733
|
+
errorCount++;
|
|
734
|
+
if (this.options.verbose && errorCount <= 10) {
|
|
735
|
+
console.warn(`Error processing line ${lineCount} in ${fileName}: ${err.message}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
processedCount++;
|
|
740
|
+
|
|
741
|
+
if (processedCount % 1000 === 0) {
|
|
742
|
+
this.updateProgress(1000);
|
|
743
|
+
}
|
|
744
|
+
} catch (error) {
|
|
745
|
+
errorCount++;
|
|
746
|
+
if (this.options.verbose && errorCount <= 10) {
|
|
747
|
+
console.warn(`Error processing line ${lineCount} in ${fileName}: ${error.message}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
rl.on('close', () => {
|
|
753
|
+
stmt.finalize();
|
|
754
|
+
db.run('COMMIT', (err) => {
|
|
755
|
+
if (err) reject(err);
|
|
756
|
+
else {
|
|
757
|
+
// Update progress for remaining records
|
|
758
|
+
const remaining = processedCount % 1000;
|
|
759
|
+
if (remaining > 0) {
|
|
760
|
+
this.updateProgress(remaining);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (this.options.verbose) {
|
|
764
|
+
console.log(` Loaded ${processedCount} records from ${fileName}${errorCount > 0 ? ` (${errorCount} errors)` : ''}`);
|
|
765
|
+
}
|
|
766
|
+
resolve();
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
rl.on('error', reject);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async makeStems(db) {
|
|
777
|
+
this.updateProgress(0, 'Generating Stems');
|
|
778
|
+
|
|
779
|
+
if (this.options.verbose) console.log('Generating word stems...');
|
|
780
|
+
|
|
781
|
+
// Simple English stemmer implementation
|
|
782
|
+
const stemmer = new SimpleStemmer();
|
|
783
|
+
const stems = new Map();
|
|
784
|
+
|
|
785
|
+
// Get all RXNORM concepts
|
|
786
|
+
return new Promise((resolve, reject) => {
|
|
787
|
+
db.all("SELECT RXCUI, STR FROM RXNCONSO WHERE SAB = 'RXNORM'", (err, rows) => {
|
|
788
|
+
if (err) return reject(err);
|
|
789
|
+
|
|
790
|
+
// Process each concept and generate stems
|
|
791
|
+
rows.forEach(row => {
|
|
792
|
+
const words = this.extractWords(row.STR);
|
|
793
|
+
words.forEach(word => {
|
|
794
|
+
const stem = stemmer.stem(word.toLowerCase());
|
|
795
|
+
if (stem.length > 0 && stem.length <= 20) {
|
|
796
|
+
if (!stems.has(stem)) {
|
|
797
|
+
stems.set(stem, new Set());
|
|
798
|
+
}
|
|
799
|
+
stems.get(stem).add(row.RXCUI);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Insert stems into database
|
|
805
|
+
db.serialize(() => {
|
|
806
|
+
db.run('BEGIN TRANSACTION');
|
|
807
|
+
const stmt = db.prepare('INSERT OR IGNORE INTO RXNSTEMS (stem, CUI) VALUES (?, ?)');
|
|
808
|
+
|
|
809
|
+
let totalInserts = 0;
|
|
810
|
+
let processedStems = 0;
|
|
811
|
+
|
|
812
|
+
for (const [stem, cuis] of stems) {
|
|
813
|
+
for (const cui of cuis) {
|
|
814
|
+
stmt.run([stem, cui]);
|
|
815
|
+
totalInserts++;
|
|
816
|
+
|
|
817
|
+
if (totalInserts % 1000 === 0) {
|
|
818
|
+
this.updateProgress(1000);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
processedStems++;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
stmt.finalize();
|
|
825
|
+
db.run('COMMIT', (err) => {
|
|
826
|
+
if (err) reject(err);
|
|
827
|
+
else {
|
|
828
|
+
// Update progress for remaining inserts
|
|
829
|
+
const remaining = totalInserts % 1000;
|
|
830
|
+
if (remaining > 0) {
|
|
831
|
+
this.updateProgress(remaining);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (this.options.verbose) {
|
|
835
|
+
console.log(` Generated ${totalInserts} stem entries from ${processedStems} unique stems`);
|
|
836
|
+
}
|
|
837
|
+
resolve();
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
extractWords(text) {
|
|
846
|
+
// Extract words from text, removing punctuation and numbers
|
|
847
|
+
return text
|
|
848
|
+
.toLowerCase()
|
|
849
|
+
.replace(/[^\w\s]/g, ' ')
|
|
850
|
+
.split(/\s+/)
|
|
851
|
+
.filter(word => word.length > 2 && !/^\d+$/.test(word))
|
|
852
|
+
.filter(word => word.match(/^[a-z]/));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async closeDatabase(db) {
|
|
856
|
+
return new Promise((resolve) => {
|
|
857
|
+
db.close((err) => {
|
|
858
|
+
if (err && this.options.verbose) {
|
|
859
|
+
console.error('Error closing database:', err);
|
|
860
|
+
}
|
|
861
|
+
resolve();
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Simple English stemmer (basic implementation)
|
|
868
|
+
class SimpleStemmer {
|
|
869
|
+
constructor() {
|
|
870
|
+
// Common English suffixes to remove
|
|
871
|
+
this.suffixes = [
|
|
872
|
+
'ing', 'ly', 'ed', 'ies', 'ied', 'ies', 'ies', 'y', 's',
|
|
873
|
+
'tion', 'sion', 'ness', 'ment', 'able', 'ible', 'ant', 'ent'
|
|
874
|
+
].sort((a, b) => b.length - a.length); // Longest first
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
stem(word) {
|
|
878
|
+
if (word.length <= 3) return word;
|
|
879
|
+
|
|
880
|
+
// Try to remove suffixes
|
|
881
|
+
for (const suffix of this.suffixes) {
|
|
882
|
+
if (word.endsWith(suffix) && word.length > suffix.length + 2) {
|
|
883
|
+
const stem = word.substring(0, word.length - suffix.length);
|
|
884
|
+
// Basic validation - stem should still be reasonable length
|
|
885
|
+
if (stem.length >= 3) {
|
|
886
|
+
return stem;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return word;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
module.exports = {
|
|
896
|
+
RxNormModule,
|
|
897
|
+
RxNormImporter
|
|
898
|
+
};
|