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
package/tx/cs/cs-db.js
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const { CodeSystem } = require('../library/codesystem');
|
|
4
|
+
const { Language, Languages} = require('../../library/languages');
|
|
5
|
+
const { CodeSystemProvider, CodeSystemFactoryProvider} = require('./cs-api');
|
|
6
|
+
const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SQL Tables:
|
|
10
|
+
*
|
|
11
|
+
* Metadata: Name, Value
|
|
12
|
+
* Concepts: ConceptKey, Code, Display, Definition, Parent
|
|
13
|
+
* Closure: ParentKey, ChildKey
|
|
14
|
+
* DesignationUses: DesignationUseKey, System, Version, Code, Display
|
|
15
|
+
* Languages: LanguageKey, LanguageCode
|
|
16
|
+
* Designations: ConceptKey, LanguageKey, DesignationUseKey, Value
|
|
17
|
+
* PropertyDefinitions: PropertyDefinitionKey, Code, Uri, Description, Type
|
|
18
|
+
* Properties: PropertyKey, PropertyDefinitionKey, Value, System, Version, Display
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
class CachedDesignation {
|
|
22
|
+
constructor(display, language, use) {
|
|
23
|
+
this.display = display;
|
|
24
|
+
this.language = language;
|
|
25
|
+
this.use = use;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class CodeDBProviderContext {
|
|
30
|
+
constructor(key, code, display, definition, status) {
|
|
31
|
+
this.key = key;
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.display = display;
|
|
34
|
+
this.definition = definition;
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.designations = null; // Array of CachedDesignation
|
|
37
|
+
this.children = null; // Will be Set of keys if this has children
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
addChild(key) {
|
|
41
|
+
if (!this.children) {
|
|
42
|
+
this.children = new Set();
|
|
43
|
+
}
|
|
44
|
+
this.children.add(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CodeDBIteratorContext {
|
|
50
|
+
constructor(context, keys) {
|
|
51
|
+
this.context = context;
|
|
52
|
+
this.keys = keys || [];
|
|
53
|
+
this.current = 0;
|
|
54
|
+
this.total = this.keys.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
more() {
|
|
58
|
+
return this.current < this.total;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
next() {
|
|
62
|
+
this.current++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class CodeDBFilterHolder {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.keys = [];
|
|
69
|
+
this.cursor = 0;
|
|
70
|
+
this.lsql = '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hasKey(key) {
|
|
74
|
+
// Binary search since keys are sorted
|
|
75
|
+
let l = 0;
|
|
76
|
+
let r = this.keys.length - 1;
|
|
77
|
+
while (l <= r) {
|
|
78
|
+
const m = Math.floor((l + r) / 2);
|
|
79
|
+
if (this.keys[m] < key) {
|
|
80
|
+
l = m + 1;
|
|
81
|
+
} else if (this.keys[m] > key) {
|
|
82
|
+
r = m - 1;
|
|
83
|
+
} else {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class CodeDBPrep {
|
|
92
|
+
constructor() {
|
|
93
|
+
this.filters = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class CodeDBServices extends CodeSystemProvider {
|
|
98
|
+
constructor(opContext, supplements, db, sharedData) {
|
|
99
|
+
super(opContext, supplements);
|
|
100
|
+
this.db = db;
|
|
101
|
+
|
|
102
|
+
// Shared data from factory
|
|
103
|
+
this.langs = sharedData.langs;
|
|
104
|
+
this.codes = sharedData.codes;
|
|
105
|
+
this.codeList = sharedData.codeList;
|
|
106
|
+
this.codeSystem = sharedData.codeSystem;
|
|
107
|
+
this.root = sharedData.root;
|
|
108
|
+
this.firstCodeKey = sharedData.firstCodeKey;
|
|
109
|
+
this.relationships = sharedData.relationships;
|
|
110
|
+
this.propertyList = sharedData.propertyList;
|
|
111
|
+
this.statusKeys = sharedData.statusKeys;
|
|
112
|
+
this.statusCodes = sharedData.statusCodes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
close() {
|
|
116
|
+
if (this.db) {
|
|
117
|
+
this.db.close();
|
|
118
|
+
this.db = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Metadata methods
|
|
123
|
+
system() {
|
|
124
|
+
return this.codeSystem.url;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
version() {
|
|
128
|
+
return this.codeSystem.version;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
name() {
|
|
132
|
+
return this.codeSystem.name;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
description() {
|
|
136
|
+
return this.codeSystem.description;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async totalCount() {
|
|
140
|
+
return this.codes.size;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
hasParents() {
|
|
144
|
+
return this.codeSystem.hierarchical;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
hasAnyDisplays(languages) {
|
|
148
|
+
const langs = this._ensureLanguages(languages);
|
|
149
|
+
|
|
150
|
+
// Check supplements first
|
|
151
|
+
if (this._hasAnySupplementDisplays(langs)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if any requested languages are available in code system data
|
|
156
|
+
for (const requestedLang of langs.languages) {
|
|
157
|
+
for (const [codeDBLangCode] of this.langs) {
|
|
158
|
+
const codeDBLang = new Language(codeDBLangCode);
|
|
159
|
+
if (codeDBLang.matchesForDisplay(requestedLang)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return super.hasAnyDisplays(langs);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Core concept methods
|
|
169
|
+
async code(context) {
|
|
170
|
+
|
|
171
|
+
const ctxt = await this.#ensureContext(context);
|
|
172
|
+
return ctxt ? ctxt.code : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async display(context) {
|
|
176
|
+
|
|
177
|
+
const ctxt = await this.#ensureContext(context);
|
|
178
|
+
if (!ctxt) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check supplements first
|
|
183
|
+
let disp = this._displayFromSupplements(ctxt.code);
|
|
184
|
+
if (disp) {
|
|
185
|
+
return disp;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Use language-aware display logic
|
|
189
|
+
if (this.opContext.langs && !this.opContext.langs.isEnglishOrNothing()) {
|
|
190
|
+
await this.#loadDesignationsForContext(ctxt);
|
|
191
|
+
|
|
192
|
+
// Try to find exact language match
|
|
193
|
+
for (const lang of this.opContext.langs.langs) {
|
|
194
|
+
for (const display of ctxt.designations) {
|
|
195
|
+
if (lang.matches(display.language, true)) {
|
|
196
|
+
return display.value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Try partial language match
|
|
202
|
+
for (const lang of this.opContext.langs.langs) {
|
|
203
|
+
for (const display of ctxt.designations) {
|
|
204
|
+
if (lang.matches(display.language, false)) {
|
|
205
|
+
return display.value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return ctxt.display || '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async definition(context) {
|
|
215
|
+
const ctxt = await this.#ensureContext(context);
|
|
216
|
+
return ctxt.definition;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async isAbstract(context) {
|
|
220
|
+
const ctxt = await this.#ensureContext(context);
|
|
221
|
+
return ctxt.abstract;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async isInactive(context) {
|
|
225
|
+
const ctxt = await this.#ensureContext(context);
|
|
226
|
+
return ctxt.status == 'inactive';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async getStatus(context) {
|
|
230
|
+
const ctxt = await this.#ensureContext(context);
|
|
231
|
+
return ctxt.status;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async isDeprecated(context) {
|
|
235
|
+
const ctxt = await this.#ensureContext(context);
|
|
236
|
+
return ctxt.status == 'deprecated';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async designations(context, displays) {
|
|
240
|
+
const ctxt = await this.#ensureContext(context);
|
|
241
|
+
if (ctxt) {
|
|
242
|
+
await this.#loadDesignationsForContext(ctxt);
|
|
243
|
+
ctxt.designations
|
|
244
|
+
// Add main display
|
|
245
|
+
displays.addDesignation(true, 'active', this.codeSystem.language, CodeSystem.makeUseForDisplay(), ctxt.desc.trim());
|
|
246
|
+
|
|
247
|
+
// Add cached designations
|
|
248
|
+
if (ctxt.displays.length === 0) {
|
|
249
|
+
await this.#loadDesignationsForContext(ctxt);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const entry of ctxt.designations) {
|
|
253
|
+
let use = undefined;
|
|
254
|
+
if (entry.type) {
|
|
255
|
+
use = {
|
|
256
|
+
system: this.codeSystem.url,
|
|
257
|
+
code: entry.type
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!use) {
|
|
261
|
+
use = entry.display ? CodeSystem.makeUseForDisplay() : null;
|
|
262
|
+
}
|
|
263
|
+
displays.addDesignation(false, 'active', entry.lang, use, entry.value.trim());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Add supplement designations
|
|
267
|
+
this._listSupplementDesignations(ctxt.code, displays);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async extendLookup(ctxt, props, params) {
|
|
273
|
+
validateArrayParameter(props, 'props', String);
|
|
274
|
+
validateArrayParameter(params, 'params', Object);
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if (typeof ctxt === 'string') {
|
|
278
|
+
const located = await this.locate(ctxt);
|
|
279
|
+
if (!located.context) {
|
|
280
|
+
throw new Error(located.message);
|
|
281
|
+
}
|
|
282
|
+
ctxt = located.context;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!(ctxt instanceof CodeDBProviderContext)) {
|
|
286
|
+
throw new Error('Invalid context for CodeDB lookup');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await this.#addConceptProperties(ctxt, params);
|
|
290
|
+
await this.#addStatusProperty(ctxt, params);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async #addConceptProperties(ctxt, params) {
|
|
294
|
+
return new Promise((resolve, reject) => {
|
|
295
|
+
const sql = `
|
|
296
|
+
SELECT PropertyTypes.Description, PropertyValues.Value
|
|
297
|
+
FROM Properties, PropertyTypes, PropertyValues
|
|
298
|
+
WHERE Properties.CodeKey = ?
|
|
299
|
+
AND Properties.PropertyTypeKey = PropertyTypes.PropertyTypeKey
|
|
300
|
+
AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
304
|
+
if (err) {
|
|
305
|
+
reject(err);
|
|
306
|
+
} else {
|
|
307
|
+
for (const row of rows) {
|
|
308
|
+
this.#addStringProperty(params, 'property', row.Description, row.Value);
|
|
309
|
+
}
|
|
310
|
+
resolve();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async #addStatusProperty(ctxt, params) {
|
|
317
|
+
if (ctxt.status) {
|
|
318
|
+
this.#addStringProperty(params, 'property', 'STATUS', ctxt.status);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#addProperty(params, type, name, value, language = null) {
|
|
323
|
+
|
|
324
|
+
const property = {
|
|
325
|
+
name: type,
|
|
326
|
+
part: [
|
|
327
|
+
{ name: 'code', valueCode: name },
|
|
328
|
+
{ name: 'value', valueString: value }
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if (language) {
|
|
333
|
+
property.part.push({ name: 'language', valueCode: language });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
params.push(property);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#addCodeProperty(params, type, name, value, language = null) {
|
|
340
|
+
|
|
341
|
+
const property = {
|
|
342
|
+
name: type,
|
|
343
|
+
part: [
|
|
344
|
+
{ name: 'code', valueCode: name },
|
|
345
|
+
{ name: 'value', valueCode: value }
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (language) {
|
|
350
|
+
property.part.push({ name: 'language', valueCode: language });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
params.push(property);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#addStringProperty(params, type, name, value, language = null) {
|
|
357
|
+
|
|
358
|
+
const property = {
|
|
359
|
+
name: type,
|
|
360
|
+
part: [
|
|
361
|
+
{ name: 'code', valueCode: name },
|
|
362
|
+
{ name: 'value', valueString: value }
|
|
363
|
+
]
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (language) {
|
|
367
|
+
property.part.push({ name: 'language', valueCode: language });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
params.push(property);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#addSupplementDisplays(displays, code) {
|
|
374
|
+
if (this.supplements) {
|
|
375
|
+
for (const supplement of this.supplements) {
|
|
376
|
+
const concept = supplement.getConceptByCode(code);
|
|
377
|
+
if (concept) {
|
|
378
|
+
if (concept.display) {
|
|
379
|
+
displays.push(new LoincDisplay(supplement.jsonObj.language || 'en', concept.display));
|
|
380
|
+
}
|
|
381
|
+
if (concept.designation) {
|
|
382
|
+
for (const designation of concept.designation) {
|
|
383
|
+
const lang = designation.language || supplement.jsonObj.language || 'en';
|
|
384
|
+
displays.push(new LoincDisplay(lang, designation.value));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async #loadDesignationsForContext(ctxt) {
|
|
393
|
+
if (!ctxt.designations) {
|
|
394
|
+
ctxt.designations = [];
|
|
395
|
+
return new Promise((resolve, reject) => {
|
|
396
|
+
const sql = `
|
|
397
|
+
SELECT Languages.Code as Lang, DescriptionTypes.Description as DType, Descriptions.Value
|
|
398
|
+
FROM Descriptions,
|
|
399
|
+
Languages,
|
|
400
|
+
DescriptionTypes
|
|
401
|
+
WHERE Descriptions.CodeKey = ?
|
|
402
|
+
AND Descriptions.DescriptionTypeKey != 4
|
|
403
|
+
AND Descriptions.DescriptionTypeKey = DescriptionTypes.DescriptionTypeKey
|
|
404
|
+
AND Descriptions.LanguageKey = Languages.LanguageKey
|
|
405
|
+
`;
|
|
406
|
+
|
|
407
|
+
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
408
|
+
if (err) {
|
|
409
|
+
reject(err);
|
|
410
|
+
} else {
|
|
411
|
+
for (const row of rows) {
|
|
412
|
+
const isDisplay = row.DType === 'LONG_COMMON_NAME';
|
|
413
|
+
ctxt.designations.push(new CachedDesignation(row.Value, , row.Lang, row.DType));
|
|
414
|
+
}
|
|
415
|
+
resolve();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async #ensureContext(context) {
|
|
423
|
+
if (!context) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if (typeof context === 'string') {
|
|
427
|
+
const ctxt = await this.locate(context);
|
|
428
|
+
if (!ctxt.context) {
|
|
429
|
+
throw new Error(ctxt.message);
|
|
430
|
+
} else {
|
|
431
|
+
return ctxt.context;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (context instanceof CodeDBProviderContext) {
|
|
435
|
+
return context;
|
|
436
|
+
}
|
|
437
|
+
throw new Error("Unknown Type at #ensureContext: " + (typeof context));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Lookup methods
|
|
441
|
+
async locate(code) {
|
|
442
|
+
|
|
443
|
+
assert(!code || typeof code === 'string', 'code must be string');
|
|
444
|
+
if (!code) return { context: null, message: 'Empty code' };
|
|
445
|
+
|
|
446
|
+
const context = this.codes.get(code);
|
|
447
|
+
if (context) {
|
|
448
|
+
return { context: context, message: null };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { context: null, message: undefined };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Iterator methods
|
|
455
|
+
async iterator(context) {
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if (!context) {
|
|
459
|
+
// Iterate all codes starting from first code
|
|
460
|
+
const keys = Array.from({ length: this.codeList.length - this.firstCodeKey }, (_, i) => i + this.firstCodeKey);
|
|
461
|
+
return new LoincIteratorContext(null, keys);
|
|
462
|
+
} else {
|
|
463
|
+
const ctxt = await this.#ensureContext(context);
|
|
464
|
+
if (ctxt.kind === LoincProviderContextKind.PART && ctxt.children) {
|
|
465
|
+
return new LoincIteratorContext(ctxt, Array.from(ctxt.children));
|
|
466
|
+
} else {
|
|
467
|
+
return new LoincIteratorContext(ctxt, []);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async nextContext(iteratorContext) {
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if (!iteratorContext.more()) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const key = iteratorContext.keys[iteratorContext.current];
|
|
480
|
+
iteratorContext.next();
|
|
481
|
+
|
|
482
|
+
return this.codeList[key];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Filter support
|
|
486
|
+
async doesFilter(prop, op, value) {
|
|
487
|
+
// Relationship filters
|
|
488
|
+
if (this.relationships.has(prop) && ['=', 'in', 'exists', 'regex'].includes(op)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Property filters
|
|
493
|
+
if (this.propertyList.has(prop) && ['=', 'in', 'exists', 'regex'].includes(op)) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Status filter
|
|
498
|
+
if (prop === 'STATUS' && op === '=' && this.statusKeys.has(value)) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// LIST filter
|
|
503
|
+
if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// CLASSSTYPE filter
|
|
508
|
+
if (prop === 'CLASSTYPE' && op === '=' && ["1", "2", "3", "4"].includes(value)) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// answers-for filter
|
|
513
|
+
if (prop === 'answers-for' && op === '=') {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// concept filters
|
|
518
|
+
if (prop === 'concept' && ['is-a', 'descendent-of', '=', 'in', 'not-in'].includes(op)) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// code filters (VSAC workaround)
|
|
523
|
+
if (prop === 'code' && ['is-a', 'descendent-of', '='].includes(op)) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// copyright filter
|
|
528
|
+
if (prop === 'copyright' && op === '=' && ['LOINC', '3rdParty'].includes(value)) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async getPrepContext(iterate) {
|
|
536
|
+
return new LoincPrep(iterate);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async filter(filterContext, prop, op, value) {
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
const filter = new LoincFilterHolder();
|
|
543
|
+
await this.#executeFilterQuery(prop, op, value, filter);
|
|
544
|
+
filterContext.filters.push(filter);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async #executeFilterQuery(prop, op, value, filter) {
|
|
548
|
+
let sql = '';
|
|
549
|
+
let lsql = '';
|
|
550
|
+
|
|
551
|
+
// LIST filter
|
|
552
|
+
if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
|
|
553
|
+
sql = `SELECT TargetKey as Key FROM Relationships
|
|
554
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
555
|
+
AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
556
|
+
ORDER BY SourceKey ASC`;
|
|
557
|
+
lsql = `SELECT COUNT(TargetKey) FROM Relationships
|
|
558
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
559
|
+
AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
560
|
+
AND TargetKey = `;
|
|
561
|
+
}
|
|
562
|
+
// answers-for filter
|
|
563
|
+
else if (prop === 'answers-for' && op === '=') {
|
|
564
|
+
if (value.startsWith('LL')) {
|
|
565
|
+
sql = `SELECT TargetKey as Key FROM Relationships
|
|
566
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
567
|
+
AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
568
|
+
ORDER BY SourceKey ASC`;
|
|
569
|
+
lsql = `SELECT COUNT(TargetKey) FROM Relationships
|
|
570
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
571
|
+
AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
572
|
+
AND TargetKey = `;
|
|
573
|
+
} else {
|
|
574
|
+
sql = `SELECT TargetKey as Key FROM Relationships
|
|
575
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
576
|
+
AND SourceKey IN (
|
|
577
|
+
SELECT SourceKey FROM Relationships
|
|
578
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('answers-for')}
|
|
579
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
580
|
+
)
|
|
581
|
+
ORDER BY SourceKey ASC`;
|
|
582
|
+
lsql = `SELECT COUNT(TargetKey) FROM Relationships
|
|
583
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
584
|
+
AND SourceKey IN (SELECT SourceKey FROM Relationships
|
|
585
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('answers-for')}
|
|
586
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}'))
|
|
587
|
+
AND TargetKey = `;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Relationship equal filter
|
|
591
|
+
else if (this.relationships.has(prop) && op === '=') {
|
|
592
|
+
if (this.codes.has(value)) {
|
|
593
|
+
sql = `SELECT SourceKey as Key FROM Relationships
|
|
594
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
595
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
596
|
+
ORDER BY SourceKey ASC`;
|
|
597
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
598
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
599
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
600
|
+
AND SourceKey = `;
|
|
601
|
+
} else {
|
|
602
|
+
sql = `SELECT SourceKey as Key FROM Relationships
|
|
603
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
604
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
605
|
+
ORDER BY SourceKey ASC`;
|
|
606
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
607
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
608
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
609
|
+
AND SourceKey = `;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Relationship 'in' filter
|
|
613
|
+
else if (this.relationships.has(prop) && op === 'in') {
|
|
614
|
+
const codes = this.#commaListOfCodes(value);
|
|
615
|
+
sql = `SELECT SourceKey as Key FROM Relationships
|
|
616
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
617
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
|
|
618
|
+
ORDER BY SourceKey ASC`;
|
|
619
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
620
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
621
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
|
|
622
|
+
AND SourceKey = `;
|
|
623
|
+
}
|
|
624
|
+
// Relationship 'exists' filter
|
|
625
|
+
else if (this.relationships.has(prop) && op === 'exists') {
|
|
626
|
+
if (this.codes.has(value)) {
|
|
627
|
+
sql = `SELECT SourceKey as Key FROM Relationships
|
|
628
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
629
|
+
AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
630
|
+
ORDER BY SourceKey ASC`;
|
|
631
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
632
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
633
|
+
AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
634
|
+
AND SourceKey = `;
|
|
635
|
+
} else {
|
|
636
|
+
sql = `SELECT SourceKey as Key FROM Relationships
|
|
637
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
638
|
+
AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
639
|
+
ORDER BY SourceKey ASC`;
|
|
640
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
641
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
642
|
+
AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
643
|
+
AND SourceKey = `;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Relationship regex filter
|
|
647
|
+
else if (this.relationships.has(prop) && op === 'regex') {
|
|
648
|
+
const matchingKeys = await this.#findRegexMatches(
|
|
649
|
+
`SELECT CodeKey as Key, Description FROM Codes
|
|
650
|
+
WHERE CodeKey IN (SELECT TargetKey FROM Relationships WHERE RelationshipTypeKey = ${this.relationships.get(prop)})`,
|
|
651
|
+
value,
|
|
652
|
+
'Description'
|
|
653
|
+
);
|
|
654
|
+
if (matchingKeys.length > 0) {
|
|
655
|
+
sql = `SELECT SourceKey as Key FROM Relationships
|
|
656
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
657
|
+
AND TargetKey IN (${matchingKeys.join(',')})
|
|
658
|
+
ORDER BY SourceKey ASC`;
|
|
659
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
660
|
+
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
661
|
+
AND TargetKey IN (${matchingKeys.join(',')})
|
|
662
|
+
AND SourceKey = `;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Property equal filter (with CLASSTYPE handling)
|
|
666
|
+
else if (this.propertyList.has(prop) && op === '=') {
|
|
667
|
+
let actualValue = value;
|
|
668
|
+
if (prop === 'CLASSTYPE' && ['1', '2', '3', '4'].includes(value)) {
|
|
669
|
+
const classTypes = {
|
|
670
|
+
'1': 'Laboratory class',
|
|
671
|
+
'2': 'Clinical class',
|
|
672
|
+
'3': 'Claims attachments',
|
|
673
|
+
'4': 'Surveys'
|
|
674
|
+
};
|
|
675
|
+
actualValue = classTypes[value];
|
|
676
|
+
}
|
|
677
|
+
sql = `SELECT CodeKey as Key FROM Properties, PropertyValues
|
|
678
|
+
WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
679
|
+
AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
|
|
680
|
+
AND PropertyValues.Value = '${this.#sqlWrapString(actualValue)}' COLLATE NOCASE
|
|
681
|
+
ORDER BY CodeKey ASC`;
|
|
682
|
+
lsql = `SELECT COUNT(CodeKey) FROM Properties, PropertyValues
|
|
683
|
+
WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
684
|
+
AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
|
|
685
|
+
AND PropertyValues.Value = '${this.#sqlWrapString(actualValue)}' COLLATE NOCASE
|
|
686
|
+
AND CodeKey = `;
|
|
687
|
+
}
|
|
688
|
+
// Property 'in' filter
|
|
689
|
+
else if (this.propertyList.has(prop) && op === 'in') {
|
|
690
|
+
const codes = this.#commaListOfCodes(value);
|
|
691
|
+
sql = `SELECT CodeKey as Key FROM Properties, PropertyValues
|
|
692
|
+
WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
693
|
+
AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
|
|
694
|
+
AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
|
|
695
|
+
ORDER BY CodeKey ASC`;
|
|
696
|
+
lsql = `SELECT COUNT(CodeKey) FROM Properties, PropertyValues
|
|
697
|
+
WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
698
|
+
AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
|
|
699
|
+
AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
|
|
700
|
+
AND CodeKey = `;
|
|
701
|
+
}
|
|
702
|
+
// Property 'exists' filter
|
|
703
|
+
else if (this.propertyList.has(prop) && op === 'exists') {
|
|
704
|
+
sql = `SELECT DISTINCT CodeKey as Key FROM Properties
|
|
705
|
+
WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
706
|
+
ORDER BY CodeKey ASC`;
|
|
707
|
+
lsql = `SELECT COUNT(CodeKey) FROM Properties
|
|
708
|
+
WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
709
|
+
AND CodeKey = `;
|
|
710
|
+
}
|
|
711
|
+
// Property regex filter
|
|
712
|
+
else if (this.propertyList.has(prop) && op === 'regex') {
|
|
713
|
+
const matchingKeys = await this.#findRegexMatches(
|
|
714
|
+
`SELECT PropertyValueKey, Value FROM PropertyValues
|
|
715
|
+
WHERE PropertyValueKey IN (SELECT PropertyValueKey FROM Properties WHERE PropertyTypeKey = ${this.propertyList.get(prop)})`,
|
|
716
|
+
value,
|
|
717
|
+
'Value',
|
|
718
|
+
'PropertyValueKey'
|
|
719
|
+
);
|
|
720
|
+
if (matchingKeys.length > 0) {
|
|
721
|
+
sql = `SELECT CodeKey as Key FROM Properties
|
|
722
|
+
WHERE PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
723
|
+
AND PropertyValueKey IN (${matchingKeys.join(',')})
|
|
724
|
+
ORDER BY CodeKey ASC`;
|
|
725
|
+
lsql = `SELECT COUNT(CodeKey) FROM Properties
|
|
726
|
+
WHERE PropertyTypeKey = ${this.propertyList.get(prop)}
|
|
727
|
+
AND PropertyValueKey IN (${matchingKeys.join(',')})
|
|
728
|
+
AND CodeKey = `;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Status filter
|
|
732
|
+
else if (prop === 'STATUS' && op === '=' && this.statusKeys.has(value)) {
|
|
733
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
734
|
+
WHERE StatusKey = ${this.statusKeys.get(value)}
|
|
735
|
+
ORDER BY CodeKey ASC`;
|
|
736
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
737
|
+
WHERE StatusKey = ${this.statusKeys.get(value)}
|
|
738
|
+
AND CodeKey = `;
|
|
739
|
+
}
|
|
740
|
+
// Concept hierarchy filters (is-a, descendent-of)
|
|
741
|
+
else if (prop === 'concept' && ['is-a', 'descendent-of'].includes(op)) {
|
|
742
|
+
sql = `SELECT DescendentKey as Key FROM Closure
|
|
743
|
+
WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
744
|
+
ORDER BY DescendentKey ASC`;
|
|
745
|
+
lsql = `SELECT COUNT(DescendentKey) FROM Closure
|
|
746
|
+
WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
747
|
+
AND DescendentKey = `;
|
|
748
|
+
}
|
|
749
|
+
// Concept equal filter (workaround for VSAC misuse)
|
|
750
|
+
else if (prop === 'concept' && op === '=') {
|
|
751
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
752
|
+
WHERE Code = '${this.#sqlWrapString(value)}'
|
|
753
|
+
ORDER BY CodeKey ASC`;
|
|
754
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
755
|
+
WHERE Code = '${this.#sqlWrapString(value)}'
|
|
756
|
+
AND CodeKey = `;
|
|
757
|
+
}
|
|
758
|
+
// Concept 'in' filter (workaround for VSAC misuse)
|
|
759
|
+
else if (prop === 'concept' && op === 'in') {
|
|
760
|
+
const codes = this.#commaListOfCodes(value);
|
|
761
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
762
|
+
WHERE Code IN (${codes})
|
|
763
|
+
ORDER BY CodeKey ASC`;
|
|
764
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
765
|
+
WHERE Code IN (${codes})
|
|
766
|
+
AND CodeKey = `;
|
|
767
|
+
}
|
|
768
|
+
// Code property filters (workaround for VSAC misuse)
|
|
769
|
+
else if (prop === 'code' && ['is-a', 'descendent-of'].includes(op)) {
|
|
770
|
+
sql = `SELECT DescendentKey as Key FROM Closure
|
|
771
|
+
WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
772
|
+
ORDER BY DescendentKey ASC`;
|
|
773
|
+
lsql = `SELECT COUNT(DescendentKey) FROM Closure
|
|
774
|
+
WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
775
|
+
AND DescendentKey = `;
|
|
776
|
+
}
|
|
777
|
+
else if (prop === 'code' && op === '=') {
|
|
778
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
779
|
+
WHERE Code = '${this.#sqlWrapString(value)}'
|
|
780
|
+
ORDER BY CodeKey ASC`;
|
|
781
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
782
|
+
WHERE Code = '${this.#sqlWrapString(value)}'
|
|
783
|
+
AND CodeKey = `;
|
|
784
|
+
}
|
|
785
|
+
// Copyright filters
|
|
786
|
+
else if (prop === 'copyright' && op === '=') {
|
|
787
|
+
if (value === 'LOINC') {
|
|
788
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
789
|
+
WHERE NOT CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
|
|
790
|
+
ORDER BY CodeKey ASC`;
|
|
791
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
792
|
+
WHERE NOT CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
|
|
793
|
+
AND CodeKey = `;
|
|
794
|
+
} else if (value === '3rdParty') {
|
|
795
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
796
|
+
WHERE CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
|
|
797
|
+
ORDER BY CodeKey ASC`;
|
|
798
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
799
|
+
WHERE CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
|
|
800
|
+
AND CodeKey = `;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (sql) {
|
|
805
|
+
await this.#executeSQL(sql, filter);
|
|
806
|
+
filter.lsql = lsql;
|
|
807
|
+
} else {
|
|
808
|
+
throw new Error(`The filter "${prop} ${op} ${value}" is not supported for LOINC`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Helper method for regex matching
|
|
813
|
+
async #findRegexMatches(sql, pattern, valueColumn, keyColumn = 'Key') {
|
|
814
|
+
return new Promise((resolve, reject) => {
|
|
815
|
+
const regex = new RegExp(pattern);
|
|
816
|
+
const matchingKeys = [];
|
|
817
|
+
|
|
818
|
+
this.db.all(sql, (err, rows) => {
|
|
819
|
+
if (err) {
|
|
820
|
+
reject(err);
|
|
821
|
+
} else {
|
|
822
|
+
for (const row of rows) {
|
|
823
|
+
if (regex.test(row[valueColumn])) {
|
|
824
|
+
matchingKeys.push(row[keyColumn]);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
resolve(matchingKeys);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Helper method for comma-separated code lists
|
|
834
|
+
#commaListOfCodes(source) {
|
|
835
|
+
const codes = source.split(',')
|
|
836
|
+
.filter(s => this.codes.has(s.trim()))
|
|
837
|
+
.map(s => `'${this.#sqlWrapString(s.trim())}'`);
|
|
838
|
+
return codes.join(',');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async #executeSQL(sql, filter) {
|
|
842
|
+
return new Promise((resolve, reject) => {
|
|
843
|
+
this.db.all(sql, (err, rows) => {
|
|
844
|
+
if (err) {
|
|
845
|
+
reject(err);
|
|
846
|
+
} else {
|
|
847
|
+
filter.keys = rows.map(row => row.Key).filter(key => key !== 0);
|
|
848
|
+
resolve();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
#sqlWrapString(str) {
|
|
855
|
+
return str.replace(/'/g, "''");
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async executeFilters(filterContext) {
|
|
859
|
+
|
|
860
|
+
return filterContext.filters;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async filterSize(filterContext, set) {
|
|
864
|
+
|
|
865
|
+
return set.keys.length;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async filterMore(filterContext, set) {
|
|
869
|
+
|
|
870
|
+
set.cursor = set.cursor || 0;
|
|
871
|
+
return set.cursor < set.keys.length;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async filterConcept(filterContext, set) {
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
if (set.cursor >= set.keys.length) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const key = set.keys[set.cursor];
|
|
882
|
+
set.cursor++;
|
|
883
|
+
|
|
884
|
+
return this.codeList[key];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async filterLocate(filterContext, set, code) {
|
|
888
|
+
const context = this.codes.get(code);
|
|
889
|
+
if (!context) {
|
|
890
|
+
return `Not a valid code: ${code}`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (!set.lsql) {
|
|
894
|
+
return 'Filter not understood';
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Check if this context's key is in the filter
|
|
898
|
+
if (set.hasKey(context.key)) {
|
|
899
|
+
return context;
|
|
900
|
+
} else {
|
|
901
|
+
return null; // `Code ${code} is not in the specified filter`;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async filterCheck(filterContext, set, concept) {
|
|
906
|
+
if (!(concept instanceof LoincProviderContext)) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return set.hasKey(concept.key);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Search filter - placeholder for text search
|
|
914
|
+
// eslint-disable-next-line no-unused-vars
|
|
915
|
+
async searchFilter(filterContext, filter, sort) {
|
|
916
|
+
|
|
917
|
+
throw new Error('Text search not implemented yet');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Subsumption testing
|
|
921
|
+
async subsumesTest(codeA, codeB) {
|
|
922
|
+
await this.#ensureContext(codeA);
|
|
923
|
+
await this.#ensureContext(codeB);
|
|
924
|
+
|
|
925
|
+
return 'not-subsumed'; // Not implemented yet
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
versionAlgorithm() {
|
|
929
|
+
return 'natural';
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
isDisplay(designation) {
|
|
933
|
+
return designation.use.code == "SHORTNAME" || designation.use.code == "LONG_COMMON_NAME" || designation.use.code == "LinguisticVariantDisplayName";
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
938
|
+
constructor(i18n, dbPath) {
|
|
939
|
+
super(i18n);
|
|
940
|
+
this.dbPath = dbPath;
|
|
941
|
+
this.uses = 0;
|
|
942
|
+
this._loaded = false;
|
|
943
|
+
this._sharedData = null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
system() {
|
|
947
|
+
return 'http://loinc.org';
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
version() {
|
|
951
|
+
return this._sharedData._version;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
name() {
|
|
955
|
+
return 'LOINC';
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async #ensureLoaded() {
|
|
959
|
+
if (!this._loaded) {
|
|
960
|
+
await this.load();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async load() {
|
|
965
|
+
const db = new sqlite3.Database(this.dbPath);
|
|
966
|
+
|
|
967
|
+
// Enable performance optimizations
|
|
968
|
+
await this.#optimizeDatabase(db);
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
this._sharedData = {
|
|
972
|
+
langs: new Map(),
|
|
973
|
+
codes: new Map(),
|
|
974
|
+
codeList: [null],
|
|
975
|
+
relationships: new Map(),
|
|
976
|
+
propertyList: new Map(),
|
|
977
|
+
statusKeys: new Map(),
|
|
978
|
+
statusCodes: new Map(),
|
|
979
|
+
_version: '',
|
|
980
|
+
root: '',
|
|
981
|
+
firstCodeKey: 0
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// Load small lookup tables in parallel
|
|
985
|
+
// eslint-disable-next-line no-unused-vars
|
|
986
|
+
const [langs, statusCodes, relationships, propertyList, config] = await Promise.all([
|
|
987
|
+
this.#loadLanguages(db),
|
|
988
|
+
this.#loadStatusCodes(db),
|
|
989
|
+
this.#loadRelationshipTypes(db),
|
|
990
|
+
this.#loadPropertyTypes(db),
|
|
991
|
+
this.#loadConfig(db)
|
|
992
|
+
]);
|
|
993
|
+
|
|
994
|
+
// Load codes (largest operation)
|
|
995
|
+
await this.#loadCodes(db);
|
|
996
|
+
|
|
997
|
+
// Load dependent data in parallel
|
|
998
|
+
await Promise.all([
|
|
999
|
+
// this.#loadDesignationsCache(db),
|
|
1000
|
+
this.#loadHierarchy(db)
|
|
1001
|
+
]);
|
|
1002
|
+
|
|
1003
|
+
} finally {
|
|
1004
|
+
db.close();
|
|
1005
|
+
}
|
|
1006
|
+
this._loaded = true;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async #optimizeDatabase(db) {
|
|
1010
|
+
return new Promise((resolve) => {
|
|
1011
|
+
db.serialize(() => {
|
|
1012
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
1013
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
1014
|
+
db.run('PRAGMA cache_size = 10000');
|
|
1015
|
+
db.run('PRAGMA temp_store = MEMORY');
|
|
1016
|
+
db.run('PRAGMA mmap_size = 268435456'); // 256MB
|
|
1017
|
+
resolve();
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async #loadLanguages(db) {
|
|
1023
|
+
return new Promise((resolve, reject) => {
|
|
1024
|
+
db.all('SELECT LanguageKey, Code FROM Languages', (err, rows) => {
|
|
1025
|
+
if (err) {
|
|
1026
|
+
reject(err);
|
|
1027
|
+
} else {
|
|
1028
|
+
for (const row of rows) {
|
|
1029
|
+
this._sharedData.langs.set(row.Code, row.LanguageKey);
|
|
1030
|
+
}
|
|
1031
|
+
resolve();
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async #loadStatusCodes(db) {
|
|
1038
|
+
return new Promise((resolve, reject) => {
|
|
1039
|
+
db.all('SELECT StatusKey, Description FROM StatusCodes', (err, rows) => {
|
|
1040
|
+
if (err) {
|
|
1041
|
+
reject(err);
|
|
1042
|
+
} else {
|
|
1043
|
+
for (const row of rows) {
|
|
1044
|
+
this._sharedData.statusKeys.set(row.Description, row.StatusKey.toString());
|
|
1045
|
+
this._sharedData.statusCodes.set(row.StatusKey.toString(), row.Description);
|
|
1046
|
+
}
|
|
1047
|
+
resolve();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async #loadRelationshipTypes(db) {
|
|
1054
|
+
return new Promise((resolve, reject) => {
|
|
1055
|
+
db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', (err, rows) => {
|
|
1056
|
+
if (err) {
|
|
1057
|
+
reject(err);
|
|
1058
|
+
} else {
|
|
1059
|
+
for (const row of rows) {
|
|
1060
|
+
this._sharedData.relationships.set(row.Description, row.RelationshipTypeKey.toString());
|
|
1061
|
+
}
|
|
1062
|
+
resolve();
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
async #loadPropertyTypes(db) {
|
|
1069
|
+
return new Promise((resolve, reject) => {
|
|
1070
|
+
db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', (err, rows) => {
|
|
1071
|
+
if (err) {
|
|
1072
|
+
reject(err);
|
|
1073
|
+
} else {
|
|
1074
|
+
for (const row of rows) {
|
|
1075
|
+
this._sharedData.propertyList.set(row.Description, row.PropertyTypeKey.toString());
|
|
1076
|
+
}
|
|
1077
|
+
resolve();
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async #loadCodes(db) {
|
|
1084
|
+
return new Promise((resolve, reject) => {
|
|
1085
|
+
// First get the count to pre-allocate array
|
|
1086
|
+
db.get('SELECT MAX(CodeKey) as maxKey FROM Codes', (err, row) => {
|
|
1087
|
+
if (err) return reject(err);
|
|
1088
|
+
|
|
1089
|
+
// Pre-allocate the array to avoid repeated resizing
|
|
1090
|
+
const maxKey = row.maxKey || 0;
|
|
1091
|
+
this._sharedData.codeList = new Array(maxKey + 1).fill(null);
|
|
1092
|
+
|
|
1093
|
+
// Now load all codes
|
|
1094
|
+
db.all('SELECT CodeKey, Code, Type, Codes.Description, StatusCodes.Description as Status FROM Codes, StatusCodes where StatusCodes.StatusKey = Codes.StatusKey order by CodeKey Asc', (err, rows) => {
|
|
1095
|
+
if (err) return reject(err);
|
|
1096
|
+
|
|
1097
|
+
// Batch process rows
|
|
1098
|
+
for (const row of rows) {
|
|
1099
|
+
const context = new LoincProviderContext(
|
|
1100
|
+
row.CodeKey,
|
|
1101
|
+
row.Type - 1,
|
|
1102
|
+
row.Code,
|
|
1103
|
+
row.Description,
|
|
1104
|
+
row.Status
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
this._sharedData.codes.set(row.Code, context);
|
|
1108
|
+
this._sharedData.codeList[row.CodeKey] = context;
|
|
1109
|
+
|
|
1110
|
+
if (this._sharedData.firstCodeKey === 0 && context.kind === LoincProviderContextKind.CODE) {
|
|
1111
|
+
this._sharedData.firstCodeKey = context.key;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
resolve();
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async #loadDesignationsCache(db) {
|
|
1121
|
+
return new Promise((resolve, reject) => {
|
|
1122
|
+
const sql = `
|
|
1123
|
+
SELECT
|
|
1124
|
+
d.CodeKey,
|
|
1125
|
+
l.Code as Lang,
|
|
1126
|
+
dt.Description as DType,
|
|
1127
|
+
d.Value,
|
|
1128
|
+
dt.Description = 'LONG_COMMON_NAME' as IsDisplay
|
|
1129
|
+
FROM Descriptions d
|
|
1130
|
+
JOIN Languages l ON d.LanguageKey = l.LanguageKey
|
|
1131
|
+
JOIN DescriptionTypes dt ON d.DescriptionTypeKey = dt.DescriptionTypeKey
|
|
1132
|
+
WHERE d.DescriptionTypeKey != 4
|
|
1133
|
+
ORDER BY d.CodeKey
|
|
1134
|
+
`;
|
|
1135
|
+
|
|
1136
|
+
db.all(sql, (err, rows) => {
|
|
1137
|
+
if (err) return reject(err);
|
|
1138
|
+
|
|
1139
|
+
// Batch process by CodeKey to reduce lookups
|
|
1140
|
+
let currentKey = null;
|
|
1141
|
+
let currentContext = null;
|
|
1142
|
+
|
|
1143
|
+
for (const row of rows) {
|
|
1144
|
+
if (row.CodeKey !== currentKey) {
|
|
1145
|
+
currentKey = row.CodeKey;
|
|
1146
|
+
currentContext = this._sharedData.codeList[currentKey];
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (currentContext) {
|
|
1150
|
+
currentContext.displays.push(
|
|
1151
|
+
new DescriptionCacheEntry(row.IsDisplay, row.Lang, row.Value, row.DType)
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
resolve();
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async #loadHierarchy(db) {
|
|
1161
|
+
const childRelKey = this._sharedData.relationships.get('child');
|
|
1162
|
+
if (!childRelKey) {
|
|
1163
|
+
return; // No child relationships defined
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return new Promise((resolve, reject) => {
|
|
1167
|
+
const sql = `
|
|
1168
|
+
SELECT SourceKey, TargetKey FROM Relationships
|
|
1169
|
+
WHERE RelationshipTypeKey = ${childRelKey}
|
|
1170
|
+
`;
|
|
1171
|
+
|
|
1172
|
+
db.all(sql, (err, rows) => {
|
|
1173
|
+
if (err) {
|
|
1174
|
+
reject(err);
|
|
1175
|
+
} else {
|
|
1176
|
+
for (const row of rows) {
|
|
1177
|
+
if (row.SourceKey !== 0 && row.TargetKey !== 0) {
|
|
1178
|
+
const parentContext = this._sharedData.codeList[row.SourceKey];
|
|
1179
|
+
if (parentContext) {
|
|
1180
|
+
parentContext.addChild(row.TargetKey);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
resolve();
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async #loadConfig(db) {
|
|
1191
|
+
return new Promise((resolve, reject) => {
|
|
1192
|
+
db.all('SELECT ConfigKey, Value FROM Config WHERE ConfigKey IN (2, 3)', (err, rows) => {
|
|
1193
|
+
if (err) {
|
|
1194
|
+
reject(err);
|
|
1195
|
+
} else {
|
|
1196
|
+
for (const row of rows) {
|
|
1197
|
+
if (row.ConfigKey === 2) {
|
|
1198
|
+
this._sharedData._version = row.Value;
|
|
1199
|
+
} else if (row.ConfigKey === 3) {
|
|
1200
|
+
this._sharedData.root = row.Value;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
resolve();
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
defaultVersion() {
|
|
1210
|
+
return this._sharedData?._version || 'unknown';
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async build(opContext, supplements) {
|
|
1214
|
+
await this.#ensureLoaded();
|
|
1215
|
+
this.recordUse();
|
|
1216
|
+
|
|
1217
|
+
// Create fresh database connection for this provider instance
|
|
1218
|
+
const db = new sqlite3.Database(this.dbPath);
|
|
1219
|
+
|
|
1220
|
+
return new LoincServices(opContext, supplements, db, this._sharedData);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
useCount() {
|
|
1224
|
+
return this.uses;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
recordUse() {
|
|
1228
|
+
this.uses++;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
async buildKnownValueSet(url, version) {
|
|
1232
|
+
|
|
1233
|
+
if (version && version != this.version()) {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
if (!url.startsWith('http://loinc.org/vs')) {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
if (url == 'http://loinc.org/vs') {
|
|
1240
|
+
// All LOINC codes
|
|
1241
|
+
return {
|
|
1242
|
+
resourceType: 'ValueSet', url: 'http://loinc.org/vs', version: this.version(), status: 'active',
|
|
1243
|
+
name: 'LOINC Value Set - all LOINC codes', description: 'All LOINC codes',
|
|
1244
|
+
date: new Date().toISOString(), experimental: false,
|
|
1245
|
+
compose: { include: [{ system: this.system() }] }
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (url.startsWith('http://loinc.org/vs/')) {
|
|
1250
|
+
const code = url.substring(20);
|
|
1251
|
+
const ci = this._sharedData.codes.get(code);
|
|
1252
|
+
if (!ci) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (ci.kind === LoincProviderContextKind.PART) {
|
|
1257
|
+
// Part-based value set with ancestor filter
|
|
1258
|
+
return {
|
|
1259
|
+
resourceType: 'ValueSet', url: url, version: this.version(), status: 'active',
|
|
1260
|
+
name: 'LOINCValueSetFor' + ci.code.replace(/-/g, '_'), description: 'LOINC value set for code ' + ci.code + ': ' + ci.desc,
|
|
1261
|
+
date: new Date().toISOString(), experimental: false,
|
|
1262
|
+
compose: { include: [{ system: this.system(), filter: [{ property: 'ancestor', op: '=', value: code }] }]
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (ci.kind === LoincProviderContextKind.LIST) {
|
|
1268
|
+
// Answer list - enumerate concepts from database
|
|
1269
|
+
const concepts = await this.#getAnswerListConcepts(ci.key);
|
|
1270
|
+
return {
|
|
1271
|
+
resourceType: 'ValueSet', url: url, version: this.version(), status: 'active',
|
|
1272
|
+
name: 'LOINCAnswerList' + ci.code.replace(/-/g, '_'), description: 'LOINC Answer list for code ' + ci.code + ': ' + ci.desc,
|
|
1273
|
+
date: new Date().toISOString(), experimental: false,
|
|
1274
|
+
compose: { include: [{ system: this.system(), concept: concepts }] }
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Get answer list concepts from database
|
|
1284
|
+
* @param {number} sourceKey - Key of the answer list
|
|
1285
|
+
* @returns {Promise<Array>} Array of {code, display} objects
|
|
1286
|
+
*/
|
|
1287
|
+
async #getAnswerListConcepts(sourceKey) {
|
|
1288
|
+
return new Promise((resolve, reject) => {
|
|
1289
|
+
let db = new sqlite3.Database(this.dbPath);
|
|
1290
|
+
const sql = `
|
|
1291
|
+
SELECT Code, Description
|
|
1292
|
+
FROM Relationships, Codes
|
|
1293
|
+
WHERE SourceKey = ?
|
|
1294
|
+
AND RelationshipTypeKey = 40
|
|
1295
|
+
AND Relationships.TargetKey = Codes.CodeKey
|
|
1296
|
+
`;
|
|
1297
|
+
|
|
1298
|
+
db.all(sql, [sourceKey], (err, rows) => {
|
|
1299
|
+
if (err) {
|
|
1300
|
+
reject(err);
|
|
1301
|
+
} else {
|
|
1302
|
+
const concepts = rows.map(row => ({
|
|
1303
|
+
code: row.Code
|
|
1304
|
+
}));
|
|
1305
|
+
resolve(concepts);
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
id() {
|
|
1312
|
+
return "loinc"+this.version();
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
module.exports = {
|
|
1317
|
+
LoincServices,
|
|
1318
|
+
LoincServicesFactory,
|
|
1319
|
+
LoincProviderContext,
|
|
1320
|
+
LoincProviderContextKind
|
|
1321
|
+
};
|