fhirsmith 0.18.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
SnomedRelationshipList, SnomedReferenceSetMembers, SnomedReferenceSetIndex
|
|
13
13
|
} = require('../sct/structures');
|
|
14
14
|
const {SnomedExpressionServices} = require("../sct/expressions");
|
|
15
|
+
const terminal = require('./terminal');
|
|
15
16
|
|
|
16
17
|
// definitionStatusId values from the RF2 concept file
|
|
17
18
|
const SNOMED_DEFINITION_STATUS_PRIMITIVE = 900000000000074008n;
|
|
@@ -106,16 +107,16 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
106
107
|
} catch (error) {
|
|
107
108
|
this.logError(`Import command failed: ${error.message}`);
|
|
108
109
|
if (options.verbose) {
|
|
109
|
-
|
|
110
|
+
terminal.error(error.stack);
|
|
110
111
|
}
|
|
111
112
|
throw error;
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
async confirmImport(config) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
|
|
118
|
+
terminal.log(` Source: ${chalk.white(config.source)}`);
|
|
119
|
+
terminal.log(` Destination: ${chalk.white(config.dest)}`);
|
|
119
120
|
|
|
120
121
|
if (config.edition) {
|
|
121
122
|
const editions = {
|
|
@@ -125,26 +126,26 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
125
126
|
// ... other editions
|
|
126
127
|
};
|
|
127
128
|
const editionName = editions[config.edition] || `Edition ${config.edition}`;
|
|
128
|
-
|
|
129
|
+
terminal.log(` Edition: ${chalk.white(editionName)} (${config.edition})`);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
if (config.version) {
|
|
132
|
-
|
|
133
|
+
terminal.log(` Version: ${chalk.white(config.version)}`);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
if (config.uri) {
|
|
136
|
-
|
|
137
|
+
terminal.log(` Version URI: ${chalk.white(config.uri)}`);
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
if (config.language) {
|
|
140
|
-
|
|
141
|
+
terminal.log(` Language: ${chalk.white(config.language)}`);
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
145
|
+
terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
|
|
145
146
|
|
|
146
147
|
if (config.estimatedDuration) {
|
|
147
|
-
|
|
148
|
+
terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
const { confirmed } = await inquirer.prompt({
|
|
@@ -357,7 +358,7 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
357
358
|
} catch (error) {
|
|
358
359
|
// Silent fail - auto-detection is best-effort
|
|
359
360
|
if (this.config?.verbose) {
|
|
360
|
-
|
|
361
|
+
terminal.log(`Auto-detection warning: ${error.message}`);
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
|
|
@@ -661,7 +662,7 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
661
662
|
|
|
662
663
|
async runImportWithoutConfigSaving(config) {
|
|
663
664
|
try {
|
|
664
|
-
|
|
665
|
+
terminal.log(chalk.blue.bold(`🏥 Starting ${this.getName()} Import...\n`));
|
|
665
666
|
|
|
666
667
|
// Pre-flight checks
|
|
667
668
|
this.logInfo('Running pre-flight checks...');
|
|
@@ -680,7 +681,7 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
680
681
|
this.stopProgress();
|
|
681
682
|
this.logError(`${this.getName()} import failed: ${error.message}`);
|
|
682
683
|
if (config.verbose) {
|
|
683
|
-
|
|
684
|
+
terminal.error(error.stack);
|
|
684
685
|
}
|
|
685
686
|
process.exit(1);
|
|
686
687
|
}
|
|
@@ -816,10 +817,10 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
816
817
|
const stats = await this.validateSnomedDirectory(options.source);
|
|
817
818
|
|
|
818
819
|
this.logSuccess('Directory validation passed');
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
820
|
+
terminal.log(` Concept files: ${stats.conceptFiles}`);
|
|
821
|
+
terminal.log(` Description files: ${stats.descriptionFiles}`);
|
|
822
|
+
terminal.log(` Relationship files: ${stats.relationshipFiles}`);
|
|
823
|
+
terminal.log(` Refset directories: ${stats.refsetDirectories}`);
|
|
823
824
|
|
|
824
825
|
} catch (error) {
|
|
825
826
|
this.logError(`Validation failed: ${error.message}`);
|
|
@@ -843,26 +844,26 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
843
844
|
const data = await reader.loadSnomedData();
|
|
844
845
|
|
|
845
846
|
this.logSuccess('Cache file status:');
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
847
|
+
terminal.log(` Cache Version: ${data.cacheVersion}`);
|
|
848
|
+
terminal.log(` Version URI: ${data.versionUri}`);
|
|
849
|
+
terminal.log(` Version Date: ${data.versionDate}`);
|
|
850
|
+
terminal.log(` Edition: ${data.edition}`);
|
|
851
|
+
terminal.log(` SNOMED Version: ${data.version}`);
|
|
851
852
|
|
|
852
853
|
// Create structure instances to get counts
|
|
853
854
|
const concepts = new SnomedConceptList(data.concept);
|
|
854
855
|
const descriptions = new SnomedDescriptions(data.desc);
|
|
855
856
|
const relationships = new SnomedRelationshipList(data.rel);
|
|
856
857
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
858
|
+
terminal.log(` Concepts: ${concepts.count().toLocaleString()}`);
|
|
859
|
+
terminal.log(` Descriptions: ${descriptions.count().toLocaleString()}`);
|
|
860
|
+
terminal.log(` Relationships: ${relationships.count().toLocaleString()}`);
|
|
861
|
+
terminal.log(` Active Roots: ${data.activeRoots.length}`);
|
|
862
|
+
terminal.log(` Inactive Roots: ${data.inactiveRoots.length}`);
|
|
862
863
|
|
|
863
864
|
const fileStat = fs.statSync(cachePath);
|
|
864
|
-
|
|
865
|
-
|
|
865
|
+
terminal.log(` File Size: ${(fileStat.size / (1024 * 1024 * 1024)).toFixed(2)} GB`);
|
|
866
|
+
terminal.log(` Last Modified: ${fileStat.mtime.toISOString()}`);
|
|
866
867
|
|
|
867
868
|
} catch (error) {
|
|
868
869
|
this.logError(`Status check failed: ${error.message}`);
|
|
@@ -924,7 +925,7 @@ class SnomedImporterWithProgress {
|
|
|
924
925
|
message += ` (${rate.toLocaleString()} items/sec)`;
|
|
925
926
|
}
|
|
926
927
|
|
|
927
|
-
|
|
928
|
+
terminal.log(message);
|
|
928
929
|
|
|
929
930
|
// Clean up
|
|
930
931
|
this.taskStartTimes.delete(taskName);
|
|
@@ -1139,6 +1140,9 @@ class SnomedImporter {
|
|
|
1139
1140
|
// Step 10a: Read alternate identifiers (e.g. the LOINC codes of the LOINC Extension)
|
|
1140
1141
|
await this.readIdentifiers();
|
|
1141
1142
|
|
|
1143
|
+
// Step 10b: Read concrete value relationships (sct2_RelationshipConcreteValues)
|
|
1144
|
+
await this.readConcreteValues();
|
|
1145
|
+
|
|
1142
1146
|
// Step 11: Link concepts
|
|
1143
1147
|
this.linkConcepts();
|
|
1144
1148
|
|
|
@@ -1158,7 +1162,7 @@ class SnomedImporter {
|
|
|
1158
1162
|
await this.saveCache();
|
|
1159
1163
|
|
|
1160
1164
|
} catch (error) {
|
|
1161
|
-
|
|
1165
|
+
terminal.error('DEBUG: Import failed with error:', error);
|
|
1162
1166
|
throw new Error(`Import failed: ${error.message}`);
|
|
1163
1167
|
}
|
|
1164
1168
|
}
|
|
@@ -1169,6 +1173,7 @@ class SnomedImporter {
|
|
|
1169
1173
|
descriptions: [],
|
|
1170
1174
|
relationships: [],
|
|
1171
1175
|
identifiers: [],
|
|
1176
|
+
concreteValues: [],
|
|
1172
1177
|
refsetDirectories: []
|
|
1173
1178
|
};
|
|
1174
1179
|
|
|
@@ -1177,7 +1182,7 @@ class SnomedImporter {
|
|
|
1177
1182
|
// extension content is layered on top.
|
|
1178
1183
|
if (this.config.base) {
|
|
1179
1184
|
if (this.config.verbose) {
|
|
1180
|
-
|
|
1185
|
+
terminal.log(`Loading base edition from: ${this.config.base}`);
|
|
1181
1186
|
}
|
|
1182
1187
|
this._scanDirectory(this.config.base, files);
|
|
1183
1188
|
}
|
|
@@ -1224,9 +1229,11 @@ class SnomedImporter {
|
|
|
1224
1229
|
files.relationships.push(filePath);
|
|
1225
1230
|
} else if (files.identifiers && firstLine.startsWith('alternateIdentifier\teffectiveTime\tactive\tmoduleId\tidentifierSchemeId\treferencedComponentId')) {
|
|
1226
1231
|
files.identifiers.push(filePath);
|
|
1232
|
+
} else if (files.concreteValues && firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tvalue\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId')) {
|
|
1233
|
+
files.concreteValues.push(filePath);
|
|
1227
1234
|
}
|
|
1228
1235
|
} catch (error) {
|
|
1229
|
-
|
|
1236
|
+
terminal.log(`DEBUG: Error reading file ${filePath}: ${error.message}`);
|
|
1230
1237
|
}
|
|
1231
1238
|
}
|
|
1232
1239
|
|
|
@@ -1522,14 +1529,15 @@ class SnomedImporter {
|
|
|
1522
1529
|
const month = parseInt(dateStr.substring(4, 6));
|
|
1523
1530
|
const day = parseInt(dateStr.substring(6, 8));
|
|
1524
1531
|
|
|
1525
|
-
//
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
//
|
|
1529
|
-
const
|
|
1532
|
+
// Days since the Pascal TDateTime epoch (December 30, 1899), counted in UTC. Local dates are
|
|
1533
|
+
// not whole days apart: 1899 local time is the zone's historical mean time (e.g. +9:39:52 in
|
|
1534
|
+
// Melbourne) and today's has daylight saving, so the difference came out a day short in zones
|
|
1535
|
+
// ahead of UTC, and every effectiveTime in a cache built there was a day early
|
|
1536
|
+
const targetDate = Date.UTC(year, month - 1, day);
|
|
1537
|
+
const pascalEpoch = Date.UTC(1899, 11, 30); // Month is 0-based in JS
|
|
1530
1538
|
|
|
1531
1539
|
// Calculate days difference
|
|
1532
|
-
const daysDiff = Math.
|
|
1540
|
+
const daysDiff = Math.round((targetDate - pascalEpoch) / (1000 * 60 * 60 * 24));
|
|
1533
1541
|
|
|
1534
1542
|
// Ensure it fits in 16 bits (0-65535) and is positive
|
|
1535
1543
|
if (daysDiff < 0 || daysDiff > 65535) {
|
|
@@ -1778,7 +1786,59 @@ class SnomedImporter {
|
|
|
1778
1786
|
}
|
|
1779
1787
|
this.identifiers.sort((a, b) => (a.join('|') < b.join('|') ? -1 : 1));
|
|
1780
1788
|
if (this.identifiers.length > 0 || missing > 0) {
|
|
1781
|
-
|
|
1789
|
+
terminal.log(`Alternate identifiers: ${this.identifiers.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Read the concrete value relationship files (sct2_RelationshipConcreteValues_*):
|
|
1795
|
+
* the numeric and string attribute values of concepts (strength, count of base of
|
|
1796
|
+
* active ingredient, ...). Kept as rows [sourceId, typeId, group, value] - the value
|
|
1797
|
+
* as written in RF2 ("#500", "#5.5", "\"text\"", "true") - for the ECL evaluator;
|
|
1798
|
+
* they are not relationships to concepts, so they do not join the relationship
|
|
1799
|
+
* table. Only active, inferred rows; where a row appears more than once the latest
|
|
1800
|
+
* effectiveTime wins.
|
|
1801
|
+
*/
|
|
1802
|
+
async readConcreteValues() {
|
|
1803
|
+
const rows = new Map(); // id -> {effectiveTime, active, row}
|
|
1804
|
+
for (const file of this.files.concreteValues || []) {
|
|
1805
|
+
const rl = readline.createInterface({
|
|
1806
|
+
input: fs.createReadStream(file),
|
|
1807
|
+
crlfDelay: Infinity
|
|
1808
|
+
});
|
|
1809
|
+
let lineCount = 0;
|
|
1810
|
+
for await (const line of rl) {
|
|
1811
|
+
lineCount++;
|
|
1812
|
+
if (lineCount === 1 || !line) continue;
|
|
1813
|
+
const parts = line.split('\t');
|
|
1814
|
+
if (parts.length < 10) continue;
|
|
1815
|
+
const [id, effectiveTime, active, , sourceId, value, group, typeId, characteristicTypeId] = parts.map(p => p.trim());
|
|
1816
|
+
if (characteristicTypeId !== '900000000000011006') continue; // inferred only
|
|
1817
|
+
const prior = rows.get(id);
|
|
1818
|
+
if (!prior || prior.effectiveTime <= effectiveTime) {
|
|
1819
|
+
rows.set(id, { effectiveTime, active: active === '1', row: [sourceId, typeId, parseInt(group, 10) || 0, value] });
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
this.concreteValues = [];
|
|
1824
|
+
let missing = 0;
|
|
1825
|
+
for (const r of rows.values()) {
|
|
1826
|
+
if (!r.active) continue;
|
|
1827
|
+
let known = false;
|
|
1828
|
+
try {
|
|
1829
|
+
known = this.conceptMap.has(BigInt(r.row[0])) && this.conceptMap.has(BigInt(r.row[1]));
|
|
1830
|
+
} catch {
|
|
1831
|
+
known = false;
|
|
1832
|
+
}
|
|
1833
|
+
if (known) {
|
|
1834
|
+
this.concreteValues.push(r.row);
|
|
1835
|
+
} else {
|
|
1836
|
+
missing++;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
this.concreteValues.sort((a, b) => (a.join('|') < b.join('|') ? -1 : 1));
|
|
1840
|
+
if (this.concreteValues.length > 0 || missing > 0) {
|
|
1841
|
+
terminal.log(`Concrete values: ${this.concreteValues.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
|
|
1782
1842
|
}
|
|
1783
1843
|
}
|
|
1784
1844
|
|
|
@@ -2193,7 +2253,7 @@ class SnomedImporter {
|
|
|
2193
2253
|
|
|
2194
2254
|
async processRefsets() {
|
|
2195
2255
|
if (this.files.refsetDirectories.length === 0) {
|
|
2196
|
-
|
|
2256
|
+
terminal.log('No reference set directories found');
|
|
2197
2257
|
this.refsetIndex.doneBuild();
|
|
2198
2258
|
this.refsetMembers.doneBuild();
|
|
2199
2259
|
return;
|
|
@@ -2309,7 +2369,7 @@ class SnomedImporter {
|
|
|
2309
2369
|
const parts = fileName.split('_');
|
|
2310
2370
|
|
|
2311
2371
|
if (parts.length < 3) {
|
|
2312
|
-
|
|
2372
|
+
terminal.log(`Skipping file with unexpected name format: ${fileName}`);
|
|
2313
2373
|
return;
|
|
2314
2374
|
}
|
|
2315
2375
|
|
|
@@ -2333,19 +2393,16 @@ class SnomedImporter {
|
|
|
2333
2393
|
lineNumber++;
|
|
2334
2394
|
|
|
2335
2395
|
if (lineNumber === 1) {
|
|
2336
|
-
// Parse headers
|
|
2396
|
+
// Parse headers. Map reference sets (simple, complex, extended) are loaded like
|
|
2397
|
+
// any other: ECL ^ and ?fhir_vs=refset/ can name any reference set, and the
|
|
2398
|
+
// ICD-10 maps are among the most used.
|
|
2337
2399
|
headers = line.split('\t').map(h => h.trim());
|
|
2338
|
-
|
|
2339
|
-
// Skip if this looks like a mapping reference set
|
|
2340
|
-
if (line.toLowerCase().includes('map')) {
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
2400
|
continue;
|
|
2344
2401
|
}
|
|
2345
2402
|
|
|
2346
2403
|
const fields = line.split('\t');
|
|
2347
2404
|
if (fields.length < 6) {
|
|
2348
|
-
|
|
2405
|
+
terminal.log(`Skipping line ${lineNumber} with insufficient fields: ${fields.length}`);
|
|
2349
2406
|
continue; // Minimum fields required
|
|
2350
2407
|
}
|
|
2351
2408
|
|
|
@@ -2381,14 +2438,14 @@ class SnomedImporter {
|
|
|
2381
2438
|
try {
|
|
2382
2439
|
member.id = this.parseGUID(id);
|
|
2383
2440
|
} catch (error) {
|
|
2384
|
-
|
|
2441
|
+
terminal.log(`Invalid GUID in ${fileName}: ${id}`);
|
|
2385
2442
|
continue;
|
|
2386
2443
|
}
|
|
2387
2444
|
|
|
2388
2445
|
// Find module concept
|
|
2389
2446
|
const moduleConcept = this.conceptMap.get(BigInt(moduleId));
|
|
2390
2447
|
if (!moduleConcept) {
|
|
2391
|
-
|
|
2448
|
+
terminal.log(`Module concept not found: ${moduleId}`);
|
|
2392
2449
|
continue;
|
|
2393
2450
|
}
|
|
2394
2451
|
member.module = moduleConcept.index;
|
|
@@ -2421,7 +2478,7 @@ class SnomedImporter {
|
|
|
2421
2478
|
// Try to find as relationship (simplified - would need relationship ID lookup)
|
|
2422
2479
|
member.kind = 3; // Other/unknown
|
|
2423
2480
|
member.ref = 0;
|
|
2424
|
-
|
|
2481
|
+
terminal.log(`Component not found: ${referencedComponentId}`);
|
|
2425
2482
|
continue;
|
|
2426
2483
|
}
|
|
2427
2484
|
}
|
|
@@ -2444,7 +2501,7 @@ class SnomedImporter {
|
|
|
2444
2501
|
}
|
|
2445
2502
|
|
|
2446
2503
|
} catch (error) {
|
|
2447
|
-
|
|
2504
|
+
terminal.error(`Error processing reference set ${filePath}:`, error);
|
|
2448
2505
|
}
|
|
2449
2506
|
}
|
|
2450
2507
|
|
|
@@ -2532,7 +2589,7 @@ class SnomedImporter {
|
|
|
2532
2589
|
value = descResult.index;
|
|
2533
2590
|
type = 2; // Description
|
|
2534
2591
|
} else {
|
|
2535
|
-
|
|
2592
|
+
terminal.log(`Referenced component not found: ${field}`);
|
|
2536
2593
|
value = 0;
|
|
2537
2594
|
type = 1;
|
|
2538
2595
|
}
|
|
@@ -2789,7 +2846,7 @@ class SnomedImporter {
|
|
|
2789
2846
|
} catch (error) {
|
|
2790
2847
|
// Log the error but continue processing other concepts
|
|
2791
2848
|
if (this.config.verbose) {
|
|
2792
|
-
|
|
2849
|
+
terminal.warn(`Warning: Could not build normal form for concept ${concept.id}: ${error.message}`);
|
|
2793
2850
|
}
|
|
2794
2851
|
}
|
|
2795
2852
|
|
|
@@ -2804,7 +2861,7 @@ class SnomedImporter {
|
|
|
2804
2861
|
}
|
|
2805
2862
|
|
|
2806
2863
|
if (this.config.verbose) {
|
|
2807
|
-
|
|
2864
|
+
terminal.log(`Normal forms: ${normalFormsAdded} concepts have non-trivial normal forms`);
|
|
2808
2865
|
}
|
|
2809
2866
|
}
|
|
2810
2867
|
|
|
@@ -2823,11 +2880,14 @@ class SnomedImporter {
|
|
|
2823
2880
|
progressBar?.update(50);
|
|
2824
2881
|
|
|
2825
2882
|
const identifiers = this.identifiers || [];
|
|
2883
|
+
const concreteValues = this.concreteValues || [];
|
|
2826
2884
|
await writer.writeCache({
|
|
2827
|
-
// 18 = 17 plus the alternate identifiers
|
|
2828
|
-
//
|
|
2829
|
-
|
|
2885
|
+
// 17 = the base format; 18 = 17 plus the alternate identifiers; 19 = 18 plus
|
|
2886
|
+
// the concrete value relationships. Each is only used when it has something
|
|
2887
|
+
// to carry, so that caches without them stay readable by older servers.
|
|
2888
|
+
version: concreteValues.length > 0 ? '19' : identifiers.length > 0 ? '18' : '17',
|
|
2830
2889
|
identifiers: identifiers,
|
|
2890
|
+
concreteValues: concreteValues,
|
|
2831
2891
|
versionUri: this.isTesting ? this.config.uri.replace("/sct/", "/xsct/") : this.config.uri,
|
|
2832
2892
|
versionDate: this.extractDateFromUri(this.config.uri),
|
|
2833
2893
|
|
|
@@ -2914,10 +2974,14 @@ class SnomedCacheWriter {
|
|
|
2914
2974
|
}
|
|
2915
2975
|
buffers.push(this.writeInteger(data.defaultLanguage));
|
|
2916
2976
|
|
|
2917
|
-
if (data.version === '18') {
|
|
2977
|
+
if (data.version === '18' || data.version === '19') {
|
|
2918
2978
|
// alternate identifiers, as JSON: [[scheme, module, code, conceptId], ...]
|
|
2919
2979
|
buffers.push(this.writeBytes(Buffer.from(JSON.stringify(data.identifiers || []), 'utf8')));
|
|
2920
2980
|
}
|
|
2981
|
+
if (data.version === '19') {
|
|
2982
|
+
// concrete value relationships, as JSON: [[sourceId, typeId, group, value], ...]
|
|
2983
|
+
buffers.push(this.writeBytes(Buffer.from(JSON.stringify(data.concreteValues || []), 'utf8')));
|
|
2984
|
+
}
|
|
2921
2985
|
|
|
2922
2986
|
// Write to file
|
|
2923
2987
|
const finalBuffer = Buffer.concat(buffers);
|
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const sqlite3 = require('sqlite3').verbose();
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const readline = require("readline");
|
|
6
|
+
const terminal = require('./terminal');
|
|
6
7
|
|
|
7
8
|
class UniiModule extends BaseTerminologyModule {
|
|
8
9
|
getName() {
|
|
@@ -81,16 +82,16 @@ class UniiModule extends BaseTerminologyModule {
|
|
|
81
82
|
const inquirer = require('inquirer');
|
|
82
83
|
const chalk = require('chalk');
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
terminal.log(chalk.cyan(`\n📋 ${this.getName()} Import Configuration:`));
|
|
86
|
+
terminal.log(` Source: ${chalk.white(config.source)}`);
|
|
87
|
+
terminal.log(` Destination: ${chalk.white(config.dest)}`);
|
|
88
|
+
terminal.log(` Version: ${chalk.white(config.version)}`);
|
|
89
|
+
terminal.log(` Create Indexes: ${chalk.white(config.createIndexes ? 'Yes' : 'No')}`);
|
|
90
|
+
terminal.log(` Overwrite: ${chalk.white(config.overwrite ? 'Yes' : 'No')}`);
|
|
91
|
+
terminal.log(` Verbose: ${chalk.white(config.verbose ? 'Yes' : 'No')}`);
|
|
91
92
|
|
|
92
93
|
if (config.estimatedDuration) {
|
|
93
|
-
|
|
94
|
+
terminal.log(` Estimated Duration: ${chalk.white(config.estimatedDuration)}`);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
const { confirmed } = await inquirer.prompt({
|
|
@@ -120,14 +121,14 @@ class UniiModule extends BaseTerminologyModule {
|
|
|
120
121
|
const stats = await this.validateUniiFile(options.source, parseInt(options.sample));
|
|
121
122
|
|
|
122
123
|
this.logSuccess('File validation passed');
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
terminal.log(` Lines: ${stats.totalLines}`);
|
|
125
|
+
terminal.log(` Estimated UNII codes: ${stats.estimatedCodes}`);
|
|
126
|
+
terminal.log(` Estimated descriptions: ${stats.estimatedDescriptions}`);
|
|
127
|
+
terminal.log(` Sample data format: ${stats.formatValid ? 'Valid' : 'Invalid'}`);
|
|
127
128
|
|
|
128
129
|
if (stats.warnings.length > 0) {
|
|
129
130
|
this.logWarning('Validation warnings:');
|
|
130
|
-
stats.warnings.forEach(warning =>
|
|
131
|
+
stats.warnings.forEach(warning => terminal.log(` ${warning}`));
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
} catch (error) {
|
|
@@ -149,11 +150,11 @@ class UniiModule extends BaseTerminologyModule {
|
|
|
149
150
|
const stats = await this.getDatabaseStats(dbPath);
|
|
150
151
|
|
|
151
152
|
this.logSuccess('Database status:');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
terminal.log(` Version: ${stats.version}`);
|
|
154
|
+
terminal.log(` UNII Codes: ${stats.uniiCount.toLocaleString()}`);
|
|
155
|
+
terminal.log(` Descriptions: ${stats.descCount.toLocaleString()}`);
|
|
156
|
+
terminal.log(` Database Size: ${stats.sizeGB.toFixed(2)} GB`);
|
|
157
|
+
terminal.log(` Last Modified: ${stats.lastModified}`);
|
|
157
158
|
|
|
158
159
|
} catch (error) {
|
|
159
160
|
this.logError(`Status check failed: ${error.message}`);
|
|
@@ -304,7 +305,7 @@ class UniiModule extends BaseTerminologyModule {
|
|
|
304
305
|
db.serialize(() => {
|
|
305
306
|
indexes.forEach(sql => {
|
|
306
307
|
db.run(sql, (err) => {
|
|
307
|
-
if (err)
|
|
308
|
+
if (err) terminal.warn(`Index creation warning: ${err.message}`);
|
|
308
309
|
});
|
|
309
310
|
});
|
|
310
311
|
});
|
|
@@ -327,7 +328,7 @@ class UniiDataMigrator {
|
|
|
327
328
|
* @returns {Promise<void>}
|
|
328
329
|
*/
|
|
329
330
|
async migrate(sourceFile, destFile, version = 'unknown', verbose = true) {
|
|
330
|
-
if (verbose)
|
|
331
|
+
if (verbose) terminal.log('Starting UNII data migration...');
|
|
331
332
|
|
|
332
333
|
// Ensure destination directory exists
|
|
333
334
|
const destDir = path.dirname(destFile);
|
|
@@ -350,7 +351,7 @@ class UniiDataMigrator {
|
|
|
350
351
|
// Process source file
|
|
351
352
|
await this.#processSourceFile(db, sourceFile, verbose);
|
|
352
353
|
|
|
353
|
-
if (verbose)
|
|
354
|
+
if (verbose) terminal.log('UNII data migration completed successfully');
|
|
354
355
|
} finally {
|
|
355
356
|
await this.#closeDatabase(db, verbose);
|
|
356
357
|
}
|
|
@@ -361,7 +362,7 @@ class UniiDataMigrator {
|
|
|
361
362
|
* @private
|
|
362
363
|
*/
|
|
363
364
|
async #createTables(db, version, verbose = true) {
|
|
364
|
-
if (verbose)
|
|
365
|
+
if (verbose) terminal.log('Creating database tables...');
|
|
365
366
|
|
|
366
367
|
return new Promise((resolve, reject) => {
|
|
367
368
|
db.serialize(() => {
|
|
@@ -400,7 +401,7 @@ class UniiDataMigrator {
|
|
|
400
401
|
// Insert version
|
|
401
402
|
db.run('INSERT INTO UniiVersion (Version) VALUES (?)', [version], (err) => {
|
|
402
403
|
if (err) return reject(err);
|
|
403
|
-
if (verbose)
|
|
404
|
+
if (verbose) terminal.log('Database tables created');
|
|
404
405
|
resolve();
|
|
405
406
|
});
|
|
406
407
|
});
|
|
@@ -412,7 +413,7 @@ class UniiDataMigrator {
|
|
|
412
413
|
* @private
|
|
413
414
|
*/
|
|
414
415
|
async #processSourceFile(db, sourceFile, verbose = true) {
|
|
415
|
-
if (verbose)
|
|
416
|
+
if (verbose) terminal.log('Processing source file:', sourceFile);
|
|
416
417
|
|
|
417
418
|
if (!fs.existsSync(sourceFile)) {
|
|
418
419
|
throw new Error(`Source file not found: ${sourceFile}`);
|
|
@@ -438,7 +439,7 @@ class UniiDataMigrator {
|
|
|
438
439
|
throw new Error('Source file is empty');
|
|
439
440
|
}
|
|
440
441
|
|
|
441
|
-
if (verbose)
|
|
442
|
+
if (verbose) terminal.log(`Read ${lines.length} lines, processing...`);
|
|
442
443
|
|
|
443
444
|
// Track processed codes and auto-increment keys
|
|
444
445
|
const codeMap = new Map(); // code -> UniiKey
|
|
@@ -467,7 +468,7 @@ class UniiDataMigrator {
|
|
|
467
468
|
|
|
468
469
|
// Need at least 3 columns (Display, Type, Code)
|
|
469
470
|
if (cols.length < 3) {
|
|
470
|
-
if (verbose)
|
|
471
|
+
if (verbose) terminal.warn(`Skipping line ${i + 1}: insufficient columns`);
|
|
471
472
|
continue;
|
|
472
473
|
}
|
|
473
474
|
|
|
@@ -476,7 +477,7 @@ class UniiDataMigrator {
|
|
|
476
477
|
const code = cols[2] || '';
|
|
477
478
|
|
|
478
479
|
if (!code) {
|
|
479
|
-
if (verbose)
|
|
480
|
+
if (verbose) terminal.warn(`Skipping line ${i + 1}: empty UNII code`);
|
|
480
481
|
continue;
|
|
481
482
|
}
|
|
482
483
|
|
|
@@ -510,14 +511,14 @@ class UniiDataMigrator {
|
|
|
510
511
|
});
|
|
511
512
|
|
|
512
513
|
if (processedLines % 10000 === 0) {
|
|
513
|
-
|
|
514
|
+
terminal.log(`Processed ${processedLines} lines...`);
|
|
514
515
|
}
|
|
515
516
|
}
|
|
516
517
|
|
|
517
518
|
if (verbose) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
519
|
+
terminal.log(`Processing completed. Total lines processed: ${processedLines}`);
|
|
520
|
+
terminal.log(`Unique UNII codes: ${codeMap.size}`);
|
|
521
|
+
terminal.log(`Total descriptions: ${lastUniiDescKey}`);
|
|
521
522
|
}
|
|
522
523
|
}
|
|
523
524
|
|
|
@@ -529,7 +530,7 @@ class UniiDataMigrator {
|
|
|
529
530
|
return new Promise((resolve) => {
|
|
530
531
|
db.close((err) => {
|
|
531
532
|
if (err && verbose) {
|
|
532
|
-
|
|
533
|
+
terminal.error('Error closing database:', err);
|
|
533
534
|
}
|
|
534
535
|
resolve();
|
|
535
536
|
});
|
|
@@ -558,8 +559,8 @@ class UniiDataMigratorWithProgress {
|
|
|
558
559
|
let processedLines = 0;
|
|
559
560
|
|
|
560
561
|
// Override the progress reporting in the original migrator
|
|
561
|
-
const
|
|
562
|
-
|
|
562
|
+
const originalLog = terminal.log;
|
|
563
|
+
terminal.log = (...args) => {
|
|
563
564
|
const message = args.join(' ');
|
|
564
565
|
if (message.includes('Processed') && message.includes('lines')) {
|
|
565
566
|
const match = message.match(/Processed (\d+) lines/);
|
|
@@ -568,13 +569,13 @@ class UniiDataMigratorWithProgress {
|
|
|
568
569
|
this.module.updateProgress(processedLines);
|
|
569
570
|
}
|
|
570
571
|
}
|
|
571
|
-
if (verbose)
|
|
572
|
+
if (verbose) originalLog(...args);
|
|
572
573
|
};
|
|
573
574
|
|
|
574
575
|
try {
|
|
575
576
|
await originalMigrator.migrate(sourceFile, destFile, version, verbose);
|
|
576
577
|
} finally {
|
|
577
|
-
|
|
578
|
+
terminal.log = originalLog;
|
|
578
579
|
this.module.stopProgress();
|
|
579
580
|
}
|
|
580
581
|
}
|