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,1081 @@
|
|
|
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 LoincSubsetModule extends BaseTerminologyModule {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getName() {
|
|
13
|
+
return 'loinc-subset';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getDescription() {
|
|
17
|
+
return 'Create a subset of LOINC data for testing purposes';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getSupportedFormats() {
|
|
21
|
+
return ['directory', 'txt'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getDefaultConfig() {
|
|
25
|
+
return {
|
|
26
|
+
verbose: true,
|
|
27
|
+
overwrite: false,
|
|
28
|
+
dest: './loinc-subset',
|
|
29
|
+
expandPartLinks: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getEstimatedDuration() {
|
|
34
|
+
return '5-15 minutes (depending on subset size)';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
registerCommands(terminologyCommand, globalOptions) {
|
|
38
|
+
// Subset command
|
|
39
|
+
terminologyCommand
|
|
40
|
+
.command('subset')
|
|
41
|
+
.description('Create a LOINC subset from a list of codes')
|
|
42
|
+
.option('-s, --source <directory>', 'Source LOINC directory')
|
|
43
|
+
.option('-d, --dest <directory>', 'Destination directory for subset')
|
|
44
|
+
.option('-c, --codes <file>', 'Text file with LOINC codes (one per line)')
|
|
45
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
await this.handleSubsetCommand({...globalOptions, ...options});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Validate command
|
|
51
|
+
terminologyCommand
|
|
52
|
+
.command('validate')
|
|
53
|
+
.description('Validate subset inputs')
|
|
54
|
+
.option('-s, --source <directory>', 'Source LOINC directory to validate')
|
|
55
|
+
.option('-c, --codes <file>', 'Codes file to validate')
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
await this.handleValidateCommand({...globalOptions, ...options});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async handleSubsetCommand(options) {
|
|
62
|
+
try {
|
|
63
|
+
// Gather configuration
|
|
64
|
+
const config = await this.gatherSubsetConfig(options);
|
|
65
|
+
|
|
66
|
+
// Add estimated duration to config for confirmation display
|
|
67
|
+
config.estimatedDuration = this.getEstimatedDuration();
|
|
68
|
+
|
|
69
|
+
// Show confirmation unless --yes is specified
|
|
70
|
+
if (!options.yes) {
|
|
71
|
+
const confirmed = await this.confirmSubset(config);
|
|
72
|
+
if (!confirmed) {
|
|
73
|
+
this.logInfo('Subset operation cancelled');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Save configuration immediately after confirmation
|
|
79
|
+
this.rememberSuccessfulConfig(config);
|
|
80
|
+
|
|
81
|
+
// Run the subset operation
|
|
82
|
+
await this.runSubset(config);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logError(`Subset operation failed: ${error.message}`);
|
|
85
|
+
if (options.verbose) {
|
|
86
|
+
console.error(error.stack);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async gatherSubsetConfig(options) {
|
|
93
|
+
const terminology = this.getName();
|
|
94
|
+
|
|
95
|
+
// Get intelligent defaults based on previous usage
|
|
96
|
+
const smartDefaults = this.configManager.generateDefaults(terminology);
|
|
97
|
+
const recentSources = this.configManager.getRecentSources(terminology, 3);
|
|
98
|
+
|
|
99
|
+
const questions = [];
|
|
100
|
+
|
|
101
|
+
// Source directory
|
|
102
|
+
if (!options.source) {
|
|
103
|
+
const sourceQuestion = {
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'source',
|
|
106
|
+
message: 'Source LOINC directory:',
|
|
107
|
+
validate: (input) => {
|
|
108
|
+
if (!input) return 'Source directory is required';
|
|
109
|
+
if (!fs.existsSync(input)) return 'Source directory does not exist';
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
filter: (input) => path.resolve(input)
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Add default if we have a previous source
|
|
116
|
+
if (smartDefaults.source) {
|
|
117
|
+
sourceQuestion.default = smartDefaults.source;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If we have recent sources, offer them as choices
|
|
121
|
+
if (recentSources.length > 0) {
|
|
122
|
+
sourceQuestion.type = 'list';
|
|
123
|
+
sourceQuestion.choices = [
|
|
124
|
+
...recentSources.map(src => ({
|
|
125
|
+
name: `${src} ${src === smartDefaults.source ? '(last used)' : ''}`.trim(),
|
|
126
|
+
value: src
|
|
127
|
+
})),
|
|
128
|
+
{ name: 'Enter new path...', value: 'NEW_PATH' }
|
|
129
|
+
];
|
|
130
|
+
sourceQuestion.message = 'Select source LOINC directory:';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
questions.push(sourceQuestion);
|
|
134
|
+
|
|
135
|
+
// Follow up question for new path - only add if we're using the list approach
|
|
136
|
+
if (recentSources.length > 0) {
|
|
137
|
+
questions.push({
|
|
138
|
+
type: 'input',
|
|
139
|
+
name: 'source',
|
|
140
|
+
message: 'Enter new source path:',
|
|
141
|
+
when: (answers) => answers.source === 'NEW_PATH',
|
|
142
|
+
validate: (input) => {
|
|
143
|
+
if (!input) return 'Source directory is required';
|
|
144
|
+
if (!fs.existsSync(input)) return 'Source directory does not exist';
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
filter: (input) => path.resolve(input)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Destination directory
|
|
153
|
+
if (!options.dest) {
|
|
154
|
+
questions.push({
|
|
155
|
+
type: 'input',
|
|
156
|
+
name: 'dest',
|
|
157
|
+
message: 'Destination directory:',
|
|
158
|
+
default: smartDefaults.dest || './loinc-subset',
|
|
159
|
+
validate: (input) => {
|
|
160
|
+
if (!input) return 'Destination directory is required';
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
filter: (input) => path.resolve(input)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Codes file
|
|
168
|
+
if (!options.codes) {
|
|
169
|
+
questions.push({
|
|
170
|
+
type: 'input',
|
|
171
|
+
name: 'codes',
|
|
172
|
+
message: 'Codes file (one code per line):',
|
|
173
|
+
default: smartDefaults.codes,
|
|
174
|
+
validate: (input) => {
|
|
175
|
+
if (!input) return 'Codes file is required';
|
|
176
|
+
if (!fs.existsSync(input)) return 'Codes file does not exist';
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
filter: (input) => path.resolve(input)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Overwrite confirmation
|
|
184
|
+
questions.push({
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'overwrite',
|
|
187
|
+
message: 'Overwrite destination directory if it exists?',
|
|
188
|
+
default: smartDefaults.overwrite !== undefined ? smartDefaults.overwrite : false,
|
|
189
|
+
when: (answers) => {
|
|
190
|
+
const destPath = options.dest || answers.dest;
|
|
191
|
+
return fs.existsSync(destPath);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
questions.push({
|
|
196
|
+
type: 'confirm',
|
|
197
|
+
name: 'expandPartLinks',
|
|
198
|
+
message: 'Expand codes based on PartLink relationships?',
|
|
199
|
+
default: smartDefaults.expandPartLinks !== undefined ? smartDefaults.expandPartLinks : true
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
questions.push({
|
|
203
|
+
type: 'confirm',
|
|
204
|
+
name: 'verbose',
|
|
205
|
+
message: 'Show verbose output?',
|
|
206
|
+
default: smartDefaults.verbose !== undefined ? smartDefaults.verbose : true
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const answers = await require('inquirer').prompt(questions);
|
|
210
|
+
|
|
211
|
+
const finalConfig = {
|
|
212
|
+
...this.getDefaultConfig(),
|
|
213
|
+
...smartDefaults,
|
|
214
|
+
...options,
|
|
215
|
+
...answers
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return finalConfig;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async confirmSubset(config) {
|
|
222
|
+
console.log(chalk.cyan(`\n📋 LOINC Subset Configuration:`));
|
|
223
|
+
console.log(` Source: ${chalk.white(config.source)}`);
|
|
224
|
+
console.log(` Destination: ${chalk.white(config.dest)}`);
|
|
225
|
+
console.log(` Codes File: ${chalk.white(config.codes)}`);
|
|
226
|
+
console.log(` Expand PartLinks: ${chalk.white(config.expandPartLinks ? 'Yes' : 'No')}`);
|
|
227
|
+
console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
228
|
+
|
|
229
|
+
if (config.estimatedDuration) {
|
|
230
|
+
console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { confirmed } = await require('inquirer').prompt({
|
|
234
|
+
type: 'confirm',
|
|
235
|
+
name: 'confirmed',
|
|
236
|
+
message: 'Proceed with subset creation?',
|
|
237
|
+
default: true
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return confirmed;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async runSubset(config) {
|
|
244
|
+
try {
|
|
245
|
+
console.log(chalk.blue.bold(`🔬 Starting LOINC Subset Creation...\n`));
|
|
246
|
+
|
|
247
|
+
if (config.verbose) {
|
|
248
|
+
console.log('Debug - Final config values:');
|
|
249
|
+
console.log(` Source: ${config.source}`);
|
|
250
|
+
console.log(` Dest: ${config.dest}`);
|
|
251
|
+
console.log(` Codes: ${config.codes}`);
|
|
252
|
+
console.log('');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Pre-flight checks
|
|
256
|
+
this.logInfo('Running pre-flight checks...');
|
|
257
|
+
const prerequisitesPassed = await this.validateSubsetPrerequisites(config);
|
|
258
|
+
|
|
259
|
+
if (!prerequisitesPassed) {
|
|
260
|
+
throw new Error('Pre-flight checks failed');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Execute the subset creation
|
|
264
|
+
await this.executeSubset(config);
|
|
265
|
+
|
|
266
|
+
this.logSuccess('LOINC subset created successfully!');
|
|
267
|
+
|
|
268
|
+
} catch (error) {
|
|
269
|
+
this.stopProgress();
|
|
270
|
+
this.logError(`LOINC subset creation failed: ${error.message}`);
|
|
271
|
+
if (config.verbose) {
|
|
272
|
+
console.error(error.stack);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async handleValidateCommand(options) {
|
|
279
|
+
if (!options.source || !options.codes) {
|
|
280
|
+
const answers = await require('inquirer').prompt([
|
|
281
|
+
{
|
|
282
|
+
type: 'input',
|
|
283
|
+
name: 'source',
|
|
284
|
+
message: 'Source LOINC directory:',
|
|
285
|
+
when: !options.source,
|
|
286
|
+
validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'input',
|
|
290
|
+
name: 'codes',
|
|
291
|
+
message: 'Codes file:',
|
|
292
|
+
when: !options.codes,
|
|
293
|
+
validate: (input) => input && fs.existsSync(input) ? true : 'File does not exist'
|
|
294
|
+
}
|
|
295
|
+
]);
|
|
296
|
+
Object.assign(options, answers);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.logInfo('Validating subset inputs...');
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const stats = await this.validateSubsetInputs(options.source, options.codes);
|
|
303
|
+
|
|
304
|
+
this.logSuccess('Validation passed');
|
|
305
|
+
console.log(` Source files found: ${stats.filesFound.length}`);
|
|
306
|
+
console.log(` Codes in list: ${stats.codeCount.toLocaleString()}`);
|
|
307
|
+
console.log(` Unique codes: ${stats.uniqueCodes.toLocaleString()}`);
|
|
308
|
+
|
|
309
|
+
if (stats.warnings.length > 0) {
|
|
310
|
+
this.logWarning('Validation warnings:');
|
|
311
|
+
stats.warnings.forEach(warning => console.log(` ${warning}`));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
} catch (error) {
|
|
315
|
+
this.logError(`Validation failed: ${error.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async validateSubsetPrerequisites(config) {
|
|
320
|
+
const checks = [
|
|
321
|
+
{
|
|
322
|
+
name: 'Source directory exists',
|
|
323
|
+
check: () => {
|
|
324
|
+
const exists = fs.existsSync(config.source);
|
|
325
|
+
if (!exists && config.verbose) {
|
|
326
|
+
console.log(` Source path being checked: ${config.source}`);
|
|
327
|
+
}
|
|
328
|
+
return exists;
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'Codes file exists',
|
|
333
|
+
check: () => {
|
|
334
|
+
const exists = fs.existsSync(config.codes);
|
|
335
|
+
if (!exists && config.verbose) {
|
|
336
|
+
console.log(` Codes path being checked: ${config.codes}`);
|
|
337
|
+
}
|
|
338
|
+
return exists;
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'Source contains LOINC files',
|
|
343
|
+
check: async () => {
|
|
344
|
+
const requiredFiles = [
|
|
345
|
+
'LoincTable/Loinc.csv',
|
|
346
|
+
'AccessoryFiles/PartFile/Part.csv'
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
const results = requiredFiles.map(file => {
|
|
350
|
+
const fullPath = path.join(config.source, file);
|
|
351
|
+
const exists = fs.existsSync(fullPath);
|
|
352
|
+
if (!exists && config.verbose) {
|
|
353
|
+
console.log(` Missing required file: ${fullPath}`);
|
|
354
|
+
}
|
|
355
|
+
return exists;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return results.every(result => result);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
let allPassed = true;
|
|
364
|
+
|
|
365
|
+
for (const { name, check } of checks) {
|
|
366
|
+
try {
|
|
367
|
+
const passed = await check();
|
|
368
|
+
if (passed) {
|
|
369
|
+
this.logSuccess(name);
|
|
370
|
+
} else {
|
|
371
|
+
this.logError(name);
|
|
372
|
+
allPassed = false;
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
this.logError(`${name}: ${error.message}`);
|
|
376
|
+
allPassed = false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return allPassed;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async executeSubset(config) {
|
|
384
|
+
this.logInfo('Loading target codes...');
|
|
385
|
+
|
|
386
|
+
// Load the initial target codes
|
|
387
|
+
const initialTargetCodes = await this.loadTargetCodes(config.codes);
|
|
388
|
+
this.logInfo(`Loaded ${initialTargetCodes.size.toLocaleString()} initial target codes`);
|
|
389
|
+
|
|
390
|
+
if (config.verbose) {
|
|
391
|
+
const sampleCodes = Array.from(initialTargetCodes).slice(0, 20);
|
|
392
|
+
console.log(`First 20 codes from file: ${sampleCodes.join(', ')}`);
|
|
393
|
+
|
|
394
|
+
// Validate some codes exist in the main LOINC file
|
|
395
|
+
this.logInfo('Validating codes exist in LOINC...');
|
|
396
|
+
const validationResults = await this.validateCodesExist(config.source, initialTargetCodes);
|
|
397
|
+
console.log(`Found ${validationResults.found} of ${validationResults.checked} codes in LOINC main table`);
|
|
398
|
+
if (validationResults.notFound.length > 0) {
|
|
399
|
+
console.log(`Sample missing codes: ${validationResults.notFound.slice(0, 5).join(', ')}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let finalTargetCodes = initialTargetCodes;
|
|
404
|
+
|
|
405
|
+
// Expand target codes based on PartLink relationships if requested
|
|
406
|
+
if (config.expandPartLinks) {
|
|
407
|
+
this.logInfo('Expanding target codes based on PartLink relationships...');
|
|
408
|
+
finalTargetCodes = await this.expandCodesFromPartLinks(config.source, initialTargetCodes, config.verbose);
|
|
409
|
+
|
|
410
|
+
const addedCodes = finalTargetCodes.size - initialTargetCodes.size;
|
|
411
|
+
this.logInfo(`Added ${addedCodes.toLocaleString()} related codes from PartLink relationships`);
|
|
412
|
+
|
|
413
|
+
if (config.verbose && addedCodes > 0) {
|
|
414
|
+
const newCodes = Array.from(finalTargetCodes).filter(code => !initialTargetCodes.has(code));
|
|
415
|
+
const sampleNewCodes = newCodes.slice(0, 10);
|
|
416
|
+
console.log(`Sample newly added codes: ${sampleNewCodes.join(', ')}`);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
this.logInfo('Skipping PartLink expansion (disabled)');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.logInfo(`Final target codes: ${finalTargetCodes.size.toLocaleString()}`);
|
|
423
|
+
|
|
424
|
+
// Export final codes to file for inspection
|
|
425
|
+
if (config.verbose) {
|
|
426
|
+
const codesOutputPath = path.join(process.cwd(), 'final-target-codes.txt');
|
|
427
|
+
this.logInfo(`Exporting final code set to: ${codesOutputPath}`);
|
|
428
|
+
await this.exportCodesToFile(finalTargetCodes, codesOutputPath);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Create subset processor
|
|
432
|
+
const processor = new LoincSubsetProcessor(this, config.verbose);
|
|
433
|
+
|
|
434
|
+
await processor.createSubset(
|
|
435
|
+
config.source,
|
|
436
|
+
config.dest,
|
|
437
|
+
finalTargetCodes,
|
|
438
|
+
{
|
|
439
|
+
verbose: config.verbose,
|
|
440
|
+
overwrite: config.overwrite,
|
|
441
|
+
originalCodes: initialTargetCodes // Pass original codes for PartLink filtering
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async expandCodesFromPartLinks(sourceDir, initialCodes, verbose = false) {
|
|
447
|
+
const partLinkPath = path.join(sourceDir, 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv');
|
|
448
|
+
|
|
449
|
+
if (!fs.existsSync(partLinkPath)) {
|
|
450
|
+
if (verbose) {
|
|
451
|
+
console.log(` PartLink file not found: ${partLinkPath}`);
|
|
452
|
+
}
|
|
453
|
+
return new Set(initialCodes);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const expandedCodes = new Set(initialCodes);
|
|
457
|
+
|
|
458
|
+
if (verbose) {
|
|
459
|
+
console.log(` Processing PartLink relationships (single pass)...`);
|
|
460
|
+
console.log(` Original codes count: ${initialCodes.size}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const rl = readline.createInterface({
|
|
464
|
+
input: fs.createReadStream(partLinkPath),
|
|
465
|
+
crlfDelay: Infinity
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
let lineNum = 0;
|
|
469
|
+
let addedCodes = 0;
|
|
470
|
+
let matchedLines = 0;
|
|
471
|
+
|
|
472
|
+
for await (const line of rl) {
|
|
473
|
+
lineNum++;
|
|
474
|
+
|
|
475
|
+
// Skip header
|
|
476
|
+
if (lineNum === 1) continue;
|
|
477
|
+
|
|
478
|
+
const items = this.csvSplit(line, 7);
|
|
479
|
+
if (items.length < 3) continue;
|
|
480
|
+
|
|
481
|
+
const firstCode = this.removeQuotes(items[0]); // First cell
|
|
482
|
+
const thirdCode = this.removeQuotes(items[2]); // Third cell
|
|
483
|
+
|
|
484
|
+
// If either code is in our INITIAL target set, add both codes
|
|
485
|
+
const firstInOriginal = initialCodes.has(firstCode);
|
|
486
|
+
const thirdInOriginal = initialCodes.has(thirdCode);
|
|
487
|
+
|
|
488
|
+
if (firstInOriginal || thirdInOriginal) {
|
|
489
|
+
matchedLines++;
|
|
490
|
+
const sizeBefore = expandedCodes.size;
|
|
491
|
+
expandedCodes.add(firstCode);
|
|
492
|
+
expandedCodes.add(thirdCode);
|
|
493
|
+
const sizeAfter = expandedCodes.size;
|
|
494
|
+
addedCodes += (sizeAfter - sizeBefore);
|
|
495
|
+
|
|
496
|
+
if (verbose && matchedLines <= 10) {
|
|
497
|
+
console.log(` Match ${matchedLines}: "${firstCode}" (${firstInOriginal ? 'in original' : 'new'}) <-> "${thirdCode}" (${thirdInOriginal ? 'in original' : 'new'})`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (verbose) {
|
|
503
|
+
console.log(` Single pass completed: ${matchedLines} matching relationships found`);
|
|
504
|
+
console.log(` Added ${addedCodes} new codes`);
|
|
505
|
+
console.log(` Final expanded codes count: ${expandedCodes.size}`);
|
|
506
|
+
|
|
507
|
+
// Show what percentage this represents
|
|
508
|
+
const expansionRatio = expandedCodes.size / initialCodes.size;
|
|
509
|
+
console.log(` Expansion ratio: ${expansionRatio.toFixed(1)}x original size`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return expandedCodes;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Helper method for CSV parsing (moved from processor for reuse)
|
|
516
|
+
csvSplit(line, expectedCount) {
|
|
517
|
+
const result = new Array(expectedCount).fill('');
|
|
518
|
+
let inQuoted = false;
|
|
519
|
+
let currentField = 0;
|
|
520
|
+
let fieldStart = 0;
|
|
521
|
+
let i = 0;
|
|
522
|
+
|
|
523
|
+
while (i < line.length && currentField < expectedCount) {
|
|
524
|
+
const ch = line[i];
|
|
525
|
+
|
|
526
|
+
if (!inQuoted && ch === ',') {
|
|
527
|
+
if (currentField < expectedCount) {
|
|
528
|
+
result[currentField] = line.substring(fieldStart, i);
|
|
529
|
+
currentField++;
|
|
530
|
+
fieldStart = i + 1;
|
|
531
|
+
}
|
|
532
|
+
} else if (ch === '"') {
|
|
533
|
+
if (inQuoted && i + 1 < line.length && line[i + 1] === '"') {
|
|
534
|
+
i++; // Skip escaped quote
|
|
535
|
+
} else {
|
|
536
|
+
inQuoted = !inQuoted;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
i++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Handle last field
|
|
543
|
+
if (currentField < expectedCount) {
|
|
544
|
+
result[currentField] = line.substring(fieldStart);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Helper method for removing quotes (moved from processor for reuse)
|
|
551
|
+
removeQuotes(str) {
|
|
552
|
+
if (!str) return '';
|
|
553
|
+
return str.replace(/^"|"$/g, '');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async exportCodesToFile(codeSet, filePath) {
|
|
557
|
+
const sortedCodes = Array.from(codeSet).sort();
|
|
558
|
+
const content = sortedCodes.join('\n') + '\n';
|
|
559
|
+
|
|
560
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
561
|
+
this.logInfo(`Exported ${sortedCodes.length.toLocaleString()} codes to ${filePath}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async validateCodesExist(sourceDir, targetCodes) {
|
|
565
|
+
const loincMainPath = path.join(sourceDir, 'LoincTable/Loinc.csv');
|
|
566
|
+
|
|
567
|
+
if (!fs.existsSync(loincMainPath)) {
|
|
568
|
+
return { found: 0, checked: 0, notFound: [] };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const foundCodes = new Set();
|
|
572
|
+
|
|
573
|
+
const rl = readline.createInterface({
|
|
574
|
+
input: fs.createReadStream(loincMainPath),
|
|
575
|
+
crlfDelay: Infinity
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
let lineNum = 0;
|
|
579
|
+
for await (const line of rl) {
|
|
580
|
+
lineNum++;
|
|
581
|
+
|
|
582
|
+
// Skip header
|
|
583
|
+
if (lineNum === 1) continue;
|
|
584
|
+
|
|
585
|
+
const items = this.csvSplit(line, 39);
|
|
586
|
+
if (items.length < 1) continue;
|
|
587
|
+
|
|
588
|
+
const code = this.removeQuotes(items[0]);
|
|
589
|
+
if (targetCodes.has(code)) {
|
|
590
|
+
foundCodes.add(code);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const checked = Math.min(targetCodes.size, 100); // Only check first 100 for performance
|
|
595
|
+
const notFound = Array.from(targetCodes).slice(0, 100).filter(code => !foundCodes.has(code));
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
found: foundCodes.size,
|
|
599
|
+
checked: checked,
|
|
600
|
+
notFound: notFound
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async loadTargetCodes(codesFile) {
|
|
605
|
+
const codes = new Set();
|
|
606
|
+
|
|
607
|
+
const rl = readline.createInterface({
|
|
608
|
+
input: fs.createReadStream(codesFile),
|
|
609
|
+
crlfDelay: Infinity
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
for await (const line of rl) {
|
|
613
|
+
const code = line.trim();
|
|
614
|
+
if (code && !code.startsWith('#')) { // Allow comments with #
|
|
615
|
+
codes.add(code);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return codes;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async validateSubsetInputs(sourceDir, codesFile) {
|
|
623
|
+
const stats = {
|
|
624
|
+
filesFound: [],
|
|
625
|
+
codeCount: 0,
|
|
626
|
+
uniqueCodes: 0,
|
|
627
|
+
warnings: []
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// Check for LOINC files
|
|
631
|
+
const loincFiles = [
|
|
632
|
+
'LoincTable/Loinc.csv',
|
|
633
|
+
'AccessoryFiles/PartFile/Part.csv',
|
|
634
|
+
'AccessoryFiles/ConsumerName/ConsumerName.csv',
|
|
635
|
+
'AccessoryFiles/AnswerFile/AnswerList.csv',
|
|
636
|
+
'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
|
|
637
|
+
'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
|
|
638
|
+
'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
|
|
639
|
+
'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv'
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
for (const file of loincFiles) {
|
|
643
|
+
const filePath = path.join(sourceDir, file);
|
|
644
|
+
if (fs.existsSync(filePath)) {
|
|
645
|
+
stats.filesFound.push(file);
|
|
646
|
+
} else {
|
|
647
|
+
stats.warnings.push(`File not found: ${file}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Validate codes file
|
|
652
|
+
const codes = await this.loadTargetCodes(codesFile);
|
|
653
|
+
stats.codeCount = codes.size;
|
|
654
|
+
stats.uniqueCodes = codes.size;
|
|
655
|
+
|
|
656
|
+
return stats;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
class LoincSubsetProcessor {
|
|
661
|
+
constructor(moduleInstance, verbose = true) {
|
|
662
|
+
this.module = moduleInstance;
|
|
663
|
+
this.verbose = verbose;
|
|
664
|
+
this.targetCodes = null;
|
|
665
|
+
this.processedFiles = 0;
|
|
666
|
+
this.totalFiles = 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Add this method to LoincSubsetProcessor
|
|
670
|
+
cleanLine(line) {
|
|
671
|
+
// Remove trailing commas and whitespace
|
|
672
|
+
return line.replace(/,+\s*$/, '');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async createSubset(sourceDir, destDir, targetCodes, options) {
|
|
676
|
+
this.targetCodes = targetCodes;
|
|
677
|
+
this.originalCodes = options.originalCodes || targetCodes; // Fallback to targetCodes if not provided
|
|
678
|
+
|
|
679
|
+
// Create destination directory structure
|
|
680
|
+
await this.createDirectoryStructure(destDir, options.overwrite);
|
|
681
|
+
|
|
682
|
+
// Define files to process with their handlers
|
|
683
|
+
const filesToProcess = [
|
|
684
|
+
{
|
|
685
|
+
source: 'LoincTable/Loinc.csv',
|
|
686
|
+
dest: 'LoincTable/Loinc.csv',
|
|
687
|
+
handler: 'processMainCodes'
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
source: 'AccessoryFiles/PartFile/Part.csv',
|
|
691
|
+
dest: 'AccessoryFiles/PartFile/Part.csv',
|
|
692
|
+
handler: 'processParts'
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
source: 'AccessoryFiles/ConsumerName/ConsumerName.csv',
|
|
696
|
+
dest: 'AccessoryFiles/ConsumerName/ConsumerName.csv',
|
|
697
|
+
handler: 'processConsumerNames'
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
source: 'AccessoryFiles/AnswerFile/AnswerList.csv',
|
|
701
|
+
dest: 'AccessoryFiles/AnswerFile/AnswerList.csv',
|
|
702
|
+
handler: 'processAnswerLists'
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
source: 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
|
|
706
|
+
dest: 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
|
|
707
|
+
handler: 'processPartLinks'
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
source: 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
|
|
711
|
+
dest: 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
|
|
712
|
+
handler: 'processAnswerListLinks'
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
source: 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
|
|
716
|
+
dest: 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
|
|
717
|
+
handler: 'processHierarchy'
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
source: 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
|
|
721
|
+
dest: 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
|
|
722
|
+
handler: 'processLanguageVariants'
|
|
723
|
+
}
|
|
724
|
+
];
|
|
725
|
+
|
|
726
|
+
// Count existing files
|
|
727
|
+
this.totalFiles = filesToProcess.filter(file =>
|
|
728
|
+
fs.existsSync(path.join(sourceDir, file.source))
|
|
729
|
+
).length;
|
|
730
|
+
|
|
731
|
+
// Add language variant files
|
|
732
|
+
const languageVariantFiles = await this.findLanguageVariantFiles(sourceDir);
|
|
733
|
+
this.totalFiles += languageVariantFiles.length;
|
|
734
|
+
|
|
735
|
+
this.module.logInfo(`Processing ${this.totalFiles} files...`);
|
|
736
|
+
this.module.createProgressBar();
|
|
737
|
+
this.module.updateProgress(0, this.totalFiles);
|
|
738
|
+
|
|
739
|
+
// Process main files
|
|
740
|
+
for (const file of filesToProcess) {
|
|
741
|
+
const sourcePath = path.join(sourceDir, file.source);
|
|
742
|
+
const destPath = path.join(destDir, file.dest);
|
|
743
|
+
|
|
744
|
+
if (fs.existsSync(sourcePath)) {
|
|
745
|
+
if (this.verbose) {
|
|
746
|
+
this.module.logInfo(`Processing ${file.source}...`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
await this[file.handler](sourcePath, destPath);
|
|
750
|
+
this.processedFiles++;
|
|
751
|
+
this.module.updateProgress(this.processedFiles);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Process language variant files
|
|
756
|
+
for (const langFile of languageVariantFiles) {
|
|
757
|
+
const sourcePath = path.join(sourceDir, langFile);
|
|
758
|
+
const destPath = path.join(destDir, langFile);
|
|
759
|
+
|
|
760
|
+
if (this.verbose) {
|
|
761
|
+
this.module.logInfo(`Processing ${langFile}...`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
await this.processLanguageVariantFile(sourcePath, destPath);
|
|
765
|
+
this.processedFiles++;
|
|
766
|
+
this.module.updateProgress(this.processedFiles);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
this.module.stopProgress();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async createDirectoryStructure(destDir, overwrite) {
|
|
773
|
+
if (fs.existsSync(destDir)) {
|
|
774
|
+
if (overwrite) {
|
|
775
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
776
|
+
} else {
|
|
777
|
+
throw new Error(`Destination directory already exists: ${destDir}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Create directory structure
|
|
782
|
+
const dirs = [
|
|
783
|
+
destDir,
|
|
784
|
+
path.join(destDir, 'LoincTable'),
|
|
785
|
+
path.join(destDir, 'AccessoryFiles'),
|
|
786
|
+
path.join(destDir, 'AccessoryFiles/PartFile'),
|
|
787
|
+
path.join(destDir, 'AccessoryFiles/ConsumerName'),
|
|
788
|
+
path.join(destDir, 'AccessoryFiles/AnswerFile'),
|
|
789
|
+
path.join(destDir, 'AccessoryFiles/ComponentHierarchyBySystem'),
|
|
790
|
+
path.join(destDir, 'AccessoryFiles/LinguisticVariants')
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
for (const dir of dirs) {
|
|
794
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async findLanguageVariantFiles(sourceDir) {
|
|
799
|
+
const languageVariantFiles = [];
|
|
800
|
+
const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
|
|
801
|
+
|
|
802
|
+
if (fs.existsSync(linguisticVariantsDir)) {
|
|
803
|
+
const files = fs.readdirSync(linguisticVariantsDir);
|
|
804
|
+
for (const file of files) {
|
|
805
|
+
if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
|
|
806
|
+
languageVariantFiles.push(`AccessoryFiles/LinguisticVariants/${file}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return languageVariantFiles;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async processMainCodes(sourcePath, destPath) {
|
|
815
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
816
|
+
if (lineNum === 1) return true; // Keep header
|
|
817
|
+
|
|
818
|
+
const items = this.csvSplit(line, 39);
|
|
819
|
+
if (items.length < 1) return false;
|
|
820
|
+
|
|
821
|
+
const code = this.removeQuotes(items[0]);
|
|
822
|
+
return this.targetCodes.has(code);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async processParts(sourcePath, destPath) {
|
|
827
|
+
if (this.verbose) {
|
|
828
|
+
console.log(` Processing parts file: ${sourcePath}`);
|
|
829
|
+
console.log(` Target codes size: ${this.targetCodes.size}`);
|
|
830
|
+
|
|
831
|
+
// Show first few target codes for comparison
|
|
832
|
+
const sampleCodes = Array.from(this.targetCodes).slice(0, 5);
|
|
833
|
+
console.log(` Sample target codes: ${sampleCodes.join(', ')}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
837
|
+
if (lineNum === 1) return true; // Keep header
|
|
838
|
+
|
|
839
|
+
const items = this.csvSplit(line, 5);
|
|
840
|
+
if (items.length < 1) return false;
|
|
841
|
+
|
|
842
|
+
const code = this.removeQuotes(items[0]);
|
|
843
|
+
const hasCode = this.targetCodes.has(code);
|
|
844
|
+
|
|
845
|
+
// Debug first few lines
|
|
846
|
+
if (this.verbose && lineNum <= 5) {
|
|
847
|
+
console.log(` Line ${lineNum}: raw="${items[0]}", clean="${code}", match=${hasCode}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return hasCode;
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async processConsumerNames(sourcePath, destPath) {
|
|
855
|
+
if (this.verbose) {
|
|
856
|
+
console.log(` Processing consumer names file: ${sourcePath}`);
|
|
857
|
+
console.log(` Target codes size: ${this.targetCodes.size}`);
|
|
858
|
+
|
|
859
|
+
// Show first few target codes for comparison
|
|
860
|
+
const sampleCodes = Array.from(this.targetCodes).slice(0, 5);
|
|
861
|
+
console.log(` Sample target codes: ${sampleCodes.join(', ')}`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
865
|
+
if (lineNum === 1) return true; // Keep header
|
|
866
|
+
|
|
867
|
+
const items = this.csvSplit(line, 2);
|
|
868
|
+
if (items.length < 1) return false;
|
|
869
|
+
|
|
870
|
+
const code = this.removeQuotes(items[0]);
|
|
871
|
+
const hasCode = this.targetCodes.has(code);
|
|
872
|
+
|
|
873
|
+
// Debug first few lines
|
|
874
|
+
if (this.verbose && lineNum <= 5) {
|
|
875
|
+
console.log(` Line ${lineNum}: raw="${items[0]}", clean="${code}", match=${hasCode}`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return hasCode;
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async processAnswerLists(sourcePath, destPath) {
|
|
883
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
884
|
+
if (lineNum === 1) return true; // Keep header
|
|
885
|
+
|
|
886
|
+
const items = this.csvSplit(line, 11);
|
|
887
|
+
if (items.length < 7) return false;
|
|
888
|
+
|
|
889
|
+
const listCode = this.removeQuotes(items[0]);
|
|
890
|
+
const answerCode = this.removeQuotes(items[6]);
|
|
891
|
+
|
|
892
|
+
return this.targetCodes.has(listCode) || this.targetCodes.has(answerCode);
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async processPartLinks(sourcePath, destPath) {
|
|
897
|
+
if (this.verbose) {
|
|
898
|
+
console.log(` Processing part links file: ${sourcePath}`);
|
|
899
|
+
console.log(` Target codes size: ${this.targetCodes.size}`);
|
|
900
|
+
console.log(` Original codes size: ${this.originalCodes.size}`);
|
|
901
|
+
console.log(` Using ORIGINAL codes for PartLink filtering to prevent expansion explosion`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
let debugCount = 0;
|
|
905
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
906
|
+
if (lineNum === 1) return true; // Keep header
|
|
907
|
+
|
|
908
|
+
const items = this.csvSplit(line, 7);
|
|
909
|
+
if (items.length < 3) return false;
|
|
910
|
+
|
|
911
|
+
const sourceCode = this.removeQuotes(items[0]); // First cell
|
|
912
|
+
const targetCode = this.removeQuotes(items[2]); // Third cell
|
|
913
|
+
|
|
914
|
+
// Use ORIGINAL codes for filtering, not expanded codes
|
|
915
|
+
const hasSource = this.originalCodes.has(sourceCode);
|
|
916
|
+
const hasTarget = this.originalCodes.has(targetCode);
|
|
917
|
+
const shouldInclude = hasSource || hasTarget;
|
|
918
|
+
|
|
919
|
+
// Debug first few included lines
|
|
920
|
+
if (this.verbose && shouldInclude && debugCount < 10) {
|
|
921
|
+
debugCount++;
|
|
922
|
+
console.log(` Include ${debugCount}: "${sourceCode}" (${hasSource ? 'original' : 'no'}) <-> "${targetCode}" (${hasTarget ? 'original' : 'no'})`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return shouldInclude;
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async processAnswerListLinks(sourcePath, destPath) {
|
|
930
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
931
|
+
if (lineNum === 1) return true; // Keep header
|
|
932
|
+
|
|
933
|
+
const items = this.csvSplit(line, 7); // Increased expected count to handle 6th column
|
|
934
|
+
if (items.length < 6) return false;
|
|
935
|
+
|
|
936
|
+
const firstCode = this.removeQuotes(items[0]); // First cell
|
|
937
|
+
const thirdCode = this.removeQuotes(items[2]); // Third cell
|
|
938
|
+
const sixthCode = this.removeQuotes(items[5]); // Sixth cell (index 5)
|
|
939
|
+
|
|
940
|
+
const res = this.targetCodes.has(firstCode) && (
|
|
941
|
+
this.targetCodes.has(thirdCode) && (!sixthCode || this.targetCodes.has(sixthCode)));
|
|
942
|
+
return res;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async processHierarchy(sourcePath, destPath) {
|
|
947
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
948
|
+
if (lineNum === 1) return true; // Keep header
|
|
949
|
+
|
|
950
|
+
const items = this.csvSplit(line, 12);
|
|
951
|
+
if (items.length < 5) return false;
|
|
952
|
+
|
|
953
|
+
const childCode = this.removeQuotes(items[2]); // Third cell (column 3)
|
|
954
|
+
const relatedCode = this.removeQuotes(items[3]); // Fourth cell (column 4)
|
|
955
|
+
|
|
956
|
+
if (!childCode) {
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
// Check if this row should be included based on columns 3 and 4
|
|
960
|
+
if (this.targetCodes.has(childCode) && this.targetCodes.has(relatedCode)) {
|
|
961
|
+
// Modify the hierarchical path in column 1 (first cell)
|
|
962
|
+
const originalPath = this.removeQuotes(items[0]);
|
|
963
|
+
const pathCodes = originalPath.split('.');
|
|
964
|
+
const filteredCodes = pathCodes.filter(code => this.targetCodes.has(code));
|
|
965
|
+
const newPath = filteredCodes.join('.');
|
|
966
|
+
|
|
967
|
+
// Rebuild the line with modified path
|
|
968
|
+
const modifiedItems = [...items];
|
|
969
|
+
modifiedItems[0] = `"${newPath}"`;
|
|
970
|
+
return { include: true, modifiedLine: modifiedItems.join(',') };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return false;
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async processLanguageVariants(sourcePath, destPath) {
|
|
978
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
979
|
+
if (lineNum === 1) return true; // Keep header
|
|
980
|
+
|
|
981
|
+
// For the main LinguisticVariants.csv, include all language definitions
|
|
982
|
+
// since we can't know which languages we'll need until we process individual files
|
|
983
|
+
return true;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async processLanguageVariantFile(sourcePath, destPath) {
|
|
988
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
989
|
+
if (lineNum === 1) return true; // Keep header
|
|
990
|
+
|
|
991
|
+
const items = this.csvSplit(line, 12);
|
|
992
|
+
if (items.length < 1) return false;
|
|
993
|
+
|
|
994
|
+
// First column contains the LOINC code
|
|
995
|
+
const code = this.removeQuotes(items[0]);
|
|
996
|
+
return this.targetCodes.has(code);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async processFileWithFilter(sourcePath, destPath, filterFunction) {
|
|
1001
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const readStream = fs.createReadStream(sourcePath);
|
|
1006
|
+
const writeStream = fs.createWriteStream(destPath);
|
|
1007
|
+
|
|
1008
|
+
const rl = readline.createInterface({
|
|
1009
|
+
input: readStream,
|
|
1010
|
+
crlfDelay: Infinity
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
let lineNum = 0;
|
|
1014
|
+
let includedLines = 0;
|
|
1015
|
+
|
|
1016
|
+
for await (const line of rl) {
|
|
1017
|
+
lineNum++;
|
|
1018
|
+
|
|
1019
|
+
const result = filterFunction(line, lineNum);
|
|
1020
|
+
|
|
1021
|
+
if (result === true) {
|
|
1022
|
+
const cleanedLine = this.cleanLine(line);
|
|
1023
|
+
writeStream.write(cleanedLine + '\n');
|
|
1024
|
+
includedLines++;
|
|
1025
|
+
} else if (result && result.include) {
|
|
1026
|
+
const cleanedLine = this.cleanLine(result.modifiedLine);
|
|
1027
|
+
writeStream.write(cleanedLine + '\n');
|
|
1028
|
+
includedLines++;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
writeStream.end();
|
|
1033
|
+
|
|
1034
|
+
if (this.verbose && lineNum > 1) {
|
|
1035
|
+
console.log(` Included ${includedLines - 1} of ${lineNum - 1} data rows`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
csvSplit(line, expectedCount) {
|
|
1040
|
+
const result = new Array(expectedCount).fill('');
|
|
1041
|
+
let inQuoted = false;
|
|
1042
|
+
let currentField = 0;
|
|
1043
|
+
let fieldStart = 0;
|
|
1044
|
+
let i = 0;
|
|
1045
|
+
|
|
1046
|
+
while (i < line.length && currentField < expectedCount) {
|
|
1047
|
+
const ch = line[i];
|
|
1048
|
+
|
|
1049
|
+
if (!inQuoted && ch === ',') {
|
|
1050
|
+
if (currentField < expectedCount) {
|
|
1051
|
+
result[currentField] = line.substring(fieldStart, i);
|
|
1052
|
+
currentField++;
|
|
1053
|
+
fieldStart = i + 1;
|
|
1054
|
+
}
|
|
1055
|
+
} else if (ch === '"') {
|
|
1056
|
+
if (inQuoted && i + 1 < line.length && line[i + 1] === '"') {
|
|
1057
|
+
i++; // Skip escaped quote
|
|
1058
|
+
} else {
|
|
1059
|
+
inQuoted = !inQuoted;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
i++;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Handle last field
|
|
1066
|
+
if (currentField < expectedCount) {
|
|
1067
|
+
result[currentField] = line.substring(fieldStart);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
removeQuotes(str) {
|
|
1074
|
+
if (!str) return '';
|
|
1075
|
+
return str.replace(/^"|"$/g, '');
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
module.exports = {
|
|
1080
|
+
LoincSubsetModule
|
|
1081
|
+
};
|