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,938 @@
|
|
|
1
|
+
const { BaseTerminologyModule } = require('./tx-import-base');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
class RxNormSubsetModule extends BaseTerminologyModule {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getName() {
|
|
13
|
+
return 'rxnorm-subset';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getDescription() {
|
|
17
|
+
return 'Create a subset of RxNorm data for testing purposes';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getSupportedFormats() {
|
|
21
|
+
return ['directory', 'txt'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getDefaultConfig() {
|
|
25
|
+
return {
|
|
26
|
+
...super.getDefaultConfig(),
|
|
27
|
+
dest: './rxnorm-subset',
|
|
28
|
+
expandRelationships: true,
|
|
29
|
+
includeSynonyms: false,
|
|
30
|
+
includeArchived: false,
|
|
31
|
+
maxIterations: 5
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getEstimatedDuration() {
|
|
36
|
+
return '10-30 minutes (depending on relationship expansion)';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
registerCommands(terminologyCommand, globalOptions) {
|
|
40
|
+
// Subset command
|
|
41
|
+
terminologyCommand
|
|
42
|
+
.command('subset')
|
|
43
|
+
.description('Create an RxNorm subset from a list of codes')
|
|
44
|
+
.option('-s, --source <directory>', 'Source RxNorm directory (RRF files)')
|
|
45
|
+
.option('-d, --dest <directory>', 'Destination directory for subset')
|
|
46
|
+
.option('-c, --codes <file>', 'Text file with RxNorm codes (one per line)')
|
|
47
|
+
.option('--no-expand', 'Skip relationship expansion')
|
|
48
|
+
.option('--include-synonyms', 'Include synonym (SY) terms')
|
|
49
|
+
.option('--include-archived', 'Include archived concepts')
|
|
50
|
+
.option('--max-iterations <n>', 'Maximum relationship expansion iterations', '5')
|
|
51
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
await this.handleSubsetCommand({...globalOptions, ...options});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Validate command
|
|
57
|
+
terminologyCommand
|
|
58
|
+
.command('validate')
|
|
59
|
+
.description('Validate subset inputs')
|
|
60
|
+
.option('-s, --source <directory>', 'Source RxNorm directory to validate')
|
|
61
|
+
.option('-c, --codes <file>', 'Codes file to validate')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
await this.handleValidateCommand({...globalOptions, ...options});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async handleSubsetCommand(options) {
|
|
68
|
+
try {
|
|
69
|
+
// Gather configuration
|
|
70
|
+
const config = await this.gatherSubsetConfig(options);
|
|
71
|
+
|
|
72
|
+
// Show confirmation unless --yes is specified
|
|
73
|
+
if (!options.yes) {
|
|
74
|
+
const confirmed = await this.confirmSubset(config);
|
|
75
|
+
if (!confirmed) {
|
|
76
|
+
this.logInfo('Subset operation cancelled');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Save configuration
|
|
82
|
+
this.rememberSuccessfulConfig(config);
|
|
83
|
+
|
|
84
|
+
// Run the subset operation
|
|
85
|
+
await this.runSubset(config);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logError(`Subset operation failed: ${error.message}`);
|
|
88
|
+
if (options.verbose) {
|
|
89
|
+
console.error(error.stack);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async gatherSubsetConfig(options) {
|
|
96
|
+
const terminology = this.getName();
|
|
97
|
+
const smartDefaults = this.configManager.generateDefaults(terminology);
|
|
98
|
+
const recentSources = this.configManager.getRecentSources(terminology, 3);
|
|
99
|
+
|
|
100
|
+
const questions = [];
|
|
101
|
+
|
|
102
|
+
// Source directory
|
|
103
|
+
if (!options.source) {
|
|
104
|
+
const sourceQuestion = {
|
|
105
|
+
type: 'input',
|
|
106
|
+
name: 'source',
|
|
107
|
+
message: 'Source RxNorm directory (RRF files):',
|
|
108
|
+
validate: (input) => {
|
|
109
|
+
if (!input) return 'Source directory is required';
|
|
110
|
+
if (!fs.existsSync(input)) return 'Source directory does not exist';
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
filter: (input) => path.resolve(input)
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (smartDefaults.source) {
|
|
117
|
+
sourceQuestion.default = smartDefaults.source;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (recentSources.length > 0) {
|
|
121
|
+
sourceQuestion.type = 'list';
|
|
122
|
+
sourceQuestion.choices = [
|
|
123
|
+
...recentSources.map(src => ({
|
|
124
|
+
name: `${src} ${src === smartDefaults.source ? '(last used)' : ''}`.trim(),
|
|
125
|
+
value: src
|
|
126
|
+
})),
|
|
127
|
+
{ name: 'Enter new path...', value: 'NEW_PATH' }
|
|
128
|
+
];
|
|
129
|
+
sourceQuestion.message = 'Select source RxNorm directory:';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
questions.push(sourceQuestion);
|
|
133
|
+
|
|
134
|
+
if (recentSources.length > 0) {
|
|
135
|
+
questions.push({
|
|
136
|
+
type: 'input',
|
|
137
|
+
name: 'source',
|
|
138
|
+
message: 'Enter new source path:',
|
|
139
|
+
when: (answers) => answers.source === 'NEW_PATH',
|
|
140
|
+
validate: (input) => {
|
|
141
|
+
if (!input) return 'Source directory is required';
|
|
142
|
+
if (!fs.existsSync(input)) return 'Source directory does not exist';
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
filter: (input) => path.resolve(input)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Destination directory
|
|
151
|
+
if (!options.dest) {
|
|
152
|
+
questions.push({
|
|
153
|
+
type: 'input',
|
|
154
|
+
name: 'dest',
|
|
155
|
+
message: 'Destination directory for subset:',
|
|
156
|
+
default: smartDefaults.dest || './rxnorm-subset',
|
|
157
|
+
validate: (input) => {
|
|
158
|
+
if (!input) return 'Destination directory is required';
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
filter: (input) => path.resolve(input)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Codes file - default to /tx/data/rxnorm-subset if it exists
|
|
166
|
+
if (!options.codes) {
|
|
167
|
+
const defaultCodesFile = '/tx/data/rxnorm-subset';
|
|
168
|
+
questions.push({
|
|
169
|
+
type: 'input',
|
|
170
|
+
name: 'codes',
|
|
171
|
+
message: 'Codes file (one code per line):',
|
|
172
|
+
default: fs.existsSync(defaultCodesFile) ? defaultCodesFile : smartDefaults.codes,
|
|
173
|
+
validate: (input) => {
|
|
174
|
+
if (!input) return 'Codes file is required';
|
|
175
|
+
if (!fs.existsSync(input)) return 'Codes file does not exist';
|
|
176
|
+
return true;
|
|
177
|
+
},
|
|
178
|
+
filter: (input) => path.resolve(input)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Overwrite confirmation
|
|
183
|
+
questions.push({
|
|
184
|
+
type: 'confirm',
|
|
185
|
+
name: 'overwrite',
|
|
186
|
+
message: 'Overwrite destination directory if it exists?',
|
|
187
|
+
default: smartDefaults.overwrite !== undefined ? smartDefaults.overwrite : false,
|
|
188
|
+
when: (answers) => {
|
|
189
|
+
const destPath = options.dest || answers.dest;
|
|
190
|
+
return fs.existsSync(destPath);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Expansion options
|
|
195
|
+
questions.push({
|
|
196
|
+
type: 'confirm',
|
|
197
|
+
name: 'expandRelationships',
|
|
198
|
+
message: 'Expand codes based on relationships (ingredients, forms, etc.)?',
|
|
199
|
+
default: smartDefaults.expandRelationships !== undefined ? smartDefaults.expandRelationships : true
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
questions.push({
|
|
203
|
+
type: 'confirm',
|
|
204
|
+
name: 'includeSynonyms',
|
|
205
|
+
message: 'Include synonym (SY) terms?',
|
|
206
|
+
default: smartDefaults.includeSynonyms !== undefined ? smartDefaults.includeSynonyms : false
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
questions.push({
|
|
210
|
+
type: 'confirm',
|
|
211
|
+
name: 'includeArchived',
|
|
212
|
+
message: 'Include archived concepts?',
|
|
213
|
+
default: smartDefaults.includeArchived !== undefined ? smartDefaults.includeArchived : false
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
questions.push({
|
|
217
|
+
type: 'confirm',
|
|
218
|
+
name: 'verbose',
|
|
219
|
+
message: 'Show verbose output?',
|
|
220
|
+
default: smartDefaults.verbose !== undefined ? smartDefaults.verbose : true
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const answers = await require('inquirer').prompt(questions);
|
|
224
|
+
|
|
225
|
+
const finalConfig = {
|
|
226
|
+
...this.getDefaultConfig(),
|
|
227
|
+
...smartDefaults,
|
|
228
|
+
...options,
|
|
229
|
+
...answers,
|
|
230
|
+
expandRelationships: !options.noExpand && (answers.expandRelationships !== false),
|
|
231
|
+
includeSynonyms: options.includeSynonyms || answers.includeSynonyms,
|
|
232
|
+
includeArchived: options.includeArchived || answers.includeArchived,
|
|
233
|
+
maxIterations: parseInt(options.maxIterations) || 5
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return finalConfig;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async confirmSubset(config) {
|
|
240
|
+
console.log(chalk.cyan(`\n📋 RxNorm Subset Configuration:`));
|
|
241
|
+
console.log(` Source: ${chalk.white(config.source)}`);
|
|
242
|
+
console.log(` Destination: ${chalk.white(config.dest)}`);
|
|
243
|
+
console.log(` Codes File: ${chalk.white(config.codes)}`);
|
|
244
|
+
console.log(` Expand Relationships: ${chalk.white(config.expandRelationships ? 'Yes' : 'No')}`);
|
|
245
|
+
console.log(` Include Synonyms: ${chalk.white(config.includeSynonyms ? 'Yes' : 'No')}`);
|
|
246
|
+
console.log(` Include Archived: ${chalk.white(config.includeArchived ? 'Yes' : 'No')}`);
|
|
247
|
+
console.log(` Max Iterations: ${chalk.white(config.maxIterations)}`);
|
|
248
|
+
console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
249
|
+
|
|
250
|
+
if (config.estimatedDuration) {
|
|
251
|
+
console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { confirmed } = await require('inquirer').prompt({
|
|
255
|
+
type: 'confirm',
|
|
256
|
+
name: 'confirmed',
|
|
257
|
+
message: 'Proceed with subset creation?',
|
|
258
|
+
default: true
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return confirmed;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async runSubset(config) {
|
|
265
|
+
try {
|
|
266
|
+
console.log(chalk.blue.bold(`🧬 Starting RxNorm Subset Creation...\n`));
|
|
267
|
+
|
|
268
|
+
// Pre-flight checks
|
|
269
|
+
this.logInfo('Running pre-flight checks...');
|
|
270
|
+
const prerequisitesPassed = await this.validateSubsetPrerequisites(config);
|
|
271
|
+
|
|
272
|
+
if (!prerequisitesPassed) {
|
|
273
|
+
throw new Error('Pre-flight checks failed');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Execute the subset creation
|
|
277
|
+
await this.executeSubset(config);
|
|
278
|
+
|
|
279
|
+
this.logSuccess('RxNorm subset created successfully!');
|
|
280
|
+
|
|
281
|
+
} catch (error) {
|
|
282
|
+
this.stopProgress();
|
|
283
|
+
this.logError(`RxNorm subset creation failed: ${error.message}`);
|
|
284
|
+
if (config.verbose) {
|
|
285
|
+
console.error(error.stack);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async handleValidateCommand(options) {
|
|
292
|
+
if (!options.source || !options.codes) {
|
|
293
|
+
const answers = await require('inquirer').prompt([
|
|
294
|
+
{
|
|
295
|
+
type: 'input',
|
|
296
|
+
name: 'source',
|
|
297
|
+
message: 'Source RxNorm directory:',
|
|
298
|
+
when: !options.source,
|
|
299
|
+
validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
type: 'input',
|
|
303
|
+
name: 'codes',
|
|
304
|
+
message: 'Codes file:',
|
|
305
|
+
when: !options.codes,
|
|
306
|
+
validate: (input) => input && fs.existsSync(input) ? true : 'File does not exist'
|
|
307
|
+
}
|
|
308
|
+
]);
|
|
309
|
+
Object.assign(options, answers);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.logInfo('Validating subset inputs...');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const stats = await this.validateSubsetInputs(options.source, options.codes);
|
|
316
|
+
|
|
317
|
+
this.logSuccess('Validation passed');
|
|
318
|
+
console.log(` Required files found: ${stats.requiredFiles.length}/3`);
|
|
319
|
+
console.log(` Optional files found: ${stats.optionalFiles.length}/3`);
|
|
320
|
+
console.log(` Codes in list: ${stats.codeCount.toLocaleString()}`);
|
|
321
|
+
console.log(` Unique codes: ${stats.uniqueCodes.toLocaleString()}`);
|
|
322
|
+
|
|
323
|
+
if (stats.warnings.length > 0) {
|
|
324
|
+
this.logWarning('Validation warnings:');
|
|
325
|
+
stats.warnings.forEach(warning => console.log(` ${warning}`));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this.logError(`Validation failed: ${error.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async validateSubsetPrerequisites(config) {
|
|
334
|
+
const checks = [
|
|
335
|
+
{
|
|
336
|
+
name: 'Source directory exists',
|
|
337
|
+
check: () => fs.existsSync(config.source)
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'Codes file exists',
|
|
341
|
+
check: () => fs.existsSync(config.codes)
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'Source contains RxNorm RRF files',
|
|
345
|
+
check: () => {
|
|
346
|
+
const requiredFiles = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF'];
|
|
347
|
+
return requiredFiles.every(file =>
|
|
348
|
+
fs.existsSync(path.join(config.source, file))
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
let allPassed = true;
|
|
355
|
+
|
|
356
|
+
for (const { name, check } of checks) {
|
|
357
|
+
try {
|
|
358
|
+
const passed = await check();
|
|
359
|
+
if (passed) {
|
|
360
|
+
this.logSuccess(name);
|
|
361
|
+
} else {
|
|
362
|
+
this.logError(name);
|
|
363
|
+
allPassed = false;
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
this.logError(`${name}: ${error.message}`);
|
|
367
|
+
allPassed = false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return allPassed;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async executeSubset(config) {
|
|
375
|
+
this.logInfo('Loading target codes...');
|
|
376
|
+
|
|
377
|
+
// Load initial target codes
|
|
378
|
+
const initialTargetCodes = await this.loadTargetCodes(config.codes);
|
|
379
|
+
this.logInfo(`Loaded ${initialTargetCodes.size.toLocaleString()} initial target codes`);
|
|
380
|
+
|
|
381
|
+
if (config.verbose) {
|
|
382
|
+
const sampleCodes = Array.from(initialTargetCodes).slice(0, 10);
|
|
383
|
+
console.log(`Sample codes: ${sampleCodes.join(', ')}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let finalTargetCodes = initialTargetCodes;
|
|
387
|
+
|
|
388
|
+
// Expand target codes based on relationships if requested
|
|
389
|
+
if (config.expandRelationships) {
|
|
390
|
+
this.logInfo('Expanding target codes based on RxNorm relationships...');
|
|
391
|
+
|
|
392
|
+
const expander = new RxNormRelationshipExpander(
|
|
393
|
+
config.source,
|
|
394
|
+
config.verbose,
|
|
395
|
+
config.maxIterations
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
finalTargetCodes = await expander.expandCodes(initialTargetCodes);
|
|
399
|
+
|
|
400
|
+
const addedCodes = finalTargetCodes.size - initialTargetCodes.size;
|
|
401
|
+
this.logInfo(`Added ${addedCodes.toLocaleString()} related codes through relationship expansion`);
|
|
402
|
+
|
|
403
|
+
if (config.verbose && addedCodes > 0) {
|
|
404
|
+
const newCodes = Array.from(finalTargetCodes).filter(code => !initialTargetCodes.has(code));
|
|
405
|
+
const sampleNewCodes = newCodes.slice(0, 10);
|
|
406
|
+
console.log(`Sample newly added codes: ${sampleNewCodes.join(', ')}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.logInfo(`Final target codes: ${finalTargetCodes.size.toLocaleString()}`);
|
|
411
|
+
|
|
412
|
+
// Export final codes for inspection
|
|
413
|
+
if (config.verbose) {
|
|
414
|
+
const codesOutputPath = path.join(process.cwd(), 'rxnorm-final-target-codes.txt');
|
|
415
|
+
await this.exportCodesToFile(finalTargetCodes, codesOutputPath);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Create subset processor
|
|
419
|
+
const processor = new RxNormSubsetProcessor(this, config.verbose);
|
|
420
|
+
|
|
421
|
+
await processor.createSubset(
|
|
422
|
+
config.source,
|
|
423
|
+
config.dest,
|
|
424
|
+
finalTargetCodes,
|
|
425
|
+
{
|
|
426
|
+
verbose: config.verbose,
|
|
427
|
+
overwrite: config.overwrite,
|
|
428
|
+
includeSynonyms: config.includeSynonyms,
|
|
429
|
+
includeArchived: config.includeArchived
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async loadTargetCodes(codesFile) {
|
|
435
|
+
const codes = new Set();
|
|
436
|
+
|
|
437
|
+
const rl = readline.createInterface({
|
|
438
|
+
input: fs.createReadStream(codesFile),
|
|
439
|
+
crlfDelay: Infinity
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
for await (const line of rl) {
|
|
443
|
+
const trimmedLine = line.trim();
|
|
444
|
+
|
|
445
|
+
// Skip empty lines and comments
|
|
446
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Extract code (everything before # if there's an inline comment)
|
|
451
|
+
const code = trimmedLine.split('#')[0].trim();
|
|
452
|
+
if (code) {
|
|
453
|
+
codes.add(code);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return codes;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async exportCodesToFile(codeSet, filePath) {
|
|
461
|
+
const sortedCodes = Array.from(codeSet).sort();
|
|
462
|
+
const content = sortedCodes.join('\n') + '\n';
|
|
463
|
+
|
|
464
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
465
|
+
this.logInfo(`Exported ${sortedCodes.length.toLocaleString()} codes to ${filePath}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async validateSubsetInputs(sourceDir, codesFile) {
|
|
469
|
+
const stats = {
|
|
470
|
+
requiredFiles: [],
|
|
471
|
+
optionalFiles: [],
|
|
472
|
+
codeCount: 0,
|
|
473
|
+
uniqueCodes: 0,
|
|
474
|
+
warnings: []
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// Check for RxNorm RRF files
|
|
478
|
+
const requiredFiles = ['RXNCONSO.RRF', 'RXNREL.RRF', 'RXNSTY.RRF'];
|
|
479
|
+
const optionalFiles = ['RXNSAB.RRF', 'RXNATOMARCHIVE.RRF', 'RXNCUI.RRF'];
|
|
480
|
+
|
|
481
|
+
for (const file of requiredFiles) {
|
|
482
|
+
const filePath = path.join(sourceDir, file);
|
|
483
|
+
if (fs.existsSync(filePath)) {
|
|
484
|
+
stats.requiredFiles.push(file);
|
|
485
|
+
} else {
|
|
486
|
+
stats.warnings.push(`Required file not found: ${file}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const file of optionalFiles) {
|
|
491
|
+
const filePath = path.join(sourceDir, file);
|
|
492
|
+
if (fs.existsSync(filePath)) {
|
|
493
|
+
stats.optionalFiles.push(file);
|
|
494
|
+
} else {
|
|
495
|
+
stats.warnings.push(`Optional file not found: ${file}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Validate codes file
|
|
500
|
+
const codes = await this.loadTargetCodes(codesFile);
|
|
501
|
+
stats.codeCount = codes.size;
|
|
502
|
+
stats.uniqueCodes = codes.size;
|
|
503
|
+
|
|
504
|
+
return stats;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// RxNorm relationship expander
|
|
509
|
+
class RxNormRelationshipExpander {
|
|
510
|
+
constructor(sourceDir, verbose = false, maxIterations = 5) {
|
|
511
|
+
this.sourceDir = sourceDir;
|
|
512
|
+
this.verbose = verbose;
|
|
513
|
+
this.maxIterations = maxIterations;
|
|
514
|
+
|
|
515
|
+
// Relationships that define components/ingredients of target codes (inward expansion)
|
|
516
|
+
this.inwardRelationships = new Set([
|
|
517
|
+
'has_ingredient',
|
|
518
|
+
'has_form',
|
|
519
|
+
'has_dose_form',
|
|
520
|
+
'form_of',
|
|
521
|
+
'ingredient_of',
|
|
522
|
+
'consists_of',
|
|
523
|
+
'contains'
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
// REL codes for inward relationships
|
|
527
|
+
this.inwardRels = new Set(['RN', 'IN']); // Ingredient relationships
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async expandCodes(initialCodes) {
|
|
531
|
+
if (this.verbose) {
|
|
532
|
+
console.log(` Starting relationship expansion with ${initialCodes.size} codes`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let currentCodes = new Set(initialCodes);
|
|
536
|
+
let iteration = 0;
|
|
537
|
+
|
|
538
|
+
while (iteration < this.maxIterations) {
|
|
539
|
+
iteration++;
|
|
540
|
+
const sizeBefore = currentCodes.size;
|
|
541
|
+
|
|
542
|
+
if (this.verbose) {
|
|
543
|
+
console.log(` Iteration ${iteration}: Starting with ${sizeBefore} codes`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const newCodes = await this.findRelatedCodes(currentCodes);
|
|
547
|
+
|
|
548
|
+
// Add new codes to current set
|
|
549
|
+
for (const code of newCodes) {
|
|
550
|
+
currentCodes.add(code);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const sizeAfter = currentCodes.size;
|
|
554
|
+
const added = sizeAfter - sizeBefore;
|
|
555
|
+
|
|
556
|
+
if (this.verbose) {
|
|
557
|
+
console.log(` Iteration ${iteration}: Added ${added} codes (total: ${sizeAfter})`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Stop if no new codes were found
|
|
561
|
+
if (added === 0) {
|
|
562
|
+
if (this.verbose) {
|
|
563
|
+
console.log(` Expansion converged after ${iteration} iterations`);
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return currentCodes;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async findRelatedCodes(targetCodes) {
|
|
573
|
+
const relatedCodes = new Set();
|
|
574
|
+
const rxnrelPath = path.join(this.sourceDir, 'RXNREL.RRF');
|
|
575
|
+
|
|
576
|
+
if (!fs.existsSync(rxnrelPath)) {
|
|
577
|
+
if (this.verbose) {
|
|
578
|
+
console.log(` RXNREL file not found: ${rxnrelPath}`);
|
|
579
|
+
}
|
|
580
|
+
return relatedCodes;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const rl = readline.createInterface({
|
|
584
|
+
input: fs.createReadStream(rxnrelPath),
|
|
585
|
+
crlfDelay: Infinity
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
let processedLines = 0;
|
|
589
|
+
let matchedRelationships = 0;
|
|
590
|
+
|
|
591
|
+
for await (const line of rl) {
|
|
592
|
+
processedLines++;
|
|
593
|
+
|
|
594
|
+
const items = line.split('|');
|
|
595
|
+
if (items.length < 11) continue;
|
|
596
|
+
|
|
597
|
+
const rxcui1 = items[0]; // Source concept
|
|
598
|
+
const rel = items[3]; // Relationship type
|
|
599
|
+
const rxcui2 = items[4]; // Target concept
|
|
600
|
+
const rela = items[7]; // Specific relationship
|
|
601
|
+
const sab = items[10]; // Source
|
|
602
|
+
|
|
603
|
+
// Focus on RXNORM relationships
|
|
604
|
+
if (sab !== 'RXNORM') continue;
|
|
605
|
+
|
|
606
|
+
// Find inward relationships - where our target codes are the "complex" concept
|
|
607
|
+
// and we want to include their "simple" components
|
|
608
|
+
if (targetCodes.has(rxcui1)) {
|
|
609
|
+
// Target code is source - include target (RXCUI2) for inward relationships
|
|
610
|
+
if (this.isInwardRelationship(rel, rela)) {
|
|
611
|
+
relatedCodes.add(rxcui2);
|
|
612
|
+
matchedRelationships++;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Also check reverse relationships - if our target is a component,
|
|
617
|
+
// include the complex concept that contains it
|
|
618
|
+
if (targetCodes.has(rxcui2)) {
|
|
619
|
+
// Target code is target - include source (RXCUI1) for outward relationships
|
|
620
|
+
if (this.isReverseInwardRelationship(rel, rela)) {
|
|
621
|
+
relatedCodes.add(rxcui1);
|
|
622
|
+
matchedRelationships++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (processedLines % 100000 === 0 && this.verbose) {
|
|
627
|
+
console.log(` Processed ${processedLines} relationships, found ${matchedRelationships} matches`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (this.verbose) {
|
|
632
|
+
console.log(` Found ${relatedCodes.size} related codes from ${matchedRelationships} relationships`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return relatedCodes;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
isInwardRelationship(rel, rela) {
|
|
639
|
+
// REL-based relationships
|
|
640
|
+
if (this.inwardRels.has(rel)) {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// RELA-based relationships (more specific)
|
|
645
|
+
if (rela && this.inwardRelationships.has(rela)) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Other common inward relationship patterns
|
|
650
|
+
const inwardPatterns = [
|
|
651
|
+
'has_ingredient',
|
|
652
|
+
'has_active_ingredient',
|
|
653
|
+
'has_precise_ingredient',
|
|
654
|
+
'has_form',
|
|
655
|
+
'has_dose_form',
|
|
656
|
+
'contains',
|
|
657
|
+
'consists_of'
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
return rela && inwardPatterns.some(pattern => rela.includes(pattern));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
isReverseInwardRelationship(rel, rela) {
|
|
664
|
+
// These are relationships where if our target is RXCUI2,
|
|
665
|
+
// we want to include RXCUI1 as it helps define our target
|
|
666
|
+
const reversePatterns = [
|
|
667
|
+
'ingredient_of',
|
|
668
|
+
'form_of',
|
|
669
|
+
'active_ingredient_of',
|
|
670
|
+
'precise_ingredient_of',
|
|
671
|
+
'contained_in',
|
|
672
|
+
'part_of'
|
|
673
|
+
];
|
|
674
|
+
|
|
675
|
+
return rela && reversePatterns.some(pattern => rela.includes(pattern));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// RxNorm subset processor
|
|
680
|
+
class RxNormSubsetProcessor {
|
|
681
|
+
constructor(moduleInstance, verbose = true) {
|
|
682
|
+
this.module = moduleInstance;
|
|
683
|
+
this.verbose = verbose;
|
|
684
|
+
this.targetCodes = null;
|
|
685
|
+
this.processedFiles = 0;
|
|
686
|
+
this.totalFiles = 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async createSubset(sourceDir, destDir, targetCodes, options) {
|
|
690
|
+
this.targetCodes = targetCodes;
|
|
691
|
+
|
|
692
|
+
// Create destination directory structure
|
|
693
|
+
await this.createDirectoryStructure(destDir, options.overwrite);
|
|
694
|
+
|
|
695
|
+
// Define RRF files to process
|
|
696
|
+
const filesToProcess = [
|
|
697
|
+
{
|
|
698
|
+
source: 'RXNCONSO.RRF',
|
|
699
|
+
dest: 'RXNCONSO.RRF',
|
|
700
|
+
handler: 'processRXNCONSO'
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
source: 'RXNREL.RRF',
|
|
704
|
+
dest: 'RXNREL.RRF',
|
|
705
|
+
handler: 'processRXNREL'
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
source: 'RXNSTY.RRF',
|
|
709
|
+
dest: 'RXNSTY.RRF',
|
|
710
|
+
handler: 'processRXNSTY'
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
source: 'RXNSAB.RRF',
|
|
714
|
+
dest: 'RXNSAB.RRF',
|
|
715
|
+
handler: 'processRXNSAB'
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
source: 'RXNCUI.RRF',
|
|
719
|
+
dest: 'RXNCUI.RRF',
|
|
720
|
+
handler: 'processRXNCUI'
|
|
721
|
+
}
|
|
722
|
+
];
|
|
723
|
+
|
|
724
|
+
// Conditionally add archived file
|
|
725
|
+
if (options.includeArchived) {
|
|
726
|
+
filesToProcess.push({
|
|
727
|
+
source: 'RXNATOMARCHIVE.RRF',
|
|
728
|
+
dest: 'RXNATOMARCHIVE.RRF',
|
|
729
|
+
handler: 'processRXNATOMARCHIVE'
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Count existing files
|
|
734
|
+
this.totalFiles = filesToProcess.filter(file =>
|
|
735
|
+
fs.existsSync(path.join(sourceDir, file.source))
|
|
736
|
+
).length;
|
|
737
|
+
|
|
738
|
+
this.module.logInfo(`Processing ${this.totalFiles} RRF files...`);
|
|
739
|
+
this.module.createProgressBar();
|
|
740
|
+
this.module.updateProgress(0, this.totalFiles);
|
|
741
|
+
|
|
742
|
+
// Process each file
|
|
743
|
+
for (const file of filesToProcess) {
|
|
744
|
+
const sourcePath = path.join(sourceDir, file.source);
|
|
745
|
+
const destPath = path.join(destDir, file.dest);
|
|
746
|
+
|
|
747
|
+
if (fs.existsSync(sourcePath)) {
|
|
748
|
+
if (this.verbose) {
|
|
749
|
+
this.module.logInfo(`Processing ${file.source}...`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
await this[file.handler](sourcePath, destPath, options);
|
|
753
|
+
this.processedFiles++;
|
|
754
|
+
this.module.updateProgress(this.processedFiles);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this.module.stopProgress();
|
|
759
|
+
|
|
760
|
+
// Generate subset statistics
|
|
761
|
+
await this.generateSubsetStats(destDir, targetCodes);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async createDirectoryStructure(destDir, overwrite) {
|
|
765
|
+
if (fs.existsSync(destDir)) {
|
|
766
|
+
if (overwrite) {
|
|
767
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
768
|
+
} else {
|
|
769
|
+
throw new Error(`Destination directory already exists: ${destDir}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async processRXNCONSO(sourcePath, destPath, options) {
|
|
777
|
+
await this.processRRFFile(sourcePath, destPath, (items) => {
|
|
778
|
+
const rxcui = items[0];
|
|
779
|
+
const tty = items[12];
|
|
780
|
+
|
|
781
|
+
// Include if RXCUI is in target set
|
|
782
|
+
if (this.targetCodes.has(rxcui)) {
|
|
783
|
+
// Optionally filter out synonyms
|
|
784
|
+
if (!options.includeSynonyms && tty === 'SY') {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return false;
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async processRXNREL(sourcePath, destPath) {
|
|
795
|
+
await this.processRRFFile(sourcePath, destPath, (items) => {
|
|
796
|
+
const rxcui1 = items[0];
|
|
797
|
+
const rxcui2 = items[4];
|
|
798
|
+
|
|
799
|
+
// Include if either RXCUI is in target set
|
|
800
|
+
return this.targetCodes.has(rxcui1) || this.targetCodes.has(rxcui2);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async processRXNSTY(sourcePath, destPath) {
|
|
805
|
+
await this.processRRFFile(sourcePath, destPath, (items) => {
|
|
806
|
+
const rxcui = items[0];
|
|
807
|
+
return this.targetCodes.has(rxcui);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async processRXNSAB(sourcePath, destPath) {
|
|
812
|
+
// For RXNSAB, we need to find which sources are referenced
|
|
813
|
+
// First pass: collect all SABs referenced in target concepts
|
|
814
|
+
const referencedSabs = await this.findReferencedSabs(sourcePath.replace('RXNSAB.RRF', 'RXNCONSO.RRF'));
|
|
815
|
+
|
|
816
|
+
await this.processRRFFile(sourcePath, destPath, (items) => {
|
|
817
|
+
const rsab = items[3]; // RSAB field
|
|
818
|
+
return referencedSabs.has(rsab);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async processRXNCUI(sourcePath, destPath) {
|
|
823
|
+
await this.processRRFFile(sourcePath, destPath, (items) => {
|
|
824
|
+
const cui1 = items[0];
|
|
825
|
+
const cui2 = items[4];
|
|
826
|
+
|
|
827
|
+
// Include if either CUI is in target set
|
|
828
|
+
return this.targetCodes.has(cui1) || (cui2 && this.targetCodes.has(cui2));
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async processRXNATOMARCHIVE(sourcePath, destPath) {
|
|
833
|
+
await this.processRRFFile(sourcePath, destPath, (items) => {
|
|
834
|
+
const rxcui = items[12]; // RXCUI field in archive
|
|
835
|
+
return rxcui && this.targetCodes.has(rxcui);
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async findReferencedSabs(rxnconsoPath) {
|
|
840
|
+
const referencedSabs = new Set();
|
|
841
|
+
|
|
842
|
+
if (!fs.existsSync(rxnconsoPath)) {
|
|
843
|
+
return referencedSabs;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const rl = readline.createInterface({
|
|
847
|
+
input: fs.createReadStream(rxnconsoPath),
|
|
848
|
+
crlfDelay: Infinity
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
for await (const line of rl) {
|
|
852
|
+
const items = line.split('|');
|
|
853
|
+
if (items.length < 12) continue;
|
|
854
|
+
|
|
855
|
+
const rxcui = items[0];
|
|
856
|
+
const sab = items[11];
|
|
857
|
+
|
|
858
|
+
if (this.targetCodes.has(rxcui)) {
|
|
859
|
+
referencedSabs.add(sab);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return referencedSabs;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async processRRFFile(sourcePath, destPath, filterFunction) {
|
|
867
|
+
const readStream = fs.createReadStream(sourcePath);
|
|
868
|
+
const writeStream = fs.createWriteStream(destPath);
|
|
869
|
+
|
|
870
|
+
const rl = readline.createInterface({
|
|
871
|
+
input: readStream,
|
|
872
|
+
crlfDelay: Infinity
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
let lineNum = 0;
|
|
876
|
+
let includedLines = 0;
|
|
877
|
+
|
|
878
|
+
for await (const line of rl) {
|
|
879
|
+
lineNum++;
|
|
880
|
+
|
|
881
|
+
const items = line.split('|');
|
|
882
|
+
|
|
883
|
+
if (filterFunction(items)) {
|
|
884
|
+
writeStream.write(line + '\n');
|
|
885
|
+
includedLines++;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
writeStream.end();
|
|
890
|
+
|
|
891
|
+
if (this.verbose && lineNum > 0) {
|
|
892
|
+
const filename = path.basename(sourcePath);
|
|
893
|
+
console.log(` Included ${includedLines.toLocaleString()} of ${lineNum.toLocaleString()} lines in ${filename}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async generateSubsetStats(destDir, targetCodes) {
|
|
898
|
+
const stats = {
|
|
899
|
+
originalTargetCodes: targetCodes.size,
|
|
900
|
+
timestamp: new Date().toISOString(),
|
|
901
|
+
files: {}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// Count lines in each output file
|
|
905
|
+
const files = fs.readdirSync(destDir);
|
|
906
|
+
for (const file of files) {
|
|
907
|
+
if (file.endsWith('.RRF')) {
|
|
908
|
+
const filePath = path.join(destDir, file);
|
|
909
|
+
const lineCount = await this.countLines(filePath);
|
|
910
|
+
stats.files[file] = lineCount;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Write stats file
|
|
915
|
+
const statsPath = path.join(destDir, 'subset-stats.json');
|
|
916
|
+
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2));
|
|
917
|
+
|
|
918
|
+
this.module.logInfo(`Subset statistics written to ${statsPath}`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async countLines(filePath) {
|
|
922
|
+
return new Promise((resolve, reject) => {
|
|
923
|
+
let lineCount = 0;
|
|
924
|
+
const rl = readline.createInterface({
|
|
925
|
+
input: fs.createReadStream(filePath),
|
|
926
|
+
crlfDelay: Infinity
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
rl.on('line', () => lineCount++);
|
|
930
|
+
rl.on('close', () => resolve(lineCount));
|
|
931
|
+
rl.on('error', reject);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
module.exports = {
|
|
937
|
+
RxNormSubsetModule
|
|
938
|
+
};
|