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,1536 @@
|
|
|
1
|
+
const { BaseTerminologyModule } = require('./tx-import-base');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
class LoincModule extends BaseTerminologyModule {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getName() {
|
|
14
|
+
return 'loinc';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getDescription() {
|
|
18
|
+
return 'Logical Observation Identifiers Names and Codes (LOINC) from Regenstrief Institute';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getSupportedFormats() {
|
|
22
|
+
return ['csv', 'directory'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getDefaultConfig() {
|
|
26
|
+
return {
|
|
27
|
+
verbose: true,
|
|
28
|
+
overwrite: false,
|
|
29
|
+
createIndexes: true,
|
|
30
|
+
mainOnly: false,
|
|
31
|
+
dest: './data/loinc.db'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getEstimatedDuration() {
|
|
36
|
+
return '45-120 minutes (depending on language variants)';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
registerCommands(terminologyCommand, globalOptions) {
|
|
40
|
+
// Import command
|
|
41
|
+
terminologyCommand
|
|
42
|
+
.command('import')
|
|
43
|
+
.description('Import LOINC data from source directory')
|
|
44
|
+
.option('-s, --source <directory>', 'Source directory containing LOINC files')
|
|
45
|
+
.option('-d, --dest <file>', 'Destination SQLite database')
|
|
46
|
+
.option('-v, --version <version>', 'LOINC version identifier')
|
|
47
|
+
.option('-y, --yes', 'Skip confirmations')
|
|
48
|
+
.option('--no-indexes', 'Skip index creation for faster import')
|
|
49
|
+
.option('--main-only', 'Import only main codes (skip language variants)')
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
await this.handleImportCommand({...globalOptions, ...options});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Validate command
|
|
55
|
+
terminologyCommand
|
|
56
|
+
.command('validate')
|
|
57
|
+
.description('Validate LOINC source directory structure')
|
|
58
|
+
.option('-s, --source <directory>', 'Source directory to validate')
|
|
59
|
+
.action(async (options) => {
|
|
60
|
+
await this.handleValidateCommand({...globalOptions, ...options});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Status command
|
|
64
|
+
terminologyCommand
|
|
65
|
+
.command('status')
|
|
66
|
+
.description('Show status of LOINC database')
|
|
67
|
+
.option('-d, --dest <file>', 'Database file to check')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
await this.handleStatusCommand({...globalOptions, ...options});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async handleImportCommand(options) {
|
|
74
|
+
try {
|
|
75
|
+
// Gather configuration with remembered values
|
|
76
|
+
const config = await this.gatherCommonConfig(options);
|
|
77
|
+
|
|
78
|
+
// LOINC-specific configuration
|
|
79
|
+
config.createIndexes = !options.noIndexes;
|
|
80
|
+
config.mainOnly = options.mainOnly || false;
|
|
81
|
+
config.estimatedDuration = this.getEstimatedDuration();
|
|
82
|
+
|
|
83
|
+
if (!options.version) {
|
|
84
|
+
const inquirer = require('inquirer');
|
|
85
|
+
const { version } = await inquirer.prompt({
|
|
86
|
+
type: 'input',
|
|
87
|
+
name: 'version',
|
|
88
|
+
message: 'LOINC version identifier:',
|
|
89
|
+
default: config.version
|
|
90
|
+
});
|
|
91
|
+
config.version = version;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Show confirmation unless --yes is specified
|
|
95
|
+
if (!options.yes) {
|
|
96
|
+
const confirmed = await this.confirmImport(config);
|
|
97
|
+
if (!confirmed) {
|
|
98
|
+
this.logInfo('Import cancelled');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Save configuration immediately after confirmation
|
|
104
|
+
this.rememberSuccessfulConfig(config);
|
|
105
|
+
|
|
106
|
+
// Run the import
|
|
107
|
+
await this.runImportWithoutConfigSaving(config);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.logError(`Import command failed: ${error.message}`);
|
|
110
|
+
if (options.verbose) {
|
|
111
|
+
console.error(error.stack);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async confirmImport(config) {
|
|
118
|
+
const inquirer = require('inquirer');
|
|
119
|
+
const chalk = require('chalk');
|
|
120
|
+
|
|
121
|
+
console.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
|
|
122
|
+
console.log(` Source: ${chalk.white(config.source)}`);
|
|
123
|
+
console.log(` Destination: ${chalk.white(config.dest)}`);
|
|
124
|
+
console.log(` Version: ${chalk.white(config.version)}`);
|
|
125
|
+
console.log(` Main Only: ${chalk.white(config.mainOnly ? 'Yes' : 'No')}`);
|
|
126
|
+
console.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
|
|
127
|
+
console.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
128
|
+
console.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
|
|
129
|
+
|
|
130
|
+
if (config.estimatedDuration) {
|
|
131
|
+
console.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { confirmed } = await inquirer.prompt({
|
|
135
|
+
type: 'confirm',
|
|
136
|
+
name: 'confirmed',
|
|
137
|
+
message: 'Proceed with import?',
|
|
138
|
+
default: true
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return confirmed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async runImportWithoutConfigSaving(config) {
|
|
145
|
+
try {
|
|
146
|
+
console.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
|
|
147
|
+
|
|
148
|
+
// Pre-flight checks
|
|
149
|
+
this.logInfo('Running pre-flight checks...');
|
|
150
|
+
const prerequisitesPassed = await this.validatePrerequisites(config);
|
|
151
|
+
|
|
152
|
+
if (!prerequisitesPassed) {
|
|
153
|
+
throw new Error('Pre-flight checks failed');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Execute the import
|
|
157
|
+
await this.executeImport(config);
|
|
158
|
+
|
|
159
|
+
this.logSuccess(`${this.getName()} import completed successfully!`);
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.stopProgress();
|
|
163
|
+
this.logError(`${this.getName()} import failed: ${error.message}`);
|
|
164
|
+
if (config.verbose) {
|
|
165
|
+
console.error(error.stack);
|
|
166
|
+
}
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async handleValidateCommand(options) {
|
|
172
|
+
if (!options.source) {
|
|
173
|
+
const answers = await require('inquirer').prompt({
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'source',
|
|
176
|
+
message: 'Source directory to validate:',
|
|
177
|
+
validate: (input) => input && fs.existsSync(input) ? true : 'Directory does not exist'
|
|
178
|
+
});
|
|
179
|
+
options.source = answers.source;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.logInfo(`Validating LOINC directory: ${options.source}`);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const stats = await this.validateLoincDirectory(options.source);
|
|
186
|
+
|
|
187
|
+
this.logSuccess('Directory validation passed');
|
|
188
|
+
console.log(` Main codes file: ${stats.mainCodesFound ? 'Found' : 'Missing'}`);
|
|
189
|
+
console.log(` Parts file: ${stats.partsFound ? 'Found' : 'Missing'}`);
|
|
190
|
+
console.log(` Language variants: ${stats.languageVariants.length} found`);
|
|
191
|
+
console.log(` Estimated main codes: ${stats.estimatedCodes.toLocaleString()}`);
|
|
192
|
+
|
|
193
|
+
if (stats.warnings.length > 0) {
|
|
194
|
+
this.logWarning('Validation warnings:');
|
|
195
|
+
stats.warnings.forEach(warning => console.log(` ${warning}`));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
} catch (error) {
|
|
199
|
+
this.logError(`Validation failed: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async handleStatusCommand(options) {
|
|
204
|
+
const dbPath = options.dest || './data/loinc.db';
|
|
205
|
+
|
|
206
|
+
if (!fs.existsSync(dbPath)) {
|
|
207
|
+
this.logError(`Database not found: ${dbPath}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.logInfo(`Checking LOINC database: ${dbPath}`);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const stats = await this.getDatabaseStats(dbPath);
|
|
215
|
+
|
|
216
|
+
this.logSuccess('Database status:');
|
|
217
|
+
console.log(` Version: ${stats.version}`);
|
|
218
|
+
console.log(` Total Codes: ${stats.codeCount.toLocaleString()}`);
|
|
219
|
+
console.log(` Main LOINC Codes: ${stats.mainCodeCount.toLocaleString()}`);
|
|
220
|
+
console.log(` Parts: ${stats.partCount.toLocaleString()}`);
|
|
221
|
+
console.log(` Answer Lists: ${stats.answerListCount.toLocaleString()}`);
|
|
222
|
+
console.log(` Languages: ${stats.languageCount.toLocaleString()}`);
|
|
223
|
+
console.log(` Relationships: ${stats.relationshipCount.toLocaleString()}`);
|
|
224
|
+
console.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
|
|
225
|
+
console.log(` Last Modified: ${stats.lastModified}`);
|
|
226
|
+
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.logError(`Status check failed: ${error.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async validatePrerequisites(config) {
|
|
233
|
+
const baseValid = await super.validatePrerequisites(config);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
this.logInfo('Validating LOINC directory structure...');
|
|
237
|
+
await this.validateLoincDirectory(config.source);
|
|
238
|
+
this.logSuccess('LOINC directory structure valid');
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.logError(`LOINC directory validation failed: ${error.message}`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return baseValid;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async executeImport(config) {
|
|
248
|
+
this.logInfo('Starting LOINC data migration...');
|
|
249
|
+
|
|
250
|
+
const enhancedMigrator = new LoincDataMigratorWithProgress(
|
|
251
|
+
this,
|
|
252
|
+
config.verbose
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await enhancedMigrator.migrate(
|
|
256
|
+
config.source,
|
|
257
|
+
config.dest,
|
|
258
|
+
config.version,
|
|
259
|
+
{
|
|
260
|
+
verbose: config.verbose,
|
|
261
|
+
mainOnly: config.mainOnly
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (config.createIndexes) {
|
|
266
|
+
this.logInfo('Creating database indexes...');
|
|
267
|
+
await this.createIndexes(config.dest);
|
|
268
|
+
this.logSuccess('Indexes created');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async validateLoincDirectory(sourceDir) {
|
|
273
|
+
if (!fs.existsSync(sourceDir)) {
|
|
274
|
+
throw new Error(`Source directory not found: ${sourceDir}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const requiredFiles = [
|
|
278
|
+
'LoincTable/Loinc.csv',
|
|
279
|
+
'AccessoryFiles/PartFile/Part.csv'
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const optionalFiles = [
|
|
283
|
+
'AccessoryFiles/ConsumerName/ConsumerName.csv',
|
|
284
|
+
'AccessoryFiles/AnswerFile/AnswerList.csv',
|
|
285
|
+
'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
|
|
286
|
+
'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
|
|
287
|
+
'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
|
|
288
|
+
'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv'
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const warnings = [];
|
|
292
|
+
let mainCodesFound = false;
|
|
293
|
+
let partsFound = false;
|
|
294
|
+
let estimatedCodes = 0;
|
|
295
|
+
|
|
296
|
+
// Check required files
|
|
297
|
+
for (const file of requiredFiles) {
|
|
298
|
+
const filePath = path.join(sourceDir, file);
|
|
299
|
+
if (!fs.existsSync(filePath)) {
|
|
300
|
+
throw new Error(`Required file missing: ${file}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (file.includes('Loinc.csv')) {
|
|
304
|
+
mainCodesFound = true;
|
|
305
|
+
estimatedCodes = await this.countLines(filePath) - 1;
|
|
306
|
+
} else if (file.includes('Part.csv')) {
|
|
307
|
+
partsFound = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check optional files
|
|
312
|
+
for (const file of optionalFiles) {
|
|
313
|
+
const filePath = path.join(sourceDir, file);
|
|
314
|
+
if (!fs.existsSync(filePath)) {
|
|
315
|
+
warnings.push(`Optional file missing: ${file}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check for language variants
|
|
320
|
+
const languageVariants = [];
|
|
321
|
+
const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
|
|
322
|
+
if (fs.existsSync(linguisticVariantsDir)) {
|
|
323
|
+
const files = fs.readdirSync(linguisticVariantsDir);
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
|
|
326
|
+
languageVariants.push(file);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
mainCodesFound,
|
|
333
|
+
partsFound,
|
|
334
|
+
estimatedCodes,
|
|
335
|
+
languageVariants,
|
|
336
|
+
warnings
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async countLines(filePath) {
|
|
341
|
+
return new Promise((resolve, reject) => {
|
|
342
|
+
let lineCount = 0;
|
|
343
|
+
const rl = readline.createInterface({
|
|
344
|
+
input: fs.createReadStream(filePath),
|
|
345
|
+
crlfDelay: Infinity
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
rl.on('line', () => lineCount++);
|
|
349
|
+
rl.on('close', () => resolve(lineCount));
|
|
350
|
+
rl.on('error', reject);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async getDatabaseStats(dbPath) {
|
|
355
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
356
|
+
const db = new sqlite3.Database(dbPath);
|
|
357
|
+
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
const stats = {};
|
|
360
|
+
|
|
361
|
+
db.get('SELECT Value FROM Config WHERE ConfigKey = 2', (err, row) => {
|
|
362
|
+
if (err) return reject(err);
|
|
363
|
+
stats.version = row ? row.Value : 'Unknown';
|
|
364
|
+
|
|
365
|
+
const queries = [
|
|
366
|
+
{ name: 'codeCount', sql: 'SELECT COUNT(*) as count FROM Codes' },
|
|
367
|
+
{ name: 'mainCodeCount', sql: 'SELECT COUNT(*) as count FROM Codes WHERE Type = 1' },
|
|
368
|
+
{ name: 'partCount', sql: 'SELECT COUNT(*) as count FROM Codes WHERE Type = 2' },
|
|
369
|
+
{ name: 'answerListCount', sql: 'SELECT COUNT(*) as count FROM Codes WHERE Type = 3' },
|
|
370
|
+
{ name: 'languageCount', sql: 'SELECT COUNT(*) as count FROM Languages' },
|
|
371
|
+
{ name: 'relationshipCount', sql: 'SELECT COUNT(*) as count FROM Relationships' }
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
let completed = 0;
|
|
375
|
+
|
|
376
|
+
queries.forEach(query => {
|
|
377
|
+
db.get(query.sql, (err, row) => {
|
|
378
|
+
if (err) return reject(err);
|
|
379
|
+
stats[query.name] = row.count;
|
|
380
|
+
completed++;
|
|
381
|
+
|
|
382
|
+
if (completed === queries.length) {
|
|
383
|
+
const fileStat = fs.statSync(dbPath);
|
|
384
|
+
stats.sizeGB = fileStat.size / (1024 * 1024 * 1024);
|
|
385
|
+
stats.lastModified = fileStat.mtime.toISOString();
|
|
386
|
+
|
|
387
|
+
db.close();
|
|
388
|
+
resolve(stats);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async createIndexes(dbPath) {
|
|
397
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
398
|
+
const db = new sqlite3.Database(dbPath);
|
|
399
|
+
|
|
400
|
+
const indexes = [
|
|
401
|
+
// Codes table indexes
|
|
402
|
+
'CREATE INDEX IF NOT EXISTS idx_codes_code ON Codes(Code)',
|
|
403
|
+
'CREATE INDEX IF NOT EXISTS idx_codes_type ON Codes(Type)',
|
|
404
|
+
'CREATE INDEX IF NOT EXISTS idx_codes_status ON Codes(StatusKey)',
|
|
405
|
+
'CREATE INDEX IF NOT EXISTS idx_codes_type_status ON Codes(Type, StatusKey)',
|
|
406
|
+
|
|
407
|
+
// Relationships table indexes
|
|
408
|
+
'CREATE INDEX IF NOT EXISTS idx_relationships_source ON Relationships(RelationshipTypeKey, SourceKey)',
|
|
409
|
+
'CREATE INDEX IF NOT EXISTS idx_relationships_target ON Relationships(RelationshipTypeKey, TargetKey)',
|
|
410
|
+
'CREATE INDEX IF NOT EXISTS idx_relationships_source_type ON Relationships(SourceKey, RelationshipTypeKey)',
|
|
411
|
+
|
|
412
|
+
// Properties table indexes
|
|
413
|
+
'CREATE INDEX IF NOT EXISTS idx_properties_code ON Properties(PropertyTypeKey, CodeKey)',
|
|
414
|
+
'CREATE INDEX IF NOT EXISTS idx_properties_code2 ON Properties(CodeKey, PropertyTypeKey)',
|
|
415
|
+
'CREATE INDEX IF NOT EXISTS idx_properties_valuekey ON Properties(PropertyValueKey)',
|
|
416
|
+
|
|
417
|
+
// PropertyValues table indexes - for value lookups in filters
|
|
418
|
+
'CREATE INDEX IF NOT EXISTS idx_propertyvalues_value ON PropertyValues(Value COLLATE NOCASE)',
|
|
419
|
+
|
|
420
|
+
// Descriptions table indexes
|
|
421
|
+
'CREATE INDEX IF NOT EXISTS idx_descriptions_code ON Descriptions(CodeKey, LanguageKey)',
|
|
422
|
+
'CREATE INDEX IF NOT EXISTS idx_descriptions_type ON Descriptions(DescriptionTypeKey)',
|
|
423
|
+
'CREATE INDEX IF NOT EXISTS idx_descriptions_code_type ON Descriptions(CodeKey, DescriptionTypeKey)',
|
|
424
|
+
|
|
425
|
+
// Closure table indexes
|
|
426
|
+
'CREATE INDEX IF NOT EXISTS idx_closure_ancestor ON Closure(AncestorKey)',
|
|
427
|
+
'CREATE INDEX IF NOT EXISTS idx_closure_descendent ON Closure(DescendentKey)'
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
db.serialize(() => {
|
|
432
|
+
indexes.forEach(sql => {
|
|
433
|
+
db.run(sql, (err) => {
|
|
434
|
+
if (err) console.warn(`Index creation warning: ${err.message}`);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
db.close((err) => {
|
|
440
|
+
if (err) reject(err);
|
|
441
|
+
else resolve();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Enhanced migrator with progress reporting
|
|
448
|
+
class LoincDataMigratorWithProgress {
|
|
449
|
+
constructor(moduleInstance, verbose = true) {
|
|
450
|
+
this.module = moduleInstance;
|
|
451
|
+
this.verbose = verbose;
|
|
452
|
+
this.totalProgress = 0;
|
|
453
|
+
this.currentOperation = 'Starting';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async migrate(sourceDir, destFile, version, options) {
|
|
457
|
+
// Estimate total work
|
|
458
|
+
this.totalProgress = await this.estimateWorkload(sourceDir, options);
|
|
459
|
+
|
|
460
|
+
this.module.logInfo(`Processing LOINC data (${this.totalProgress.toLocaleString()} estimated records)...`);
|
|
461
|
+
|
|
462
|
+
// Create progress bar with operation display
|
|
463
|
+
const progressFormat = '{operation} |{bar}| {percentage}% | {value}/{total} | ETA: {eta}s';
|
|
464
|
+
this.module.createProgressBar(progressFormat);
|
|
465
|
+
|
|
466
|
+
// Start the progress bar
|
|
467
|
+
this.module.progressBar.start(this.totalProgress, 0, {
|
|
468
|
+
operation: chalk.cyan('Starting'.padEnd(20).substring(0, 20))
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Create migrator with progress callback that properly updates both progress and operation
|
|
472
|
+
const migratorWithProgress = new LoincDataMigrator((currentProgress, operation) => {
|
|
473
|
+
if (operation) {
|
|
474
|
+
this.currentOperation = operation;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (this.module.progressBar) {
|
|
478
|
+
this.module.progressBar.update(currentProgress, {
|
|
479
|
+
operation: chalk.cyan(this.currentOperation.padEnd(20).substring(0, 20))
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await migratorWithProgress.migrate(sourceDir, destFile, version, options);
|
|
486
|
+
} finally {
|
|
487
|
+
this.module.stopProgress();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async estimateWorkload(sourceDir, options) {
|
|
492
|
+
let totalLines = 0;
|
|
493
|
+
const files = [
|
|
494
|
+
'LoincTable/Loinc.csv',
|
|
495
|
+
'AccessoryFiles/PartFile/Part.csv'
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
const optionalFiles = [
|
|
499
|
+
'AccessoryFiles/ConsumerName/ConsumerName.csv',
|
|
500
|
+
'AccessoryFiles/AnswerFile/AnswerList.csv',
|
|
501
|
+
'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
|
|
502
|
+
'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
|
|
503
|
+
'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv'
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
// Count main files
|
|
507
|
+
for (const file of [...files, ...optionalFiles]) {
|
|
508
|
+
const filePath = path.join(sourceDir, file);
|
|
509
|
+
if (fs.existsSync(filePath)) {
|
|
510
|
+
totalLines += await this.countLines(filePath);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Count language variant files if not main-only
|
|
515
|
+
if (!options.mainOnly) {
|
|
516
|
+
const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
|
|
517
|
+
if (fs.existsSync(linguisticVariantsDir)) {
|
|
518
|
+
const files = fs.readdirSync(linguisticVariantsDir);
|
|
519
|
+
for (const file of files) {
|
|
520
|
+
if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
|
|
521
|
+
const filePath = path.join(linguisticVariantsDir, file);
|
|
522
|
+
totalLines += await this.countLines(filePath);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return Math.max(totalLines - 20, 1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async countLines(filePath) {
|
|
532
|
+
return new Promise((resolve, reject) => {
|
|
533
|
+
let lineCount = 0;
|
|
534
|
+
const rl = readline.createInterface({
|
|
535
|
+
input: fs.createReadStream(filePath),
|
|
536
|
+
crlfDelay: Infinity
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
rl.on('line', () => lineCount++);
|
|
540
|
+
rl.on('close', () => resolve(lineCount));
|
|
541
|
+
rl.on('error', reject);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
class LoincDataMigrator {
|
|
547
|
+
constructor(progressCallback = null) {
|
|
548
|
+
this.progressCallback = progressCallback;
|
|
549
|
+
this.currentProgress = 0;
|
|
550
|
+
this.stepCount = 16;
|
|
551
|
+
this.currentOperation = 'Initializing';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
updateProgress(amount = 1, operation = null) {
|
|
555
|
+
this.currentProgress += amount;
|
|
556
|
+
if (operation) {
|
|
557
|
+
this.currentOperation = operation;
|
|
558
|
+
}
|
|
559
|
+
if (this.progressCallback) {
|
|
560
|
+
this.progressCallback(this.currentProgress, this.currentOperation);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async migrate(sourceDir, destFile, version = 'unknown', options = {}) {
|
|
565
|
+
if (options.verbose) console.log('Starting LOINC data migration...');
|
|
566
|
+
|
|
567
|
+
const destDir = path.dirname(destFile);
|
|
568
|
+
if (!fs.existsSync(destDir)) {
|
|
569
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (fs.existsSync(destFile)) {
|
|
573
|
+
fs.unlinkSync(destFile);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const db = new sqlite3.Database(destFile);
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
// Initialize tracking variables
|
|
580
|
+
this.codeKey = 0;
|
|
581
|
+
this.relKey = 0;
|
|
582
|
+
this.descKey = 0;
|
|
583
|
+
this.propKey = 0;
|
|
584
|
+
this.propValueKey = 0;
|
|
585
|
+
this.langKey = 1;
|
|
586
|
+
|
|
587
|
+
this.codes = new Map();
|
|
588
|
+
this.codeList = [];
|
|
589
|
+
this.statii = new Map();
|
|
590
|
+
this.langs = new Map();
|
|
591
|
+
this.rels = new Map();
|
|
592
|
+
this.dTypes = new Map();
|
|
593
|
+
this.props = new Map();
|
|
594
|
+
this.propValues = new Map();
|
|
595
|
+
this.partNames = new Map();
|
|
596
|
+
|
|
597
|
+
// Create tables and initial data
|
|
598
|
+
await this.createTables(db, version, options.verbose);
|
|
599
|
+
|
|
600
|
+
// Discover language variants first
|
|
601
|
+
const languageVariants = await this.discoverLanguageVariants(sourceDir);
|
|
602
|
+
if (!options.mainOnly) {
|
|
603
|
+
this.stepCount = 12 + languageVariants.length;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Process all data with proper operation names
|
|
607
|
+
this.updateProgress(0, 'Language Variants');
|
|
608
|
+
await this.processLanguageVariants(db, sourceDir, 2, options);
|
|
609
|
+
|
|
610
|
+
this.updateProgress(0, 'Parts');
|
|
611
|
+
await this.processParts(db, sourceDir, 3, options);
|
|
612
|
+
|
|
613
|
+
this.updateProgress(0, 'Main Codes');
|
|
614
|
+
await this.processCodes(db, sourceDir, 4, options);
|
|
615
|
+
|
|
616
|
+
this.updateProgress(0, 'Consumer Names');
|
|
617
|
+
await this.processConsumerNames(db, sourceDir, 5, options);
|
|
618
|
+
|
|
619
|
+
this.updateProgress(0, 'Answer Lists');
|
|
620
|
+
await this.processLists(db, sourceDir, 6, options);
|
|
621
|
+
|
|
622
|
+
this.updateProgress(0, 'Part Links');
|
|
623
|
+
await this.processPartLinks(db, sourceDir, 7, options);
|
|
624
|
+
|
|
625
|
+
this.updateProgress(0, 'List Links');
|
|
626
|
+
await this.processListLinks(db, sourceDir, 8, options);
|
|
627
|
+
|
|
628
|
+
this.updateProgress(0, 'Hierarchy');
|
|
629
|
+
await this.processHierarchy(db, sourceDir, 9, options);
|
|
630
|
+
|
|
631
|
+
this.updateProgress(0, 'Property Values');
|
|
632
|
+
await this.processPropertyValues(db, 10, options);
|
|
633
|
+
|
|
634
|
+
this.updateProgress(0, 'Closure Table');
|
|
635
|
+
await this.storeClosureTable(db, 11, options);
|
|
636
|
+
|
|
637
|
+
// Process individual language variants
|
|
638
|
+
if (!options.mainOnly) {
|
|
639
|
+
for (let i = 0; i < languageVariants.length; i++) {
|
|
640
|
+
this.updateProgress(0, `Lang: ${languageVariants[i]}`);
|
|
641
|
+
await this.processLanguage(db, sourceDir, 12 + i, languageVariants[i], options);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (options.verbose) console.log('LOINC data migration completed successfully');
|
|
646
|
+
} finally {
|
|
647
|
+
await this.closeDatabase(db, options.verbose);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async createTables(db, version, verbose = true) {
|
|
652
|
+
if (verbose) console.log('Creating database tables...');
|
|
653
|
+
|
|
654
|
+
const tableSQL = [
|
|
655
|
+
`CREATE TABLE Config (
|
|
656
|
+
ConfigKey INTEGER NOT NULL PRIMARY KEY,
|
|
657
|
+
Value TEXT NOT NULL
|
|
658
|
+
)`,
|
|
659
|
+
`CREATE TABLE Types (
|
|
660
|
+
TypeKey INTEGER NOT NULL PRIMARY KEY,
|
|
661
|
+
Code TEXT NOT NULL
|
|
662
|
+
)`,
|
|
663
|
+
`CREATE TABLE Languages (
|
|
664
|
+
LanguageKey INTEGER NOT NULL PRIMARY KEY,
|
|
665
|
+
Code TEXT NOT NULL,
|
|
666
|
+
Description TEXT NOT NULL
|
|
667
|
+
)`,
|
|
668
|
+
`CREATE TABLE StatusCodes (
|
|
669
|
+
StatusKey INTEGER NOT NULL PRIMARY KEY,
|
|
670
|
+
Description TEXT NOT NULL
|
|
671
|
+
)`,
|
|
672
|
+
`CREATE TABLE RelationshipTypes (
|
|
673
|
+
RelationshipTypeKey INTEGER NOT NULL PRIMARY KEY,
|
|
674
|
+
Description TEXT NOT NULL
|
|
675
|
+
)`,
|
|
676
|
+
`CREATE TABLE DescriptionTypes (
|
|
677
|
+
DescriptionTypeKey INTEGER NOT NULL PRIMARY KEY,
|
|
678
|
+
Description TEXT NOT NULL
|
|
679
|
+
)`,
|
|
680
|
+
`CREATE TABLE PropertyTypes (
|
|
681
|
+
PropertyTypeKey INTEGER NOT NULL PRIMARY KEY,
|
|
682
|
+
Description TEXT NOT NULL
|
|
683
|
+
)`,
|
|
684
|
+
`CREATE TABLE Codes (
|
|
685
|
+
CodeKey INTEGER NOT NULL PRIMARY KEY,
|
|
686
|
+
Code TEXT NOT NULL,
|
|
687
|
+
Type INTEGER NOT NULL,
|
|
688
|
+
RelationshipKey INTEGER NULL,
|
|
689
|
+
StatusKey INTEGER NOT NULL,
|
|
690
|
+
Description TEXT NOT NULL
|
|
691
|
+
)`,
|
|
692
|
+
`CREATE TABLE Relationships (
|
|
693
|
+
RelationshipKey INTEGER NOT NULL PRIMARY KEY,
|
|
694
|
+
RelationshipTypeKey INTEGER NOT NULL,
|
|
695
|
+
SourceKey INTEGER NOT NULL,
|
|
696
|
+
TargetKey INTEGER NOT NULL,
|
|
697
|
+
StatusKey INTEGER NOT NULL
|
|
698
|
+
)`,
|
|
699
|
+
`CREATE TABLE PropertyValues (
|
|
700
|
+
PropertyValueKey INTEGER NOT NULL PRIMARY KEY,
|
|
701
|
+
Value TEXT NOT NULL
|
|
702
|
+
)`,
|
|
703
|
+
`CREATE TABLE Properties (
|
|
704
|
+
PropertyKey INTEGER NOT NULL PRIMARY KEY,
|
|
705
|
+
PropertyTypeKey INTEGER NOT NULL,
|
|
706
|
+
CodeKey INTEGER NOT NULL,
|
|
707
|
+
PropertyValueKey INTEGER NOT NULL
|
|
708
|
+
)`,
|
|
709
|
+
`CREATE TABLE Descriptions (
|
|
710
|
+
DescriptionKey INTEGER NOT NULL PRIMARY KEY,
|
|
711
|
+
CodeKey INTEGER NOT NULL,
|
|
712
|
+
LanguageKey INTEGER NOT NULL,
|
|
713
|
+
DescriptionTypeKey INTEGER NOT NULL,
|
|
714
|
+
Value TEXT NOT NULL
|
|
715
|
+
)`,
|
|
716
|
+
`CREATE TABLE Closure (
|
|
717
|
+
AncestorKey INTEGER NOT NULL,
|
|
718
|
+
DescendentKey INTEGER NOT NULL,
|
|
719
|
+
PRIMARY KEY (AncestorKey, DescendentKey)
|
|
720
|
+
)`,
|
|
721
|
+
`CREATE VIRTUAL TABLE TextIndex USING fts5(
|
|
722
|
+
codekey UNINDEXED,
|
|
723
|
+
type UNINDEXED,
|
|
724
|
+
lang UNINDEXED,
|
|
725
|
+
text
|
|
726
|
+
)`
|
|
727
|
+
];
|
|
728
|
+
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
db.serialize(() => {
|
|
731
|
+
tableSQL.forEach(sql => {
|
|
732
|
+
db.run(sql, (err) => {
|
|
733
|
+
if (err) return reject(err);
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
this.insertInitialData(db, version);
|
|
738
|
+
if (verbose) console.log('Database tables created');
|
|
739
|
+
resolve();
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
insertInitialData(db, version) {
|
|
745
|
+
// Config
|
|
746
|
+
db.run('INSERT INTO Config (ConfigKey, Value) VALUES (1, "c3c89b66-5930-4aa2-8962-124561a5f8c1")');
|
|
747
|
+
db.run('INSERT INTO Config (ConfigKey, Value) VALUES (2, ?)', [version]);
|
|
748
|
+
|
|
749
|
+
// Types
|
|
750
|
+
db.run('INSERT INTO Types (TypeKey, Code) VALUES (1, "Code")');
|
|
751
|
+
db.run('INSERT INTO Types (TypeKey, Code) VALUES (2, "Part")');
|
|
752
|
+
db.run('INSERT INTO Types (TypeKey, Code) VALUES (3, "AnswerList")');
|
|
753
|
+
db.run('INSERT INTO Types (TypeKey, Code) VALUES (4, "Answer")');
|
|
754
|
+
|
|
755
|
+
// Status codes
|
|
756
|
+
const statusCodes = [
|
|
757
|
+
[0, 'NotStated'], [1, 'ACTIVE'], [2, 'DEPRECATED'], [3, 'TRIAL'],
|
|
758
|
+
[4, 'DISCOURAGED'], [5, 'EXAMPLE'], [6, 'PREFERRED'], [7, 'Primary'],
|
|
759
|
+
[8, 'DocumentOntology'], [9, 'Radiology'], [10, 'NORMATIVE']
|
|
760
|
+
];
|
|
761
|
+
statusCodes.forEach(([key, desc]) => {
|
|
762
|
+
db.run('INSERT INTO StatusCodes (StatusKey, Description) VALUES (?, ?)', [key, desc]);
|
|
763
|
+
this.statii.set(desc, key);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// Relationship types
|
|
767
|
+
const relationshipTypes = [
|
|
768
|
+
[0, 'N/A'], [1, 'adjustment'], [2, 'challenge'], [3, 'CLASS'], [4, 'COMPONENT'],
|
|
769
|
+
[5, 'count'], [6, 'analyte-divisor'], [7, 'document-kind'], [8, 'document-role'],
|
|
770
|
+
[9, 'document-setting'], [10, 'document-subject-matter-domain'], [11, 'document-type-of-service'],
|
|
771
|
+
[12, 'analyte-gene'], [13, 'METHOD_TYP'], [14, 'PROPERTY'], [15, 'rad-anatomic-location-imaging-focus'],
|
|
772
|
+
[16, 'rad-guidance-for-action'], [17, 'rad-guidance-for-approach'], [18, 'rad-guidance-for-object'],
|
|
773
|
+
[19, 'rad-guidance-for-presence'], [20, 'rad-maneuver-maneuver-type'], [21, 'rad-modality-modality-subtype'],
|
|
774
|
+
[22, 'rad-modality-modality-type'], [23, 'rad-pharmaceutical-route'], [24, 'rad-pharmaceutical-substance-given'],
|
|
775
|
+
[25, 'rad-reason-for-exam'], [26, 'rad-subject'], [27, 'rad-timing'], [28, 'rad-view-aggregation'],
|
|
776
|
+
[29, 'rad-view-view-type'], [30, 'SCALE_TYP'], [31, 'analyte-suffix'], [32, 'super-system'],
|
|
777
|
+
[33, 'SYSTEM'], [34, 'TIME_ASPCT'], [35, 'time-modifier'], [36, 'rad-anatomic-location-laterality'],
|
|
778
|
+
[37, 'rad-anatomic-location-laterality-presence'], [38, 'rad-anatomic-location-region-imaged'],
|
|
779
|
+
[39, 'AnswerList'], [40, 'Answer'], [41, 'answers-for'], [42, 'parent'], [43, 'child']
|
|
780
|
+
];
|
|
781
|
+
relationshipTypes.forEach(([key, desc]) => {
|
|
782
|
+
db.run('INSERT INTO RelationshipTypes (RelationshipTypeKey, Description) VALUES (?, ?)', [key, desc]);
|
|
783
|
+
this.rels.set(desc, key);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Description types
|
|
787
|
+
const descriptionTypes = [
|
|
788
|
+
[1, 'LONG_COMMON_NAME'], [2, 'SHORTNAME'], [3, 'ConsumerName'],
|
|
789
|
+
[4, 'RELATEDNAMES2'], [5, 'DisplayName'], [6, 'LinguisticVariantDisplayName']
|
|
790
|
+
];
|
|
791
|
+
descriptionTypes.forEach(([key, desc]) => {
|
|
792
|
+
db.run('INSERT INTO DescriptionTypes (DescriptionTypeKey, Description) VALUES (?, ?)', [key, desc]);
|
|
793
|
+
this.dTypes.set(desc, key);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Property types
|
|
797
|
+
const propertyTypes = [
|
|
798
|
+
[1, 'CLASSTYPE'], [2, 'ORDER_OBS'], [3, 'EXAMPLE_UNITS'], [4, 'EXAMPLE_UCUM_UNITS'],
|
|
799
|
+
[5, 'PanelType'], [6, 'AskAtOrderEntry'], [7, 'UNITSREQUIRED'], [9, 'Copyright'],
|
|
800
|
+
[10, 'ValidHL7AttachmentRequest']
|
|
801
|
+
];
|
|
802
|
+
propertyTypes.forEach(([key, desc]) => {
|
|
803
|
+
db.run('INSERT INTO PropertyTypes (PropertyTypeKey, Description) VALUES (?, ?)', [key, desc]);
|
|
804
|
+
this.props.set(desc, key);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Languages (English US is default)
|
|
808
|
+
db.run('INSERT INTO Languages (LanguageKey, Code, Description) VALUES (1, "en-US", "English (United States)")');
|
|
809
|
+
this.langs.set('en-US', 1);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async discoverLanguageVariants(sourceDir) {
|
|
813
|
+
const languageVariants = [];
|
|
814
|
+
const linguisticVariantsDir = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants');
|
|
815
|
+
|
|
816
|
+
if (fs.existsSync(linguisticVariantsDir)) {
|
|
817
|
+
const files = fs.readdirSync(linguisticVariantsDir);
|
|
818
|
+
for (const file of files) {
|
|
819
|
+
if (file.includes('LinguisticVariant.csv') && !file.startsWith('LinguisticVariants.csv')) {
|
|
820
|
+
const match = file.match(/^([a-z]{2}[A-Z]{2})/);
|
|
821
|
+
if (match) {
|
|
822
|
+
const langCode = match[1].substring(0, 2) + '-' + match[1].substring(2);
|
|
823
|
+
languageVariants.push(langCode);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return languageVariants;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async processLanguageVariants(db, sourceDir, step, options) {
|
|
832
|
+
if (options.verbose) console.log('Processing Language Variants...');
|
|
833
|
+
|
|
834
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv');
|
|
835
|
+
if (!fs.existsSync(filePath)) {
|
|
836
|
+
if (options.verbose) console.warn(`Language variants file not found: ${filePath}`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const rl = readline.createInterface({
|
|
841
|
+
input: fs.createReadStream(filePath),
|
|
842
|
+
crlfDelay: Infinity
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
let lineCount = 0;
|
|
846
|
+
for await (const line of rl) {
|
|
847
|
+
lineCount++;
|
|
848
|
+
if (lineCount === 1) continue;
|
|
849
|
+
|
|
850
|
+
const items = csvSplit(line, 4);
|
|
851
|
+
if (items.length < 4) continue;
|
|
852
|
+
|
|
853
|
+
const key = parseInt(items[0]);
|
|
854
|
+
const langCode = items[1] + '-' + items[2];
|
|
855
|
+
const description = items[3];
|
|
856
|
+
|
|
857
|
+
db.run('INSERT INTO Languages (LanguageKey, Code, Description) VALUES (?, ?, ?)',
|
|
858
|
+
[key, langCode, description]);
|
|
859
|
+
this.langs.set(langCode, key);
|
|
860
|
+
|
|
861
|
+
if (key > this.langKey) this.langKey = key;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async processParts(db, sourceDir, step, options) {
|
|
866
|
+
if (options.verbose) console.log('Processing Parts...');
|
|
867
|
+
|
|
868
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/Part.csv');
|
|
869
|
+
const rl = readline.createInterface({
|
|
870
|
+
input: fs.createReadStream(filePath),
|
|
871
|
+
crlfDelay: Infinity
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
let lineCount = 0;
|
|
875
|
+
let processedCount = 0;
|
|
876
|
+
|
|
877
|
+
for await (const line of rl) {
|
|
878
|
+
lineCount++;
|
|
879
|
+
if (lineCount === 1) continue;
|
|
880
|
+
|
|
881
|
+
const items = csvSplit(line, 5);
|
|
882
|
+
if (items.length < 5) continue;
|
|
883
|
+
|
|
884
|
+
await this.processPartItem(db, items);
|
|
885
|
+
processedCount++;
|
|
886
|
+
|
|
887
|
+
if (processedCount % 100 === 0) {
|
|
888
|
+
this.updateProgress(100);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const remaining = processedCount % 100;
|
|
893
|
+
if (remaining > 0) {
|
|
894
|
+
this.updateProgress(remaining);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (options.verbose) console.log(` Processed ${processedCount} parts`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async processPartItem(db, items) {
|
|
901
|
+
this.codeKey++;
|
|
902
|
+
const codeKey = this.codeKey;
|
|
903
|
+
const code = items[0];
|
|
904
|
+
const type = 2;
|
|
905
|
+
const relKey = this.rels.get(adjustPropName(items[1]));
|
|
906
|
+
const description = items[2];
|
|
907
|
+
const statusKey = this.statii.get(items[4]) || 0;
|
|
908
|
+
|
|
909
|
+
db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
|
|
910
|
+
[codeKey, code, type, relKey, statusKey, description]);
|
|
911
|
+
|
|
912
|
+
const codeInfo = { key: codeKey, children: new Set() };
|
|
913
|
+
this.codes.set(code, codeInfo);
|
|
914
|
+
this.codeList.push(codeInfo);
|
|
915
|
+
this.partNames.set(items[1] + '.' + items[2], items[0]);
|
|
916
|
+
|
|
917
|
+
this.addDescription(db, codeKey, 1, this.dTypes.get('DisplayName'), items[3]);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async processCodes(db, sourceDir, step, options) {
|
|
921
|
+
if (options.verbose) console.log('Processing Main Codes...');
|
|
922
|
+
|
|
923
|
+
const filePath = path.join(sourceDir, 'LoincTable/Loinc.csv');
|
|
924
|
+
const rl = readline.createInterface({
|
|
925
|
+
input: fs.createReadStream(filePath),
|
|
926
|
+
crlfDelay: Infinity
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
let lineCount = 0;
|
|
930
|
+
let processedCount = 0;
|
|
931
|
+
|
|
932
|
+
for await (const line of rl) {
|
|
933
|
+
lineCount++;
|
|
934
|
+
if (lineCount === 1) continue;
|
|
935
|
+
|
|
936
|
+
const items = csvSplit(line, 39);
|
|
937
|
+
if (items.length < 39) continue;
|
|
938
|
+
|
|
939
|
+
await this.processCodeItem(db, items);
|
|
940
|
+
processedCount++;
|
|
941
|
+
|
|
942
|
+
if (processedCount % 100 === 0) {
|
|
943
|
+
this.updateProgress(100);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const remaining = processedCount % 100;
|
|
948
|
+
if (remaining > 0) {
|
|
949
|
+
this.updateProgress(remaining);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (options.verbose) console.log(` Processed ${processedCount} main codes`);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async processCodeItem(db, items) {
|
|
956
|
+
this.codeKey++;
|
|
957
|
+
const codeKey = this.codeKey;
|
|
958
|
+
const code = removeQuotes(items[0]);
|
|
959
|
+
const type = 1;
|
|
960
|
+
const description = removeQuotes(items[25]);
|
|
961
|
+
const statusKey = this.statii.get(items[11]) || 1;
|
|
962
|
+
|
|
963
|
+
db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
|
|
964
|
+
[codeKey, code, type, null, statusKey, description]);
|
|
965
|
+
|
|
966
|
+
const codeInfo = { key: codeKey, children: new Set() };
|
|
967
|
+
this.codes.set(code, codeInfo);
|
|
968
|
+
this.codeList.push(codeInfo);
|
|
969
|
+
|
|
970
|
+
// Add CLASS relationship
|
|
971
|
+
const clsCode = this.partNames.get('CLASS.' + items[7]);
|
|
972
|
+
if (clsCode && this.codes.has(clsCode)) {
|
|
973
|
+
this.addRelationship(db, codeKey, this.codes.get(clsCode).key, this.rels.get('CLASS'));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Add properties
|
|
977
|
+
this.addProperty(db, codeKey, this.props.get('CLASSTYPE'), descClassType(items[13]));
|
|
978
|
+
this.addProperty(db, codeKey, this.props.get('ORDER_OBS'), items[21]);
|
|
979
|
+
this.addProperty(db, codeKey, this.props.get('EXAMPLE_UNITS'), items[24]);
|
|
980
|
+
this.addProperty(db, codeKey, this.props.get('EXAMPLE_UCUM_UNITS'), items[26]);
|
|
981
|
+
this.addProperty(db, codeKey, this.props.get('PanelType'), items[34]);
|
|
982
|
+
this.addProperty(db, codeKey, this.props.get('AskAtOrderEntry'), items[35]);
|
|
983
|
+
this.addProperty(db, codeKey, this.props.get('UNITSREQUIRED'), items[18]);
|
|
984
|
+
this.addProperty(db, codeKey, this.props.get('Copyright'), items[23]);
|
|
985
|
+
this.addProperty(db, codeKey, this.props.get('ValidHL7AttachmentRequest'), items[38]);
|
|
986
|
+
|
|
987
|
+
// Add descriptions
|
|
988
|
+
this.addDescription(db, codeKey, 1, this.dTypes.get('LONG_COMMON_NAME'), description);
|
|
989
|
+
this.addDescription(db, codeKey, 1, this.dTypes.get('ConsumerName'), items[12]);
|
|
990
|
+
this.addDescription(db, codeKey, 1, this.dTypes.get('RELATEDNAMES2'), items[19]);
|
|
991
|
+
this.addDescription(db, codeKey, 1, this.dTypes.get('SHORTNAME'), items[20]);
|
|
992
|
+
this.addDescription(db, codeKey, 1, this.dTypes.get('DisplayName'), items[39]);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async processConsumerNames(db, sourceDir, step, options) {
|
|
996
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/ConsumerName/ConsumerName.csv');
|
|
997
|
+
if (!fs.existsSync(filePath)) {
|
|
998
|
+
if (options.verbose) console.warn(`Consumer names file not found: ${filePath}`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (options.verbose) console.log('Processing Consumer Names...');
|
|
1003
|
+
|
|
1004
|
+
const rl = readline.createInterface({
|
|
1005
|
+
input: fs.createReadStream(filePath),
|
|
1006
|
+
crlfDelay: Infinity
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
let lineCount = 0;
|
|
1010
|
+
let processedCount = 0;
|
|
1011
|
+
for await (const line of rl) {
|
|
1012
|
+
lineCount++;
|
|
1013
|
+
if (lineCount === 1) continue;
|
|
1014
|
+
|
|
1015
|
+
const items = csvSplit(line, 2);
|
|
1016
|
+
if (items.length < 2 || !this.codes.has(items[0])) continue;
|
|
1017
|
+
|
|
1018
|
+
this.addDescription(db, this.codes.get(items[0]).key, 1,
|
|
1019
|
+
this.dTypes.get('ConsumerName'), items[1]);
|
|
1020
|
+
processedCount++;
|
|
1021
|
+
|
|
1022
|
+
if (processedCount % 100 === 0) {
|
|
1023
|
+
this.updateProgress(100);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const remaining = processedCount % 100;
|
|
1028
|
+
if (remaining > 0) {
|
|
1029
|
+
this.updateProgress(remaining);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async processLists(db, sourceDir, step, options) {
|
|
1034
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/AnswerFile/AnswerList.csv');
|
|
1035
|
+
if (!fs.existsSync(filePath)) {
|
|
1036
|
+
if (options.verbose) console.warn(`Answer lists file not found: ${filePath}`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (options.verbose) console.log('Processing Answer Lists...');
|
|
1041
|
+
|
|
1042
|
+
const rl = readline.createInterface({
|
|
1043
|
+
input: fs.createReadStream(filePath),
|
|
1044
|
+
crlfDelay: Infinity
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
let lineCount = 0;
|
|
1048
|
+
let processedCount = 0;
|
|
1049
|
+
let currentList = '';
|
|
1050
|
+
|
|
1051
|
+
for await (const line of rl) {
|
|
1052
|
+
lineCount++;
|
|
1053
|
+
if (lineCount === 1) continue;
|
|
1054
|
+
|
|
1055
|
+
const items = csvSplit(line, 11);
|
|
1056
|
+
if (items.length < 11) continue;
|
|
1057
|
+
|
|
1058
|
+
await this.processListItem(db, items, currentList);
|
|
1059
|
+
currentList = items[0];
|
|
1060
|
+
processedCount++;
|
|
1061
|
+
|
|
1062
|
+
if (processedCount % 100 === 0) {
|
|
1063
|
+
this.updateProgress(100);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const remaining = processedCount % 100;
|
|
1068
|
+
if (remaining > 0) {
|
|
1069
|
+
this.updateProgress(remaining);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async processListItem(db, items, currentList) {
|
|
1074
|
+
const listCode = removeQuotes(items[0]);
|
|
1075
|
+
let listCodeKey;
|
|
1076
|
+
|
|
1077
|
+
if (listCode !== currentList) {
|
|
1078
|
+
this.codeKey++;
|
|
1079
|
+
listCodeKey = this.codeKey;
|
|
1080
|
+
const description = removeQuotes(items[1]);
|
|
1081
|
+
|
|
1082
|
+
db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
|
|
1083
|
+
[listCodeKey, listCode, 3, null, 0, description]);
|
|
1084
|
+
|
|
1085
|
+
this.codes.set(listCode, { key: listCodeKey, children: new Set() });
|
|
1086
|
+
} else {
|
|
1087
|
+
listCodeKey = this.codes.get(listCode).key;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const answerCode = removeQuotes(items[6]);
|
|
1091
|
+
let answerCodeKey;
|
|
1092
|
+
|
|
1093
|
+
if (this.codes.has(answerCode)) {
|
|
1094
|
+
answerCodeKey = this.codes.get(answerCode).key;
|
|
1095
|
+
} else {
|
|
1096
|
+
this.codeKey++;
|
|
1097
|
+
answerCodeKey = this.codeKey;
|
|
1098
|
+
const description = removeQuotes(items[10]);
|
|
1099
|
+
|
|
1100
|
+
db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
|
|
1101
|
+
[answerCodeKey, answerCode, 4, null, 0, description]);
|
|
1102
|
+
|
|
1103
|
+
this.codes.set(answerCode, { key: answerCodeKey, children: new Set() });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
this.addRelationship(db, listCodeKey, answerCodeKey, this.rels.get('Answer'));
|
|
1107
|
+
this.addRelationship(db, answerCodeKey, listCodeKey, this.rels.get('AnswerList'));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async processPartLinks(db, sourceDir, step, options) {
|
|
1111
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/LoincPartLink_Primary.csv');
|
|
1112
|
+
if (!fs.existsSync(filePath)) {
|
|
1113
|
+
if (options.verbose) console.warn(`Part links file not found: ${filePath}`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (options.verbose) console.log('Processing Part Links...');
|
|
1118
|
+
|
|
1119
|
+
const rl = readline.createInterface({
|
|
1120
|
+
input: fs.createReadStream(filePath),
|
|
1121
|
+
crlfDelay: Infinity
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
let lineCount = 0;
|
|
1125
|
+
let processedCount = 0;
|
|
1126
|
+
for await (const line of rl) {
|
|
1127
|
+
lineCount++;
|
|
1128
|
+
if (lineCount === 1) continue;
|
|
1129
|
+
|
|
1130
|
+
const items = csvSplit(line, 7);
|
|
1131
|
+
if (items.length < 7) continue;
|
|
1132
|
+
|
|
1133
|
+
const sourceCode = items[0];
|
|
1134
|
+
const targetCode = items[2];
|
|
1135
|
+
const relType = adjustPropName(items[5]);
|
|
1136
|
+
const status = items[6];
|
|
1137
|
+
|
|
1138
|
+
if (this.codes.has(sourceCode) && this.codes.has(targetCode)) {
|
|
1139
|
+
this.addRelationship(db,
|
|
1140
|
+
this.codes.get(sourceCode).key,
|
|
1141
|
+
this.codes.get(targetCode).key,
|
|
1142
|
+
this.rels.get(relType),
|
|
1143
|
+
this.statii.get(status) || 0
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
processedCount++;
|
|
1147
|
+
|
|
1148
|
+
if (processedCount % 100 === 0) {
|
|
1149
|
+
this.updateProgress(100);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const remaining = processedCount % 100;
|
|
1154
|
+
if (remaining > 0) {
|
|
1155
|
+
this.updateProgress(remaining);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async processListLinks(db, sourceDir, step, options) {
|
|
1160
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv');
|
|
1161
|
+
if (!fs.existsSync(filePath)) {
|
|
1162
|
+
if (options.verbose) console.warn(`List links file not found: ${filePath}`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (options.verbose) console.log('Processing List Links...');
|
|
1167
|
+
|
|
1168
|
+
const rl = readline.createInterface({
|
|
1169
|
+
input: fs.createReadStream(filePath),
|
|
1170
|
+
crlfDelay: Infinity
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
let lineCount = 0;
|
|
1174
|
+
let processedCount = 0;
|
|
1175
|
+
for await (const line of rl) {
|
|
1176
|
+
lineCount++;
|
|
1177
|
+
if (lineCount === 1) continue;
|
|
1178
|
+
|
|
1179
|
+
const items = csvSplit(line, 5);
|
|
1180
|
+
if (items.length < 5) continue;
|
|
1181
|
+
|
|
1182
|
+
const sourceCode = items[0];
|
|
1183
|
+
const targetCode = items[2];
|
|
1184
|
+
const status = items[4];
|
|
1185
|
+
|
|
1186
|
+
if (this.codes.has(sourceCode) && this.codes.has(targetCode)) {
|
|
1187
|
+
const statusKey = this.statii.get(status) || 0;
|
|
1188
|
+
this.addRelationship(db,
|
|
1189
|
+
this.codes.get(sourceCode).key,
|
|
1190
|
+
this.codes.get(targetCode).key,
|
|
1191
|
+
this.rels.get('AnswerList'),
|
|
1192
|
+
statusKey
|
|
1193
|
+
);
|
|
1194
|
+
this.addRelationship(db,
|
|
1195
|
+
this.codes.get(targetCode).key,
|
|
1196
|
+
this.codes.get(sourceCode).key,
|
|
1197
|
+
this.rels.get('answers-for'),
|
|
1198
|
+
statusKey
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
processedCount++;
|
|
1202
|
+
|
|
1203
|
+
if (processedCount % 100 === 0) {
|
|
1204
|
+
this.updateProgress(100);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const remaining = processedCount % 100;
|
|
1209
|
+
if (remaining > 0) {
|
|
1210
|
+
this.updateProgress(remaining);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async processHierarchy(db, sourceDir, step, options) {
|
|
1215
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv');
|
|
1216
|
+
if (!fs.existsSync(filePath)) {
|
|
1217
|
+
if (options.verbose) console.warn(`Hierarchy file not found: ${filePath}`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (options.verbose) console.log('Processing Hierarchy...');
|
|
1222
|
+
|
|
1223
|
+
const rl = readline.createInterface({
|
|
1224
|
+
input: fs.createReadStream(filePath),
|
|
1225
|
+
crlfDelay: Infinity
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
let lineCount = 0;
|
|
1229
|
+
let processedCount = 0;
|
|
1230
|
+
for await (const line of rl) {
|
|
1231
|
+
lineCount++;
|
|
1232
|
+
if (lineCount === 1) continue;
|
|
1233
|
+
|
|
1234
|
+
const items = csvSplit(line, 12);
|
|
1235
|
+
if (items.length < 5) continue;
|
|
1236
|
+
|
|
1237
|
+
await this.processHierarchyItem(db, items);
|
|
1238
|
+
processedCount++;
|
|
1239
|
+
|
|
1240
|
+
if (processedCount % 100 === 0) {
|
|
1241
|
+
this.updateProgress(100);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const remaining = processedCount % 100;
|
|
1246
|
+
if (remaining > 0) {
|
|
1247
|
+
this.updateProgress(remaining);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
async processHierarchyItem(db, items) {
|
|
1252
|
+
const pathCode = items[3];
|
|
1253
|
+
const parentPath = items[2];
|
|
1254
|
+
const description = items[4];
|
|
1255
|
+
|
|
1256
|
+
if (!this.codes.has(pathCode)) {
|
|
1257
|
+
this.codeKey++;
|
|
1258
|
+
const codeKey = this.codeKey;
|
|
1259
|
+
|
|
1260
|
+
db.run('INSERT INTO Codes (CodeKey, Code, Type, RelationshipKey, StatusKey, Description) VALUES (?, ?, ?, ?, ?, ?)',
|
|
1261
|
+
[codeKey, pathCode, 2, 0, 0, description]);
|
|
1262
|
+
|
|
1263
|
+
const codeInfo = { key: codeKey, children: new Set() };
|
|
1264
|
+
this.codes.set(pathCode, codeInfo);
|
|
1265
|
+
this.codeList.push(codeInfo);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (!parentPath) {
|
|
1269
|
+
db.run('INSERT INTO Config (ConfigKey, Value) VALUES (3, ?)', [pathCode]);
|
|
1270
|
+
} else if (this.codes.has(parentPath)) {
|
|
1271
|
+
const childKey = this.codes.get(pathCode).key;
|
|
1272
|
+
const parentKey = this.codes.get(parentPath).key;
|
|
1273
|
+
|
|
1274
|
+
this.addRelationship(db, childKey, parentKey, this.rels.get('parent'));
|
|
1275
|
+
this.addRelationship(db, parentKey, childKey, this.rels.get('child'));
|
|
1276
|
+
|
|
1277
|
+
const pathParts = items[0].split('.');
|
|
1278
|
+
for (const ancestorCode of pathParts) {
|
|
1279
|
+
if (this.codes.has(ancestorCode)) {
|
|
1280
|
+
const ancestorInfo = this.codes.get(ancestorCode);
|
|
1281
|
+
ancestorInfo.children.add(childKey);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async processPropertyValues(db, step, options) {
|
|
1288
|
+
if (options.verbose) console.log('Processing Property Values...');
|
|
1289
|
+
|
|
1290
|
+
for (const [value, key] of this.propValues) {
|
|
1291
|
+
db.run('INSERT INTO PropertyValues (PropertyValueKey, Value) VALUES (?, ?)', [key, value]);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async storeClosureTable(db, step, options) {
|
|
1296
|
+
if (options.verbose) console.log('Storing Closure Table...');
|
|
1297
|
+
|
|
1298
|
+
let count = 0;
|
|
1299
|
+
for (const codeInfo of this.codeList) {
|
|
1300
|
+
count++;
|
|
1301
|
+
|
|
1302
|
+
if (codeInfo.children.size > 0) {
|
|
1303
|
+
for (const descendentKey of codeInfo.children) {
|
|
1304
|
+
db.run('INSERT INTO Closure (AncestorKey, DescendentKey) VALUES (?, ?)',
|
|
1305
|
+
[codeInfo.key, descendentKey]);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (count % 100 === 0) {
|
|
1310
|
+
this.updateProgress(100);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const remaining = count % 100;
|
|
1315
|
+
if (remaining > 0) {
|
|
1316
|
+
this.updateProgress(remaining);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async processLanguage(db, sourceDir, step, langCode, options) {
|
|
1321
|
+
if (options.verbose) console.log(`Processing Language ${langCode}...`);
|
|
1322
|
+
|
|
1323
|
+
const langKey = this.langs.get(langCode);
|
|
1324
|
+
if (!langKey) return;
|
|
1325
|
+
|
|
1326
|
+
const baseCode = langCode.replace('-', '');
|
|
1327
|
+
const fileName = `${baseCode}${langKey}LinguisticVariant.csv`;
|
|
1328
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/LinguisticVariants', fileName);
|
|
1329
|
+
|
|
1330
|
+
if (!fs.existsSync(filePath)) {
|
|
1331
|
+
if (options.verbose) console.warn(`Language file not found: ${filePath}`);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const rl = readline.createInterface({
|
|
1336
|
+
input: fs.createReadStream(filePath),
|
|
1337
|
+
crlfDelay: Infinity
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
let lineCount = 0;
|
|
1341
|
+
let processedCount = 0;
|
|
1342
|
+
for await (const line of rl) {
|
|
1343
|
+
lineCount++;
|
|
1344
|
+
if (lineCount === 1) continue;
|
|
1345
|
+
|
|
1346
|
+
const items = csvSplit(line, 12);
|
|
1347
|
+
if (items.length < 12 || !this.codes.has(items[0])) continue;
|
|
1348
|
+
|
|
1349
|
+
const codeKey = this.codes.get(items[0]).key;
|
|
1350
|
+
|
|
1351
|
+
this.addDescription(db, codeKey, langKey, this.dTypes.get('LONG_COMMON_NAME'), items[9]);
|
|
1352
|
+
this.addDescription(db, codeKey, langKey, this.dTypes.get('RELATEDNAMES2'), items[10]);
|
|
1353
|
+
this.addDescription(db, codeKey, langKey, this.dTypes.get('SHORTNAME'), items[8]);
|
|
1354
|
+
this.addDescription(db, codeKey, langKey, this.dTypes.get('LinguisticVariantDisplayName'), items[11]);
|
|
1355
|
+
|
|
1356
|
+
processedCount++;
|
|
1357
|
+
|
|
1358
|
+
if (processedCount % 100 === 0) {
|
|
1359
|
+
this.updateProgress(100);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const remaining = processedCount % 100;
|
|
1364
|
+
if (remaining > 0) {
|
|
1365
|
+
this.updateProgress(remaining);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
addDescription(db, codeKey, languageKey, descriptionType, value) {
|
|
1370
|
+
if (!value) return;
|
|
1371
|
+
|
|
1372
|
+
this.descKey++;
|
|
1373
|
+
db.run('INSERT INTO Descriptions (DescriptionKey, CodeKey, LanguageKey, DescriptionTypeKey, Value) VALUES (?, ?, ?, ?, ?)',
|
|
1374
|
+
[this.descKey, codeKey, languageKey, descriptionType, value]);
|
|
1375
|
+
|
|
1376
|
+
db.run('INSERT INTO TextIndex (codekey, type, lang, text) VALUES (?, ?, ?, ?)',
|
|
1377
|
+
[codeKey, descriptionType, languageKey, value]);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
addProperty(db, codeKey, propertyType, value) {
|
|
1381
|
+
if (!value || !propertyType) return;
|
|
1382
|
+
|
|
1383
|
+
this.propKey++;
|
|
1384
|
+
const propertyValueKey = this.getPropertyValueKey(value);
|
|
1385
|
+
|
|
1386
|
+
db.run('INSERT INTO Properties (PropertyKey, PropertyTypeKey, CodeKey, PropertyValueKey) VALUES (?, ?, ?, ?)',
|
|
1387
|
+
[this.propKey, propertyType, codeKey, propertyValueKey]);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
addRelationship(db, sourceKey, targetKey, relationshipType, statusKey = 0) {
|
|
1391
|
+
if (!relationshipType) return;
|
|
1392
|
+
|
|
1393
|
+
this.relKey++;
|
|
1394
|
+
db.run('INSERT INTO Relationships (RelationshipKey, RelationshipTypeKey, SourceKey, TargetKey, StatusKey) VALUES (?, ?, ?, ?, ?)',
|
|
1395
|
+
[this.relKey, relationshipType, sourceKey, targetKey, statusKey]);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
getPropertyValueKey(value) {
|
|
1399
|
+
if (this.propValues.has(value)) {
|
|
1400
|
+
return this.propValues.get(value);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
this.propValueKey++;
|
|
1404
|
+
this.propValues.set(value, this.propValueKey);
|
|
1405
|
+
return this.propValueKey;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async closeDatabase(db, verbose = true) {
|
|
1409
|
+
return new Promise((resolve) => {
|
|
1410
|
+
db.close((err) => {
|
|
1411
|
+
if (err && verbose) {
|
|
1412
|
+
console.error('Error closing database:', err);
|
|
1413
|
+
}
|
|
1414
|
+
resolve();
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Property name mappings from Pascal code
|
|
1421
|
+
const KNOWN_PROPERTY_NAMES = [
|
|
1422
|
+
'AskAtOrderEntry', 'AssociatedObservations', 'CHANGE_REASON_PUBLIC', 'CHNG_TYPE', 'CLASS', 'CLASSTYPE', 'COMMON_ORDER_RANK', 'COMMON_TEST_RANK', 'COMPONENT', 'CONSUMER_NAME',
|
|
1423
|
+
'DefinitionDescription', 'DisplayName', 'EXAMPLE_UCUM_UNITS', 'EXAMPLE_UNITS', 'EXMPL_ANSWERS', 'EXTERNAL_COPYRIGHT_LINK', 'EXTERNAL_COPYRIGHT_NOTICE', 'FORMULA',
|
|
1424
|
+
'HL7_ATTACHMENT_STRUCTURE', 'HL7_FIELD_SUBFIELD_ID', 'LONG_COMMON_NAME', 'MAP_TO', 'METHOD_TYP', 'ORDER_OBS', 'PROPERTY', 'PanelType', 'RELATEDNAMES2', 'SCALE_TYP',
|
|
1425
|
+
'SHORTNAME', 'STATUS', 'STATUS_REASON', 'STATUS_TEXT', 'SURVEY_QUEST_SRC', 'SURVEY_QUEST_TEXT', 'SYSTEM', 'TIME_ASPCT', 'UNITSREQUIRED', 'ValidHL7AttachmentRequest',
|
|
1426
|
+
'VersionFirstReleased', 'VersionLastChanged', 'adjustment', 'analyte', 'analyte-core', 'analyte-divisor', 'analyte-divisor-suffix', 'analyte-gene', 'analyte-numerator',
|
|
1427
|
+
'analyte-suffix', 'answer-list', 'answers-for', 'category', 'challenge', 'child', 'count', 'document-kind', 'document-role', 'document-setting', 'document-subject-matter-domain',
|
|
1428
|
+
'document-type-of-service', 'parent', 'rad-anatomic-location-imaging-focus', 'rad-anatomic-location-laterality', 'rad-anatomic-location-laterality-presence', 'rad-anatomic-location-region-imaged',
|
|
1429
|
+
'rad-guidance-for-action', 'rad-guidance-for-approach', 'rad-guidance-for-object', 'rad-guidance-for-presence', 'rad-maneuver-maneuver-type', 'rad-modality-modality-subtype',
|
|
1430
|
+
'rad-modality-modality-type', 'rad-pharmaceutical-route', 'rad-pharmaceutical-substance-given', 'rad-reason-for-exam', 'rad-subject', 'rad-timing', 'rad-view-aggregation',
|
|
1431
|
+
'rad-view-view-type', 'search', 'super-system', 'system-core', 'time-core', 'time-modifier',
|
|
1432
|
+
'Answer', 'AnswerList'
|
|
1433
|
+
];
|
|
1434
|
+
|
|
1435
|
+
function adjustPropName(s) {
|
|
1436
|
+
if (KNOWN_PROPERTY_NAMES.includes(s)) {
|
|
1437
|
+
return s;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const mappings = {
|
|
1441
|
+
'ADJUSTMENT': 'adjustment',
|
|
1442
|
+
'CHALLENGE': 'challenge',
|
|
1443
|
+
'COUNT': 'count',
|
|
1444
|
+
'DIVISORS': 'analyte-divisor',
|
|
1445
|
+
'Document.Kind': 'document-kind',
|
|
1446
|
+
'Document.Role': 'document-role',
|
|
1447
|
+
'Document.Setting': 'document-setting',
|
|
1448
|
+
'Document.SubjectMatterDomain': 'document-subject-matter-domain',
|
|
1449
|
+
'Document.TypeOfService': 'document-type-of-service',
|
|
1450
|
+
'GENE': 'analyte-gene',
|
|
1451
|
+
'METHOD': 'METHOD_TYP',
|
|
1452
|
+
'Rad.Anatomic Location.Imaging Focus': 'rad-anatomic-location-imaging-focus',
|
|
1453
|
+
'Rad.Anatomic Location.Laterality': 'rad-anatomic-location-laterality',
|
|
1454
|
+
'Rad.Anatomic Location.Laterality.Presence': 'rad-anatomic-location-laterality-presence',
|
|
1455
|
+
'Rad.Anatomic Location.Region Imaged': 'rad-anatomic-location-region-imaged',
|
|
1456
|
+
'Rad.Guidance for.Action': 'rad-guidance-for-action',
|
|
1457
|
+
'Rad.Guidance for.Approach': 'rad-guidance-for-approach',
|
|
1458
|
+
'Rad.Guidance for.Object': 'rad-guidance-for-object',
|
|
1459
|
+
'Rad.Guidance for.Presence': 'rad-guidance-for-presence',
|
|
1460
|
+
'Rad.Maneuver.Maneuver Type': 'rad-maneuver-maneuver-type',
|
|
1461
|
+
'Rad.Modality.Modality Subtype': 'rad-modality-modality-subtype',
|
|
1462
|
+
'Rad.Modality.Modality Type': 'rad-modality-modality-type',
|
|
1463
|
+
'Rad.Pharmaceutical.Route': 'rad-pharmaceutical-route',
|
|
1464
|
+
'Rad.Pharmaceutical.Substance Given': 'rad-pharmaceutical-substance-given',
|
|
1465
|
+
'Rad.Reason for Exam': 'rad-reason-for-exam',
|
|
1466
|
+
'Rad.Subject': 'rad-subject',
|
|
1467
|
+
'Rad.Timing': 'rad-timing',
|
|
1468
|
+
'Rad.View.Aggregation': 'rad-view-aggregation',
|
|
1469
|
+
'Rad.View.View Type': 'rad-view-view-type',
|
|
1470
|
+
'SCALE': 'SCALE_TYP',
|
|
1471
|
+
'SUFFIX': 'analyte-suffix',
|
|
1472
|
+
'SUPER SYSTEM': 'super-system',
|
|
1473
|
+
'TIME': 'TIME_ASPCT',
|
|
1474
|
+
'TIME MODIFIER': 'time-modifier'
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
if (mappings[s]) {
|
|
1478
|
+
return mappings[s];
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
throw new Error(`Unknown Property Name: ${s}`);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function descClassType(s) {
|
|
1485
|
+
const types = {
|
|
1486
|
+
'1': 'Laboratory class',
|
|
1487
|
+
'2': 'Clinical class',
|
|
1488
|
+
'3': 'Claims attachment',
|
|
1489
|
+
'4': 'Surveys'
|
|
1490
|
+
};
|
|
1491
|
+
return types[s] || s;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// CSV parsing utility
|
|
1495
|
+
function csvSplit(line, expectedCount) {
|
|
1496
|
+
const result = new Array(expectedCount).fill('');
|
|
1497
|
+
let inQuoted = false;
|
|
1498
|
+
let currentField = 0;
|
|
1499
|
+
let fieldStart = 0;
|
|
1500
|
+
let i = 0;
|
|
1501
|
+
|
|
1502
|
+
while (i < line.length && currentField < expectedCount) {
|
|
1503
|
+
const ch = line[i];
|
|
1504
|
+
|
|
1505
|
+
if (!inQuoted && ch === ',') {
|
|
1506
|
+
if (currentField < expectedCount) {
|
|
1507
|
+
result[currentField] = line.substring(fieldStart, i).replace(/^"|"$/g, '').replace(/""/g, '"');
|
|
1508
|
+
currentField++;
|
|
1509
|
+
fieldStart = i + 1;
|
|
1510
|
+
}
|
|
1511
|
+
} else if (ch === '"') {
|
|
1512
|
+
if (inQuoted && i + 1 < line.length && line[i + 1] === '"') {
|
|
1513
|
+
i++;
|
|
1514
|
+
} else {
|
|
1515
|
+
inQuoted = !inQuoted;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
i++;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (currentField < expectedCount) {
|
|
1522
|
+
result[currentField] = line.substring(fieldStart).replace(/^"|"$/g, '').replace(/""/g, '"');
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return result;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function removeQuotes(str) {
|
|
1529
|
+
if (!str) return '';
|
|
1530
|
+
return str.replace(/^"|"$/g, '');
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
module.exports = {
|
|
1534
|
+
LoincModule,
|
|
1535
|
+
LoincDataMigrator
|
|
1536
|
+
};
|