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-omop.js
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const { CodeSystem } = require('../library/codesystem');
|
|
4
|
+
const { CodeSystemProvider, FilterExecutionContext, CodeSystemFactoryProvider } = require('./cs-api');
|
|
5
|
+
const {validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
|
|
6
|
+
const {ConceptMap} = require("../library/conceptmap");
|
|
7
|
+
|
|
8
|
+
class OMOPConcept {
|
|
9
|
+
constructor(code, display, domain, conceptClass, standard, vocabulary) {
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.display = display;
|
|
12
|
+
this.domain = domain;
|
|
13
|
+
this.conceptClass = conceptClass;
|
|
14
|
+
this.standard = standard || 'NS';
|
|
15
|
+
this.vocabulary = vocabulary;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class OMOPFilter extends FilterExecutionContext {
|
|
20
|
+
constructor(db, sql, value = null) {
|
|
21
|
+
super();
|
|
22
|
+
this.db = db;
|
|
23
|
+
this.sql = sql;
|
|
24
|
+
this.value = value;
|
|
25
|
+
this.rows = [];
|
|
26
|
+
this.cursor = 0;
|
|
27
|
+
this.executed = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async execute(params = []) {
|
|
31
|
+
if (this.executed) return;
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const callback = (err, rows) => {
|
|
35
|
+
if (err) {
|
|
36
|
+
reject(err);
|
|
37
|
+
} else {
|
|
38
|
+
this.rows = rows || [];
|
|
39
|
+
this.executed = true;
|
|
40
|
+
resolve();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (params.length > 0) {
|
|
45
|
+
this.db.all(this.sql, params, callback);
|
|
46
|
+
} else {
|
|
47
|
+
this.db.all(this.sql, callback);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async executeForLocate(params) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
this.db.get(this.sql, params, (err, row) => {
|
|
55
|
+
if (err) {
|
|
56
|
+
reject(err);
|
|
57
|
+
} else {
|
|
58
|
+
resolve(row);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
close() {
|
|
65
|
+
// Database connection is managed by the provider
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class OMOPPrep extends FilterExecutionContext {
|
|
70
|
+
iterate;
|
|
71
|
+
|
|
72
|
+
constructor(iterate) {
|
|
73
|
+
super();
|
|
74
|
+
this.iterate = iterate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Vocabulary mapping functions
|
|
79
|
+
function getVocabId(url) {
|
|
80
|
+
const mapping = {
|
|
81
|
+
'http://hl7.org/fhir/sid/icd-9-cm': 5046,
|
|
82
|
+
'http://snomed.info/sct': 44819097,
|
|
83
|
+
'http://hl7.org/fhir/sid/icd-10-cm': 44819098,
|
|
84
|
+
'http://hl7.org/fhir/sid/icd-9-proc': 44819099,
|
|
85
|
+
'http://www.ama-assn.org/go/cpt': 44819100,
|
|
86
|
+
'http://terminology.hl7.org/CodeSystem/HCPCS-all-codes': 44819101,
|
|
87
|
+
'http://loinc.org': 44819102,
|
|
88
|
+
'http://www.nlm.nih.gov/research/umls/rxnorm': 44819104,
|
|
89
|
+
'http://hl7.org/fhir/sid/ndc': 44819105,
|
|
90
|
+
'http://unitsofmeasure.org': 44819107,
|
|
91
|
+
'http://nucc.org/provider-taxonomy': 44819137,
|
|
92
|
+
'http://www.whocc.no/atc': 44819117
|
|
93
|
+
};
|
|
94
|
+
return mapping[url] || -1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getUri(key) {
|
|
98
|
+
const mapping = {
|
|
99
|
+
5046: 'http://hl7.org/fhir/sid/icd-9-cm',
|
|
100
|
+
44819097: 'http://snomed.info/sct',
|
|
101
|
+
44819098: 'http://hl7.org/fhir/sid/icd-10-cm',
|
|
102
|
+
44819099: 'http://hl7.org/fhir/sid/icd-9-proc',
|
|
103
|
+
44819100: 'http://www.ama-assn.org/go/cpt',
|
|
104
|
+
44819101: 'http://terminology.hl7.org/CodeSystem/HCPCS-all-codes',
|
|
105
|
+
44819102: 'http://loinc.org',
|
|
106
|
+
44819104: 'http://www.nlm.nih.gov/research/umls/rxnorm',
|
|
107
|
+
44819105: 'http://hl7.org/fhir/sid/ndc',
|
|
108
|
+
44819107: 'http://unitsofmeasure.org',
|
|
109
|
+
44819117: 'http://www.whocc.no/atc',
|
|
110
|
+
44819137: 'http://nucc.org/provider-taxonomy'
|
|
111
|
+
};
|
|
112
|
+
return mapping[key] || '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getUriOrError(key) {
|
|
116
|
+
const uri = getUri(key);
|
|
117
|
+
if (!uri) {
|
|
118
|
+
throw new Error(`Unmapped OMOP Vocabulary id: ${key}`);
|
|
119
|
+
}
|
|
120
|
+
return uri;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getLang(langConcept) {
|
|
124
|
+
if (langConcept === 'English language') return 'en';
|
|
125
|
+
if (langConcept === 'Spanish language') return 'es';
|
|
126
|
+
return 'en'; // default
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class OMOPServices extends CodeSystemProvider {
|
|
130
|
+
constructor(opContext, supplements, db, sharedData) {
|
|
131
|
+
super(opContext, supplements);
|
|
132
|
+
this.db = db;
|
|
133
|
+
this._version = sharedData._version;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
close() {
|
|
137
|
+
if (this.db) {
|
|
138
|
+
this.db.close();
|
|
139
|
+
this.db = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Metadata methods
|
|
144
|
+
system() {
|
|
145
|
+
return 'https://fhir-terminology.ohdsi.org';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
version() {
|
|
149
|
+
return this._version;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
name() {
|
|
153
|
+
return `OMOP Concepts`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
description() {
|
|
157
|
+
return `OMOP Concepts, release ${this._version}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async totalCount() {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
this.db.get('SELECT COUNT(*) as count FROM Concepts', (err, row) => {
|
|
163
|
+
if (err) reject(err);
|
|
164
|
+
else resolve(row.count);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Core concept methods
|
|
170
|
+
async code(context) {
|
|
171
|
+
|
|
172
|
+
const ctxt = await this.#ensureContext(context);
|
|
173
|
+
return ctxt ? ctxt.code : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async display(context) {
|
|
177
|
+
|
|
178
|
+
const ctxt = await this.#ensureContext(context);
|
|
179
|
+
|
|
180
|
+
if (!ctxt) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check supplements first
|
|
185
|
+
let disp = this._displayFromSupplements(ctxt.code);
|
|
186
|
+
if (disp) {
|
|
187
|
+
return disp;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return ctxt.display || '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async definition(context) {
|
|
194
|
+
await this.#ensureContext(context);
|
|
195
|
+
return ''; // OMOP doesn't provide definitions
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async isAbstract(context) {
|
|
199
|
+
await this.#ensureContext(context);
|
|
200
|
+
return false; // OMOP concepts are not abstract
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async isInactive(context) {
|
|
204
|
+
await this.#ensureContext(context);
|
|
205
|
+
return false; // Handle via standard_concept if needed
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async isDeprecated(context) {
|
|
209
|
+
await this.#ensureContext(context);
|
|
210
|
+
return false; // Handle via invalid_reason if needed
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async designations(context, displays) {
|
|
214
|
+
|
|
215
|
+
const ctxt = await this.#ensureContext(context);
|
|
216
|
+
|
|
217
|
+
if (ctxt) {
|
|
218
|
+
// Add main display
|
|
219
|
+
displays.addDesignation(true, 'active', 'en', CodeSystem.makeUseForDisplay(), ctxt.display);
|
|
220
|
+
|
|
221
|
+
// Add synonyms
|
|
222
|
+
const synonyms = await this.#getSynonyms(ctxt.code);
|
|
223
|
+
for (const synonym of synonyms) {
|
|
224
|
+
displays.addDesignation(false, 'active', synonym.language, null, synonym.value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add supplement designations
|
|
228
|
+
this._listSupplementDesignations(String(ctxt.code), displays);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async #getSynonyms(code) {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const sql = `
|
|
235
|
+
SELECT concept_synonym_name, concept_name
|
|
236
|
+
FROM ConceptSynonyms, Concepts
|
|
237
|
+
WHERE ConceptSynonyms.language_concept_id = Concepts.concept_id
|
|
238
|
+
AND ConceptSynonyms.concept_id = ?
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
this.db.all(sql, [code], (err, rows) => {
|
|
242
|
+
if (err) {
|
|
243
|
+
reject(err);
|
|
244
|
+
} else {
|
|
245
|
+
const synonyms = rows.map(row => ({
|
|
246
|
+
language: getLang(row.concept_name),
|
|
247
|
+
value: row.concept_synonym_name
|
|
248
|
+
}));
|
|
249
|
+
resolve(synonyms);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async extendLookup(ctxt, props, params) {
|
|
256
|
+
validateArrayParameter(props, 'props', String);
|
|
257
|
+
validateArrayParameter(params, 'params', Object);
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if (typeof ctxt === 'string') {
|
|
261
|
+
const located = await this.locate(ctxt);
|
|
262
|
+
if (!located.context) {
|
|
263
|
+
throw new Error(located.message);
|
|
264
|
+
}
|
|
265
|
+
ctxt = located.context;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!(ctxt instanceof OMOPConcept)) {
|
|
269
|
+
throw new Error('Invalid context for OMOP lookup');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add basic properties
|
|
273
|
+
if (this.#hasProp(props, 'domain-id', true)) {
|
|
274
|
+
this.#addCodeProperty(params, 'property', 'domain-id', ctxt.domain);
|
|
275
|
+
}
|
|
276
|
+
if (this.#hasProp(props, 'concept-class-id', true)) {
|
|
277
|
+
this.#addCodeProperty(params, 'property', 'concept-class-id', ctxt.conceptClass);
|
|
278
|
+
}
|
|
279
|
+
if (this.#hasProp(props, 'standard-concept', true)) {
|
|
280
|
+
this.#addCodeProperty(params, 'property', 'standard-concept', ctxt.standard);
|
|
281
|
+
}
|
|
282
|
+
if (this.#hasProp(props, 'vocabulary-id', true)) {
|
|
283
|
+
this.#addStringProperty(params, 'property', 'vocabulary-id', ctxt.vocabulary);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Add synonyms as designations
|
|
287
|
+
const synonyms = await this.#getSynonyms(ctxt.code);
|
|
288
|
+
for (const synonym of synonyms) {
|
|
289
|
+
this.#addStringProperty(params, 'designation', 'synonym', synonym.value, synonym.language);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Add extended properties from database
|
|
293
|
+
await this.#addExtendedProperties(ctxt, props, params);
|
|
294
|
+
|
|
295
|
+
// Add relationships
|
|
296
|
+
await this.#addRelationships(ctxt, props, params);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async #addExtendedProperties(ctxt, props, params) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const sql = 'SELECT * FROM Concepts WHERE concept_id = ?';
|
|
302
|
+
|
|
303
|
+
this.db.get(sql, [ctxt.code], (err, row) => {
|
|
304
|
+
if (err) {
|
|
305
|
+
reject(err);
|
|
306
|
+
} else if (row) {
|
|
307
|
+
if (this.#hasProp(props, 'concept-class-concept-id', true)) {
|
|
308
|
+
this.#addCodeProperty(params, 'property', 'concept-class-concept-id', row.concept_class_id);
|
|
309
|
+
}
|
|
310
|
+
if (this.#hasProp(props, 'domain-concept-id', true)) {
|
|
311
|
+
this.#addCodeProperty(params, 'property', 'domain-concept-id', row.domain_id);
|
|
312
|
+
}
|
|
313
|
+
if (this.#hasProp(props, 'valid-start-date', true) && row.valid_start_date) {
|
|
314
|
+
this.#addDateProperty(params, 'property', 'valid-start-date', row.valid_start_date);
|
|
315
|
+
}
|
|
316
|
+
if (this.#hasProp(props, 'valid-end-date', true) && row.valid_end_date) {
|
|
317
|
+
this.#addDateProperty(params, 'property', 'valid-end-date', row.valid_end_date);
|
|
318
|
+
}
|
|
319
|
+
if (this.#hasProp(props, 'source-concept-code', true) && row.concept_code && getUri(row.vocabulary_id)) {
|
|
320
|
+
this.#addCodingProperty(params, 'property', 'source-concept-code',
|
|
321
|
+
getUriOrError(row.vocabulary_id), row.concept_code);
|
|
322
|
+
}
|
|
323
|
+
if (this.#hasProp(props, 'vocabulary-concept-id', true)) {
|
|
324
|
+
this.#addCodeProperty(params, 'property', 'vocabulary-concept-id', row.vocabulary_id);
|
|
325
|
+
}
|
|
326
|
+
if (this.#hasProp(props, 'invalid-reason', true) && row.invalid_reason) {
|
|
327
|
+
this.#addStringProperty(params, 'property', 'invalid-reason', row.invalid_reason);
|
|
328
|
+
}
|
|
329
|
+
resolve();
|
|
330
|
+
} else {
|
|
331
|
+
resolve();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async #addRelationships(ctxt, props, params) {
|
|
338
|
+
const seenConcepts = new Set();
|
|
339
|
+
|
|
340
|
+
// Forward relationships
|
|
341
|
+
await new Promise((resolve, reject) => {
|
|
342
|
+
const sql = `
|
|
343
|
+
SELECT Concepts.concept_id, Concepts.concept_name, Relationships.relationship_id
|
|
344
|
+
FROM Concepts, ConceptRelationships, Relationships
|
|
345
|
+
WHERE ConceptRelationships.relationship_id = Relationships.relationship_concept_id
|
|
346
|
+
AND ConceptRelationships.concept_id_2 = Concepts.concept_id
|
|
347
|
+
AND ConceptRelationships.concept_id_1 = ?
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
this.db.all(sql, [ctxt.code], (err, rows) => {
|
|
351
|
+
if (err) {
|
|
352
|
+
reject(err);
|
|
353
|
+
} else {
|
|
354
|
+
for (const row of rows) {
|
|
355
|
+
seenConcepts.add(row.concept_id);
|
|
356
|
+
if (this.#hasProp(props, row.relationship_id, true)) {
|
|
357
|
+
this.#addCodingProperty(params, 'property', row.relationship_id,
|
|
358
|
+
this.system(), row.concept_id, row.concept_name);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
resolve();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Reverse relationships
|
|
367
|
+
await new Promise((resolve, reject) => {
|
|
368
|
+
const sql = `
|
|
369
|
+
SELECT Concepts.concept_id, Concepts.concept_name, Relationships.reverse_relationship_id
|
|
370
|
+
FROM Concepts, ConceptRelationships, Relationships
|
|
371
|
+
WHERE ConceptRelationships.relationship_id = Relationships.relationship_concept_id
|
|
372
|
+
AND ConceptRelationships.concept_id_1 = Concepts.concept_id
|
|
373
|
+
AND ConceptRelationships.concept_id_2 = ?
|
|
374
|
+
`;
|
|
375
|
+
|
|
376
|
+
this.db.all(sql, [ctxt.code], (err, rows) => {
|
|
377
|
+
if (err) {
|
|
378
|
+
reject(err);
|
|
379
|
+
} else {
|
|
380
|
+
for (const row of rows) {
|
|
381
|
+
if (!seenConcepts.has(row.concept_id)) {
|
|
382
|
+
if (this.#hasProp(props, row.reverse_relationship_id, true)) {
|
|
383
|
+
this.#addCodingProperty(params, 'property', row.reverse_relationship_id,
|
|
384
|
+
this.system(), row.concept_id, row.concept_name);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
resolve();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#addStringProperty(params, type, name, value, language = null) {
|
|
395
|
+
const property = {
|
|
396
|
+
name: type,
|
|
397
|
+
part: [
|
|
398
|
+
{ name: 'code', valueCode: name },
|
|
399
|
+
{ name: 'value', valueString: String(value) } // Ensure value is always a string
|
|
400
|
+
]
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (language) {
|
|
404
|
+
property.part.push({ name: 'language', valueCode: language });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
params.push(property);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#addDateProperty(params, type, name, value, language = null) {
|
|
411
|
+
value = String(value);
|
|
412
|
+
if (value && value.length === 8 && !value.includes('-')) {
|
|
413
|
+
value = value.substring(0, 4) + '-' + value.substring(4, 6) + '-' + value.substring(6, 8);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const property = {
|
|
417
|
+
name: type,
|
|
418
|
+
part: [
|
|
419
|
+
{ name: 'code', valueCode: name },
|
|
420
|
+
{ name: 'value', valueDate: value }
|
|
421
|
+
]
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
if (language) {
|
|
425
|
+
property.part.push({ name: 'language', valueCode: language });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
params.push(property);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#addCodingProperty(params, type, name, system, code, display = undefined) {
|
|
432
|
+
const valueCoding = {
|
|
433
|
+
system: system,
|
|
434
|
+
code: String(code)
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
if (display !== undefined) {
|
|
438
|
+
valueCoding.display = display;
|
|
439
|
+
}
|
|
440
|
+
const property = {
|
|
441
|
+
name: type,
|
|
442
|
+
part: [
|
|
443
|
+
{ name: 'code', valueCode: name },
|
|
444
|
+
{ name: 'value', valueCoding: valueCoding }
|
|
445
|
+
]
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
params.push(property);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#addCodeProperty(params, type, name, value, language = null) {
|
|
452
|
+
const property = {
|
|
453
|
+
name: type,
|
|
454
|
+
part: [
|
|
455
|
+
{ name: 'code', valueCode: name },
|
|
456
|
+
{ name: 'value', valueCode: String(value) } // Ensure value is always a string
|
|
457
|
+
]
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (language) {
|
|
461
|
+
property.part.push({ name: 'language', valueCode: language });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
params.push(property);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#hasProp(props, name, defaultValue) {
|
|
468
|
+
if (!props || props.length === 0) return defaultValue;
|
|
469
|
+
return props.includes(name);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async #ensureContext(context) {
|
|
473
|
+
if (!context) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
if (typeof context === 'string') {
|
|
477
|
+
const ctxt = await this.locate(context);
|
|
478
|
+
if (!ctxt.context) {
|
|
479
|
+
throw new Error(ctxt.message ? ctxt.message : `OMOP Concept '${context}' not found`);
|
|
480
|
+
} else {
|
|
481
|
+
return ctxt.context;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (context instanceof OMOPConcept) {
|
|
485
|
+
return context;
|
|
486
|
+
}
|
|
487
|
+
throw new Error("Unknown Type at #ensureContext: " + (typeof context));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Lookup methods
|
|
491
|
+
async locate(code) {
|
|
492
|
+
|
|
493
|
+
assert(!code || typeof code === 'string', 'code must be string');
|
|
494
|
+
if (!code) return { context: null, message: 'Empty code' };
|
|
495
|
+
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
const sql = `
|
|
498
|
+
SELECT concept_id, concept_name, standard_concept,
|
|
499
|
+
Domains.domain_id, ConceptClasses.concept_class_id,
|
|
500
|
+
Vocabularies.vocabulary_id
|
|
501
|
+
FROM Concepts, Domains, ConceptClasses, Vocabularies
|
|
502
|
+
WHERE Concepts.domain_id = Domains.domain_concept_id
|
|
503
|
+
AND ConceptClasses.concept_class_concept_id = Concepts.concept_class_id
|
|
504
|
+
AND Concepts.vocabulary_id = Vocabularies.vocabulary_concept_id
|
|
505
|
+
AND concept_id = ?
|
|
506
|
+
`;
|
|
507
|
+
|
|
508
|
+
this.db.get(sql, [code], (err, row) => {
|
|
509
|
+
if (err) {
|
|
510
|
+
reject(err);
|
|
511
|
+
} else if (row && row.concept_id.toString() === code) {
|
|
512
|
+
const concept = new OMOPConcept(
|
|
513
|
+
code,
|
|
514
|
+
row.concept_name,
|
|
515
|
+
row.domain_id,
|
|
516
|
+
row.concept_class_id,
|
|
517
|
+
row.standard_concept || 'NS',
|
|
518
|
+
row.vocabulary_id
|
|
519
|
+
);
|
|
520
|
+
resolve({ context: concept, message: null });
|
|
521
|
+
} else {
|
|
522
|
+
resolve({ context: null, message: undefined });
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Iterator methods - not supported for OMOP due to size
|
|
529
|
+
async iterator(context) {
|
|
530
|
+
await this.#ensureContext(context);
|
|
531
|
+
throw new Error('getNextContext not supported by OMOP - too large to iterate');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// eslint-disable-next-line no-unused-vars
|
|
535
|
+
async nextContext(iteratorContext) {
|
|
536
|
+
throw new Error('getNextContext not supported by OMOP - too large to iterate');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Filter support
|
|
540
|
+
async doesFilter(prop, op, value) {
|
|
541
|
+
if (prop === 'domain' && op === '=') {
|
|
542
|
+
return value != null;
|
|
543
|
+
}
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async getPrepContext(iterate) {
|
|
548
|
+
return new OMOPPrep(iterate);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async filter(filterContext, prop, op, value) {
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if (prop === 'domain' && op === '=') {
|
|
555
|
+
let sql = `
|
|
556
|
+
SELECT concept_id, concept_name, domain_id
|
|
557
|
+
FROM Concepts
|
|
558
|
+
WHERE standard_concept = 'S'
|
|
559
|
+
AND domain_id IN (
|
|
560
|
+
SELECT domain_concept_id
|
|
561
|
+
FROM Domains
|
|
562
|
+
WHERE domain_id = ?
|
|
563
|
+
)
|
|
564
|
+
`;
|
|
565
|
+
|
|
566
|
+
let filter;
|
|
567
|
+
if (filterContext.iterate) {
|
|
568
|
+
filter = new OMOPFilter(this.db, sql, value);
|
|
569
|
+
await filter.execute([value]);
|
|
570
|
+
} else {
|
|
571
|
+
sql = sql + ' and concept_id = ?';
|
|
572
|
+
filter = new OMOPFilter(this.db, sql, value);
|
|
573
|
+
}
|
|
574
|
+
filterContext.filters.push(filter);
|
|
575
|
+
} else {
|
|
576
|
+
throw new Error(`Filter "${prop} ${op} ${value}" not understood for OMOP`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async executeFilters(filterContext) {
|
|
581
|
+
return filterContext.filters;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async filterSize(filterContext, set) {
|
|
585
|
+
return set.rows.length;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async filterMore(filterContext, set) {
|
|
589
|
+
return set.cursor < set.rows.length;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async filterConcept(filterContext, set) {
|
|
593
|
+
if (set.cursor >= set.rows.length) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const row = set.rows[set.cursor];
|
|
598
|
+
set.cursor++;
|
|
599
|
+
|
|
600
|
+
return new OMOPConcept(
|
|
601
|
+
row.concept_id,
|
|
602
|
+
row.concept_name,
|
|
603
|
+
row.domain_id,
|
|
604
|
+
'', // concept_class not in basic filter query
|
|
605
|
+
'S', // standard_concept is 'S' by filter
|
|
606
|
+
'' // vocabulary not in basic filter query
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async filterLocate(filterContext, set, code) {
|
|
611
|
+
if (filterContext.iterate) {
|
|
612
|
+
return `Filter not configured for locate operations`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const row = await set.executeForLocate([set.value, code]);
|
|
616
|
+
if (row && row.concept_id.toString() === code) {
|
|
617
|
+
return new OMOPConcept(
|
|
618
|
+
String(row.concept_id),
|
|
619
|
+
row.concept_name,
|
|
620
|
+
row.domain_id,
|
|
621
|
+
'',
|
|
622
|
+
'S',
|
|
623
|
+
''
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
return `Code '${code}' is not in the value set`;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async filterCheck(filterContext, set, concept) {
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
if (!(concept instanceof OMOPConcept)) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return set.rows.some(row => row.concept_id.toString() === concept.code);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async filterFinish(filterContext) {
|
|
641
|
+
|
|
642
|
+
for (const filter of filterContext.filters) {
|
|
643
|
+
filter.close();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async filtersNotClosed(filterContext) {
|
|
648
|
+
validateOptionalParameter(filterContext, "filterContext", FilterExecutionContext);
|
|
649
|
+
return false; // OMOP filters are closed
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Search filter - not implemented
|
|
653
|
+
// eslint-disable-next-line no-unused-vars
|
|
654
|
+
async searchFilter(filterContext, filter, sort) {
|
|
655
|
+
|
|
656
|
+
throw new Error('Search filter not implemented yet');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Subsumption testing - not implemented
|
|
660
|
+
async subsumesTest(codeA, codeB) {
|
|
661
|
+
await this.#ensureContext(codeA);
|
|
662
|
+
await this.#ensureContext(codeB);
|
|
663
|
+
|
|
664
|
+
return 'not-subsumed';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Translation support
|
|
668
|
+
async getTranslations(coding, target) {
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
const vocabId = getVocabId(target);
|
|
672
|
+
if (vocabId === -1) {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return new Promise((resolve, reject) => {
|
|
677
|
+
const sql = `
|
|
678
|
+
SELECT concept_code, concept_name
|
|
679
|
+
FROM Concepts
|
|
680
|
+
WHERE concept_id = ? AND vocabulary_id = ?
|
|
681
|
+
`;
|
|
682
|
+
|
|
683
|
+
this.db.all(sql, [coding.code, vocabId], (err, rows) => {
|
|
684
|
+
if (err) {
|
|
685
|
+
reject(err);
|
|
686
|
+
} else {
|
|
687
|
+
const translations = rows.map(row => ({
|
|
688
|
+
uri: target,
|
|
689
|
+
code: row.concept_code,
|
|
690
|
+
display: row.concept_name,
|
|
691
|
+
relationship: 'equivalent',
|
|
692
|
+
map: `${this.system()}/ConceptMap/to-${vocabId}|${this._version}`
|
|
693
|
+
}));
|
|
694
|
+
resolve(translations);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Build value sets for domains
|
|
701
|
+
async buildValueSet(factory, id) {
|
|
702
|
+
const domain = id.substring(44); // Remove prefix
|
|
703
|
+
|
|
704
|
+
return new Promise((resolve, reject) => {
|
|
705
|
+
const sql = `
|
|
706
|
+
SELECT concept_id, concept_name, Domains.domain_id
|
|
707
|
+
FROM Concepts, Domains
|
|
708
|
+
WHERE Domains.domain_id = ?
|
|
709
|
+
AND Domains.domain_concept_id = Concepts.concept_id
|
|
710
|
+
`;
|
|
711
|
+
|
|
712
|
+
this.db.get(sql, [domain], (err, row) => {
|
|
713
|
+
if (err) {
|
|
714
|
+
reject(err);
|
|
715
|
+
} else if (row && row.domain_id === domain) {
|
|
716
|
+
// Create value set structure
|
|
717
|
+
const valueSet = {
|
|
718
|
+
url: id,
|
|
719
|
+
status: 'active',
|
|
720
|
+
version: this._version,
|
|
721
|
+
name: `OMOPDomain${domain}`,
|
|
722
|
+
description: `OMOP value set for domain ${row.concept_name}`,
|
|
723
|
+
date: new Date().toISOString(),
|
|
724
|
+
experimental: false,
|
|
725
|
+
compose: {
|
|
726
|
+
include: [{
|
|
727
|
+
system: this.system(),
|
|
728
|
+
filter: [{
|
|
729
|
+
property: 'domain',
|
|
730
|
+
op: '=',
|
|
731
|
+
value: domain
|
|
732
|
+
}]
|
|
733
|
+
}]
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
resolve(valueSet);
|
|
737
|
+
} else {
|
|
738
|
+
reject(new Error(`Unknown Value Domain ${id}`));
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Register concept maps for vocabularies
|
|
745
|
+
async registerConceptMaps(list) {
|
|
746
|
+
return new Promise((resolve, reject) => {
|
|
747
|
+
const sql = 'SELECT DISTINCT vocabulary_id FROM Concepts';
|
|
748
|
+
|
|
749
|
+
this.db.all(sql, (err, rows) => {
|
|
750
|
+
if (err) {
|
|
751
|
+
reject(err);
|
|
752
|
+
} else {
|
|
753
|
+
for (const row of rows) {
|
|
754
|
+
const key = row.vocabulary_id;
|
|
755
|
+
const uri = getUri(key);
|
|
756
|
+
if (uri) {
|
|
757
|
+
// Create concept maps (simplified structure)
|
|
758
|
+
list.push({
|
|
759
|
+
id: `to-${key}`,
|
|
760
|
+
url: `${this.system()}/ConceptMap/to-${key}`,
|
|
761
|
+
sourceUri: this.system(),
|
|
762
|
+
targetUri: uri
|
|
763
|
+
});
|
|
764
|
+
list.push({
|
|
765
|
+
id: `from-${key}`,
|
|
766
|
+
url: `${this.system()}/ConceptMap/from-${key}`,
|
|
767
|
+
sourceUri: uri,
|
|
768
|
+
targetUri: this.system()
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
resolve();
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
versionAlgorithm() {
|
|
779
|
+
return 'date';
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
class OMOPServicesFactory extends CodeSystemFactoryProvider {
|
|
784
|
+
constructor(i18n, dbPath) {
|
|
785
|
+
super(i18n);
|
|
786
|
+
this.dbPath = dbPath;
|
|
787
|
+
this.uses = 0;
|
|
788
|
+
this._loaded = false;
|
|
789
|
+
this._sharedData = null;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
system() {
|
|
793
|
+
return 'https://fhir-terminology.ohdsi.org';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
version() {
|
|
797
|
+
return this._sharedData._version;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async buildKnownValueSet(url, version) {
|
|
801
|
+
if (!url.startsWith('https://fhir-terminology.ohdsi.org/ValueSet')) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
if (version && version != this.version()) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
if (url == 'https://fhir-terminology.ohdsi.org') {
|
|
808
|
+
return {
|
|
809
|
+
resourceType: 'ValueSet',
|
|
810
|
+
url: url,
|
|
811
|
+
status: 'active',
|
|
812
|
+
version: this.version(),
|
|
813
|
+
name: 'OMOP',
|
|
814
|
+
description: 'OMOP value set',
|
|
815
|
+
date: new Date().toISOString(),
|
|
816
|
+
experimental: false,
|
|
817
|
+
compose: {
|
|
818
|
+
include: [{
|
|
819
|
+
system: this.system()
|
|
820
|
+
}]
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const domain = url.substring(44);
|
|
825
|
+
return {
|
|
826
|
+
resourceType: 'ValueSet',
|
|
827
|
+
url: url,
|
|
828
|
+
status: 'active',
|
|
829
|
+
version: this.version(),
|
|
830
|
+
name: 'OMOPDomain' + domain,
|
|
831
|
+
description: 'OMOP value set for domain ' + domain,
|
|
832
|
+
date: new Date().toISOString(),
|
|
833
|
+
experimental: false,
|
|
834
|
+
compose: {
|
|
835
|
+
include: [{
|
|
836
|
+
system: this.system(),
|
|
837
|
+
filter: [{
|
|
838
|
+
property: 'domain',
|
|
839
|
+
op: '=',
|
|
840
|
+
value: domain
|
|
841
|
+
}]
|
|
842
|
+
}]
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async #ensureLoaded() {
|
|
850
|
+
if (!this._loaded) {
|
|
851
|
+
await this.load();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async load() {
|
|
856
|
+
const db = new sqlite3.Database(this.dbPath);
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
this._sharedData = {
|
|
860
|
+
_version: 'unknown'
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// Load version from OMOP Extension vocabulary
|
|
864
|
+
await this.#loadVersion(db);
|
|
865
|
+
|
|
866
|
+
} finally {
|
|
867
|
+
db.close();
|
|
868
|
+
}
|
|
869
|
+
this._loaded = true;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async #loadVersion(db) {
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
const sql = `
|
|
875
|
+
SELECT vocabulary_version
|
|
876
|
+
FROM Vocabularies
|
|
877
|
+
WHERE vocabulary_id = 'OMOP Extension'
|
|
878
|
+
`;
|
|
879
|
+
|
|
880
|
+
db.get(sql, (err, row) => {
|
|
881
|
+
if (err) {
|
|
882
|
+
reject(err);
|
|
883
|
+
} else if (row) {
|
|
884
|
+
// Extract version number from the end of the version string
|
|
885
|
+
const version = row.vocabulary_version;
|
|
886
|
+
const lastSpaceIndex = version.lastIndexOf(' ');
|
|
887
|
+
this._sharedData._version = lastSpaceIndex !== -1 ?
|
|
888
|
+
version.substring(lastSpaceIndex + 1) : version;
|
|
889
|
+
resolve();
|
|
890
|
+
} else {
|
|
891
|
+
this._sharedData._version = 'unknown';
|
|
892
|
+
resolve();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
defaultVersion() {
|
|
899
|
+
return this._sharedData?._version || 'unknown';
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async build(opContext, supplements) {
|
|
903
|
+
await this.#ensureLoaded();
|
|
904
|
+
this.recordUse();
|
|
905
|
+
|
|
906
|
+
// Create fresh database connection for this provider instance
|
|
907
|
+
const db = new sqlite3.Database(this.dbPath);
|
|
908
|
+
|
|
909
|
+
return new OMOPServices(opContext, supplements, db, this._sharedData);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
static checkDB(dbPath) {
|
|
913
|
+
try {
|
|
914
|
+
const fs = require('fs');
|
|
915
|
+
|
|
916
|
+
// Check if file exists
|
|
917
|
+
if (!fs.existsSync(dbPath)) {
|
|
918
|
+
return 'Database file not found';
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Check file size
|
|
922
|
+
const stats = fs.statSync(dbPath);
|
|
923
|
+
if (stats.size < 1024) {
|
|
924
|
+
return 'Database file too small';
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Try to open database and check for required tables
|
|
928
|
+
const db = new sqlite3.Database(dbPath);
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
// Simple count query to verify database integrity
|
|
932
|
+
db.get('SELECT COUNT(*) as count FROM Concepts', (err) => {
|
|
933
|
+
if (err) {
|
|
934
|
+
db.close();
|
|
935
|
+
return 'Missing Tables - needs re-importing (by java)';
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
db.close();
|
|
940
|
+
return 'OK (check via provider for count)';
|
|
941
|
+
} catch (e) {
|
|
942
|
+
return 'Missing Tables - needs re-importing (by java)';
|
|
943
|
+
}
|
|
944
|
+
} catch (e) {
|
|
945
|
+
return `Database error: ${e.message}`;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* build and return a known concept map from the URL, if there is one.
|
|
952
|
+
*
|
|
953
|
+
* @param url
|
|
954
|
+
* @param version
|
|
955
|
+
* @returns {ConceptMap}
|
|
956
|
+
*/
|
|
957
|
+
async findImplicitConceptMaps(conceptMaps, source, dest) {
|
|
958
|
+
if (source == 'https://fhir-terminology.ohdsi.org') {
|
|
959
|
+
let key = this.#getVocabId(dest);
|
|
960
|
+
if (key) {
|
|
961
|
+
conceptMaps.push(new ConceptMap(this.makeCM(source, dest, key)));
|
|
962
|
+
}
|
|
963
|
+
} else if (dest == 'https://fhir-terminology.ohdsi.org') {
|
|
964
|
+
let key = this.#getVocabId(source);
|
|
965
|
+
if (key) {
|
|
966
|
+
conceptMaps.push(new ConceptMap(this.makeCM(source, dest, key)));
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
// nothing
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// eslint-disable-next-line no-unused-vars
|
|
974
|
+
async findImplicitConceptMap(url, version) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
#getVocabId(url) {
|
|
979
|
+
const vocabMap = {
|
|
980
|
+
'http://hl7.org/fhir/sid/icd-9-cm': 5046,
|
|
981
|
+
'http://snomed.info/sct': 44819097,
|
|
982
|
+
'http://hl7.org/fhir/sid/icd-10-cm': 44819098,
|
|
983
|
+
// 'http://hl7.org/fhir/sid/icd-9-cm': 44819099, // duplicate - using first value
|
|
984
|
+
'http://www.ama-assn.org/go/cpt': 44819100,
|
|
985
|
+
'http://terminology.hl7.org/CodeSystem/HCPCS-all-codes': 44819101,
|
|
986
|
+
'http://loinc.org': 44819102,
|
|
987
|
+
'http://www.nlm.nih.gov/research/umls/rxnorm': 44819104,
|
|
988
|
+
'http://hl7.org/fhir/sid/ndc': 44819105,
|
|
989
|
+
'http://unitsofmeasure.org': 44819107,
|
|
990
|
+
'http://nucc.org/provider-taxonomy': 44819137,
|
|
991
|
+
'http://www.whocc.no/atc': 44819117
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
return vocabMap[url];
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
makeCM(source, dest, key) {
|
|
998
|
+
return {
|
|
999
|
+
resourceType: 'ConceptMap',
|
|
1000
|
+
internalSource: this,
|
|
1001
|
+
url: this.system() + '/ConceptMap/' + key,
|
|
1002
|
+
status: 'active',
|
|
1003
|
+
group: [{
|
|
1004
|
+
source: source,
|
|
1005
|
+
target: dest
|
|
1006
|
+
}]
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
name() {
|
|
1012
|
+
return `OMOP Concepts`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
id() {
|
|
1016
|
+
return "omop";
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
module.exports = {
|
|
1021
|
+
OMOPServices,
|
|
1022
|
+
OMOPServicesFactory,
|
|
1023
|
+
OMOPConcept,
|
|
1024
|
+
OMOPFilter
|
|
1025
|
+
};
|