fhirsmith 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
3
|
+
const { VersionUtilities } = require('../../library/version-utilities');
|
|
4
|
+
const ValueSet = require("../library/valueset");
|
|
5
|
+
const row = require("../library/valueset");
|
|
6
|
+
|
|
7
|
+
// Columns that can be returned directly without parsing JSON
|
|
8
|
+
const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared database layer for ValueSet providers
|
|
12
|
+
* Handles SQLite operations for indexing and searching ValueSets
|
|
13
|
+
*/
|
|
14
|
+
class ValueSetDatabase {
|
|
15
|
+
vsCount;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} dbPath - Path to the SQLite database file
|
|
19
|
+
*/
|
|
20
|
+
constructor(dbPath) {
|
|
21
|
+
this.dbPath = dbPath;
|
|
22
|
+
this._db = null; // Shared read-only connection
|
|
23
|
+
this._writeDb = null; // Write connection (opened only when needed)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get a read-only database connection (opens lazily if needed)
|
|
28
|
+
* @returns {Promise<sqlite3.Database>}
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
_getReadConnection() {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
if (this._db) {
|
|
34
|
+
resolve(this._db);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
this._db = null;
|
|
41
|
+
reject(new Error(`Failed to open database: ${err.message}`));
|
|
42
|
+
} else {
|
|
43
|
+
resolve(this._db);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a read-write database connection (opens lazily if needed)
|
|
51
|
+
* @returns {Promise<sqlite3.Database>}
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
_getWriteConnection() {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
if (this._writeDb) {
|
|
57
|
+
resolve(this._writeDb);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this._writeDb = new sqlite3.Database(this.dbPath, (err) => {
|
|
62
|
+
if (err) {
|
|
63
|
+
this._writeDb = null;
|
|
64
|
+
reject(new Error(`Failed to open database for writing: ${err.message}`));
|
|
65
|
+
} else {
|
|
66
|
+
resolve(this._writeDb);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Close all database connections
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async close() {
|
|
77
|
+
const closePromises = [];
|
|
78
|
+
|
|
79
|
+
if (this._db) {
|
|
80
|
+
closePromises.push(new Promise((resolve) => {
|
|
81
|
+
this._db.close((err) => {
|
|
82
|
+
if (err) console.warn(`Warning closing read connection: ${err.message}`);
|
|
83
|
+
this._db = null;
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this._writeDb) {
|
|
90
|
+
closePromises.push(new Promise((resolve) => {
|
|
91
|
+
this._writeDb.close((err) => {
|
|
92
|
+
if (err) console.warn(`Warning closing write connection: ${err.message}`);
|
|
93
|
+
this._writeDb = null;
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await Promise.all(closePromises);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if the SQLite database already exists
|
|
104
|
+
* @returns {Promise<boolean>}
|
|
105
|
+
*/
|
|
106
|
+
async exists() {
|
|
107
|
+
try {
|
|
108
|
+
await fs.access(this.dbPath);
|
|
109
|
+
return true;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create the SQLite database with required schema
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
*/
|
|
119
|
+
async create() {
|
|
120
|
+
// Close any existing connections first
|
|
121
|
+
await this.close();
|
|
122
|
+
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const db = new sqlite3.Database(this.dbPath, (err) => {
|
|
125
|
+
if (err) {
|
|
126
|
+
reject(new Error(`Failed to create database: ${err.message}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Create tables
|
|
131
|
+
db.serialize(() => {
|
|
132
|
+
// Main value sets table
|
|
133
|
+
db.run(`
|
|
134
|
+
CREATE TABLE valuesets (
|
|
135
|
+
id TEXT PRIMARY KEY,
|
|
136
|
+
url TEXT,
|
|
137
|
+
version TEXT,
|
|
138
|
+
date TEXT,
|
|
139
|
+
description TEXT,
|
|
140
|
+
effectivePeriod_start TEXT,
|
|
141
|
+
effectivePeriod_end TEXT,
|
|
142
|
+
expansion_identifier TEXT,
|
|
143
|
+
name TEXT,
|
|
144
|
+
publisher TEXT,
|
|
145
|
+
status TEXT,
|
|
146
|
+
title TEXT,
|
|
147
|
+
content TEXT NOT NULL,
|
|
148
|
+
last_seen INTEGER DEFAULT (strftime('%s', 'now'))
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
// Identifiers table (0..* Identifier)
|
|
153
|
+
db.run(`
|
|
154
|
+
CREATE TABLE valueset_identifiers (
|
|
155
|
+
valueset_id TEXT,
|
|
156
|
+
system TEXT,
|
|
157
|
+
value TEXT,
|
|
158
|
+
use_code TEXT,
|
|
159
|
+
type_system TEXT,
|
|
160
|
+
type_code TEXT,
|
|
161
|
+
FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
|
|
162
|
+
)
|
|
163
|
+
`);
|
|
164
|
+
|
|
165
|
+
// Jurisdictions table (0..* CodeableConcept with 0..* Coding)
|
|
166
|
+
db.run(`
|
|
167
|
+
CREATE TABLE valueset_jurisdictions (
|
|
168
|
+
valueset_id TEXT,
|
|
169
|
+
system TEXT,
|
|
170
|
+
code TEXT,
|
|
171
|
+
display TEXT,
|
|
172
|
+
FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
|
|
173
|
+
)
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
// Systems table (from compose.include[].system)
|
|
177
|
+
db.run(`
|
|
178
|
+
CREATE TABLE valueset_systems (
|
|
179
|
+
valueset_id TEXT,
|
|
180
|
+
system TEXT,
|
|
181
|
+
FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
|
|
182
|
+
)
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
// Create indexes for better search performance
|
|
186
|
+
db.run('CREATE INDEX idx_valuesets_url ON valuesets(url, version)');
|
|
187
|
+
db.run('CREATE INDEX idx_valuesets_version ON valuesets(version)');
|
|
188
|
+
db.run('CREATE INDEX idx_valuesets_status ON valuesets(status)');
|
|
189
|
+
db.run('CREATE INDEX idx_valuesets_name ON valuesets(name)');
|
|
190
|
+
db.run('CREATE INDEX idx_valuesets_title ON valuesets(title)');
|
|
191
|
+
db.run('CREATE INDEX idx_valuesets_publisher ON valuesets(publisher)');
|
|
192
|
+
db.run('CREATE INDEX idx_valuesets_last_seen ON valuesets(last_seen)');
|
|
193
|
+
db.run('CREATE INDEX idx_identifiers_system ON valueset_identifiers(system)');
|
|
194
|
+
db.run('CREATE INDEX idx_identifiers_value ON valueset_identifiers(value)');
|
|
195
|
+
db.run('CREATE INDEX idx_jurisdictions_system ON valueset_jurisdictions(system)');
|
|
196
|
+
db.run('CREATE INDEX idx_jurisdictions_code ON valueset_jurisdictions(code)');
|
|
197
|
+
db.run('CREATE INDEX idx_systems_system ON valueset_systems(system)');
|
|
198
|
+
|
|
199
|
+
db.close((err) => {
|
|
200
|
+
if (err) {
|
|
201
|
+
reject(new Error(`Failed to close database after creation: ${err.message}`));
|
|
202
|
+
} else {
|
|
203
|
+
resolve();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Insert or update a single ValueSet in the database
|
|
213
|
+
* @param {Object} valueSet - The ValueSet resource
|
|
214
|
+
* @returns {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
async upsertValueSet(valueSet) {
|
|
217
|
+
if (!valueSet.url) {
|
|
218
|
+
throw new Error('ValueSet must have a url property');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const db = await this._getWriteConnection();
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
// Step 1: Delete existing related records
|
|
225
|
+
db.run('DELETE FROM valueset_identifiers WHERE valueset_id = ?', [valueSet.id], (err) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
reject(new Error(`Failed to delete identifiers: ${err.message}`));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
db.run('DELETE FROM valueset_jurisdictions WHERE valueset_id = ?', [valueSet.id], (err) => {
|
|
232
|
+
if (err) {
|
|
233
|
+
reject(new Error(`Failed to delete jurisdictions: ${err.message}`));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
db.run('DELETE FROM valueset_systems WHERE valueset_id = ?', [valueSet.id], (err) => {
|
|
238
|
+
if (err) {
|
|
239
|
+
reject(new Error(`Failed to delete systems: ${err.message}`));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Step 2: Insert main record
|
|
244
|
+
const effectiveStart = valueSet.effectivePeriod?.start || null;
|
|
245
|
+
const effectiveEnd = valueSet.effectivePeriod?.end || null;
|
|
246
|
+
const expansionId = valueSet.expansion?.identifier || null;
|
|
247
|
+
|
|
248
|
+
db.run(`
|
|
249
|
+
INSERT OR REPLACE INTO valuesets (
|
|
250
|
+
id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
|
|
251
|
+
expansion_identifier, name, publisher, status, title, content, last_seen
|
|
252
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
|
253
|
+
`, [
|
|
254
|
+
valueSet.id,
|
|
255
|
+
valueSet.url,
|
|
256
|
+
valueSet.version || null,
|
|
257
|
+
valueSet.date || null,
|
|
258
|
+
valueSet.description || null,
|
|
259
|
+
effectiveStart,
|
|
260
|
+
effectiveEnd,
|
|
261
|
+
expansionId,
|
|
262
|
+
valueSet.name || null,
|
|
263
|
+
valueSet.publisher || null,
|
|
264
|
+
valueSet.status || null,
|
|
265
|
+
valueSet.title || null,
|
|
266
|
+
JSON.stringify(valueSet)
|
|
267
|
+
], (err) => {
|
|
268
|
+
if (err) {
|
|
269
|
+
reject(new Error(`Failed to insert main record: ${err.message}`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Step 3: Insert related records
|
|
274
|
+
this._insertRelatedRecords(db, valueSet, resolve, reject);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Insert related records for a ValueSet
|
|
284
|
+
* @param {sqlite3.Database} db - Database connection
|
|
285
|
+
* @param {Object} valueSet - ValueSet resource
|
|
286
|
+
* @param {Function} resolve - Promise resolve function
|
|
287
|
+
* @param {Function} reject - Promise reject function
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
_insertRelatedRecords(db, valueSet, resolve, reject) {
|
|
291
|
+
let pendingOperations = 0;
|
|
292
|
+
let hasError = false;
|
|
293
|
+
|
|
294
|
+
const operationComplete = () => {
|
|
295
|
+
pendingOperations--;
|
|
296
|
+
if (pendingOperations === 0 && !hasError) {
|
|
297
|
+
resolve();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const operationError = (err) => {
|
|
302
|
+
if (!hasError) {
|
|
303
|
+
hasError = true;
|
|
304
|
+
reject(err);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Insert identifiers
|
|
309
|
+
if (valueSet.identifier) {
|
|
310
|
+
const identifiers = Array.isArray(valueSet.identifier) ? valueSet.identifier : [valueSet.identifier];
|
|
311
|
+
for (const id of identifiers) {
|
|
312
|
+
pendingOperations++;
|
|
313
|
+
const typeSystem = id.type?.coding?.[0]?.system || null;
|
|
314
|
+
const typeCode = id.type?.coding?.[0]?.code || null;
|
|
315
|
+
|
|
316
|
+
db.run(`
|
|
317
|
+
INSERT INTO valueset_identifiers (
|
|
318
|
+
valueset_id, system, value, use_code, type_system, type_code
|
|
319
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
320
|
+
`, [
|
|
321
|
+
valueSet.id,
|
|
322
|
+
id.system || null,
|
|
323
|
+
id.value || null,
|
|
324
|
+
id.use || null,
|
|
325
|
+
typeSystem,
|
|
326
|
+
typeCode
|
|
327
|
+
], (err) => {
|
|
328
|
+
if (err) operationError(new Error(`Failed to insert identifier: ${err.message}`));
|
|
329
|
+
else operationComplete();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Insert jurisdictions
|
|
335
|
+
if (valueSet.jurisdiction) {
|
|
336
|
+
for (const jurisdiction of valueSet.jurisdiction) {
|
|
337
|
+
if (jurisdiction.coding) {
|
|
338
|
+
for (const coding of jurisdiction.coding) {
|
|
339
|
+
pendingOperations++;
|
|
340
|
+
db.run(`
|
|
341
|
+
INSERT INTO valueset_jurisdictions (
|
|
342
|
+
valueset_id, system, code, display
|
|
343
|
+
) VALUES (?, ?, ?, ?)
|
|
344
|
+
`, [
|
|
345
|
+
valueSet.id,
|
|
346
|
+
coding.system || null,
|
|
347
|
+
coding.code || null,
|
|
348
|
+
coding.display || null
|
|
349
|
+
], (err) => {
|
|
350
|
+
if (err) operationError(new Error(`Failed to insert jurisdiction: ${err.message}`));
|
|
351
|
+
else operationComplete();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Insert systems from compose.include
|
|
359
|
+
if (valueSet.compose?.include) {
|
|
360
|
+
for (const include of valueSet.compose.include) {
|
|
361
|
+
if (include.system) {
|
|
362
|
+
pendingOperations++;
|
|
363
|
+
|
|
364
|
+
db.run(`
|
|
365
|
+
INSERT INTO valueset_systems (valueset_id, system) VALUES (?, ?)
|
|
366
|
+
`, [valueSet.id, include.system], function(err) {
|
|
367
|
+
if (err) {
|
|
368
|
+
operationError(new Error(`Failed to insert system: ${err.message}`));
|
|
369
|
+
} else {
|
|
370
|
+
operationComplete();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// If no pending operations, resolve immediately
|
|
378
|
+
if (pendingOperations === 0) {
|
|
379
|
+
resolve();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Insert multiple ValueSets in a batch operation
|
|
385
|
+
* @param {Array<Object>} valueSets - Array of ValueSet resources
|
|
386
|
+
* @returns {Promise<void>}
|
|
387
|
+
*/
|
|
388
|
+
async batchUpsertValueSets(valueSets) {
|
|
389
|
+
if (valueSets.length === 0) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Process sequentially to avoid database locking
|
|
394
|
+
for (const valueSet of valueSets) {
|
|
395
|
+
await this.upsertValueSet(valueSet);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Load all ValueSets from the database
|
|
401
|
+
* @returns {Promise<Map<string, Object>>} Map of all ValueSets keyed by various combinations
|
|
402
|
+
*/
|
|
403
|
+
async loadAllValueSets(source) {
|
|
404
|
+
const db = await this._getReadConnection();
|
|
405
|
+
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
db.all('SELECT id, url, version, content FROM valuesets', [], (err, rows) => {
|
|
408
|
+
if (err) {
|
|
409
|
+
reject(new Error(`Failed to load value sets: ${err.message}`));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
this.vsCount = rows.length;
|
|
415
|
+
const valueSetMap = new Map();
|
|
416
|
+
|
|
417
|
+
for (const row of rows) {
|
|
418
|
+
const valueSet = new ValueSet(JSON.parse(row.content));
|
|
419
|
+
valueSet.sourcePackage = source;
|
|
420
|
+
|
|
421
|
+
// Store by URL and id alone
|
|
422
|
+
valueSetMap.set(row.url, valueSet);
|
|
423
|
+
valueSetMap.set(row.id, valueSet);
|
|
424
|
+
|
|
425
|
+
if (row.version) {
|
|
426
|
+
// Store by url|version
|
|
427
|
+
const versionKey = `${row.url}|${row.version}`;
|
|
428
|
+
valueSetMap.set(versionKey, valueSet);
|
|
429
|
+
|
|
430
|
+
// If version is semver, also store by url|major.minor
|
|
431
|
+
try {
|
|
432
|
+
if (VersionUtilities.isSemVer(row.version)) {
|
|
433
|
+
const majorMinor = VersionUtilities.getMajMin(row.version);
|
|
434
|
+
if (majorMinor) {
|
|
435
|
+
const majorMinorKey = `${row.url}|${majorMinor}`;
|
|
436
|
+
valueSetMap.set(majorMinorKey, valueSet);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch (error) {
|
|
440
|
+
// Ignore version parsing errors, just don't add major.minor key
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
resolve(valueSetMap);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
reject(new Error(`Failed to parse value set content: ${error.message}`));
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Search for ValueSets based on criteria
|
|
455
|
+
* @param {Array<{name: string, value: string}>} searchParams - Search criteria
|
|
456
|
+
* @param {Array<string>|null} elements - Optional list of elements to return (for optimization)
|
|
457
|
+
* @returns {Promise<Array<Object>>} List of matching ValueSets
|
|
458
|
+
*/
|
|
459
|
+
async search(spaceId, map, searchParams, elements = null) {
|
|
460
|
+
// Check if we can optimize by selecting only indexed columns
|
|
461
|
+
const canOptimize = elements && elements.length > 0 &&
|
|
462
|
+
elements.every(e => INDEXED_COLUMNS.includes(e));
|
|
463
|
+
|
|
464
|
+
// Always include 'id' in the columns to select when optimizing
|
|
465
|
+
const columnsToSelect = canOptimize
|
|
466
|
+
? (elements.includes('id') ? elements : ['id', ...elements])
|
|
467
|
+
: null;
|
|
468
|
+
|
|
469
|
+
const db = await this._getReadConnection();
|
|
470
|
+
|
|
471
|
+
return new Promise((resolve, reject) => {
|
|
472
|
+
const { query, params } = this._buildSearchQuery(searchParams, columnsToSelect);
|
|
473
|
+
|
|
474
|
+
db.all(query, params, (err, rows) => {
|
|
475
|
+
if (err) {
|
|
476
|
+
reject(new Error(`Search query failed: ${err.message}`));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
let results;
|
|
482
|
+
if (canOptimize) {
|
|
483
|
+
// Construct objects directly from columns - much faster!
|
|
484
|
+
results = rows.map(row => {
|
|
485
|
+
const obj = { resourceType: 'ValueSet' };
|
|
486
|
+
for (const elem of columnsToSelect) {
|
|
487
|
+
if (row[elem] !== null && row[elem] !== undefined) {
|
|
488
|
+
if (elem === 'id' && spaceId) {
|
|
489
|
+
obj[elem] = `${spaceId}-${row[elem]}`;
|
|
490
|
+
} else {
|
|
491
|
+
obj[elem] = row[elem];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return obj;
|
|
496
|
+
});
|
|
497
|
+
} else {
|
|
498
|
+
// Fall back to parsing JSON
|
|
499
|
+
results = rows.map(row => {
|
|
500
|
+
const vs = map.get(row.id);
|
|
501
|
+
return vs;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
resolve(results);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
reject(new Error(`Failed to parse search results: ${error.message}`));
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Delete ValueSets that weren't seen in the latest scan
|
|
515
|
+
* @param {number} cutoffTimestamp - Unix timestamp, delete records older than this
|
|
516
|
+
* @returns {Promise<number>} Number of records deleted
|
|
517
|
+
*/
|
|
518
|
+
async deleteOldValueSets(cutoffTimestamp) {
|
|
519
|
+
const db = await this._getWriteConnection();
|
|
520
|
+
|
|
521
|
+
return new Promise((resolve, reject) => {
|
|
522
|
+
// Get URLs to delete first
|
|
523
|
+
db.all('SELECT url FROM valuesets WHERE last_seen < ?', [cutoffTimestamp], (err, rows) => {
|
|
524
|
+
if (err) {
|
|
525
|
+
reject(new Error(`Failed to find old records: ${err.message}`));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (rows.length === 0) {
|
|
530
|
+
resolve(0);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const idsToDelete = rows.map(row => row.id);
|
|
535
|
+
let deletedCount = 0;
|
|
536
|
+
let pendingDeletes = 0;
|
|
537
|
+
let hasError = false;
|
|
538
|
+
|
|
539
|
+
const deleteComplete = () => {
|
|
540
|
+
pendingDeletes--;
|
|
541
|
+
if (pendingDeletes === 0 && !hasError) {
|
|
542
|
+
// Finally delete main records
|
|
543
|
+
db.run('DELETE FROM valuesets WHERE last_seen < ?', [cutoffTimestamp], function(err) {
|
|
544
|
+
if (err) {
|
|
545
|
+
reject(new Error(`Failed to delete old records: ${err.message}`));
|
|
546
|
+
} else {
|
|
547
|
+
deletedCount = this.changes;
|
|
548
|
+
resolve(deletedCount);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const deleteError = (err) => {
|
|
555
|
+
if (!hasError) {
|
|
556
|
+
hasError = true;
|
|
557
|
+
reject(err);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// Delete related records first
|
|
562
|
+
const placeholders = idsToDelete.map(() => '?').join(',');
|
|
563
|
+
|
|
564
|
+
pendingDeletes = 3; // identifiers, jurisdictions, systems
|
|
565
|
+
|
|
566
|
+
db.run(`DELETE FROM valueset_identifiers WHERE valueset_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
567
|
+
if (err) deleteError(new Error(`Failed to delete identifier records: ${err.message}`));
|
|
568
|
+
else deleteComplete();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
db.run(`DELETE FROM valueset_jurisdictions WHERE valueset_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
572
|
+
if (err) deleteError(new Error(`Failed to delete jurisdiction records: ${err.message}`));
|
|
573
|
+
else deleteComplete();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
db.run(`DELETE FROM valueset_systems WHERE valueset_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
577
|
+
if (err) deleteError(new Error(`Failed to delete system records: ${err.message}`));
|
|
578
|
+
else deleteComplete();
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get statistics about the database
|
|
586
|
+
* @returns {Promise<Object>} Statistics object
|
|
587
|
+
*/
|
|
588
|
+
async getStatistics() {
|
|
589
|
+
const db = await this._getReadConnection();
|
|
590
|
+
|
|
591
|
+
return new Promise((resolve, reject) => {
|
|
592
|
+
const queries = [
|
|
593
|
+
'SELECT COUNT(*) as total FROM valuesets',
|
|
594
|
+
'SELECT status, COUNT(*) as count FROM valuesets GROUP BY status',
|
|
595
|
+
'SELECT COUNT(DISTINCT system) as systems FROM valueset_systems'
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
const results = {};
|
|
599
|
+
let completed = 0;
|
|
600
|
+
let hasError = false;
|
|
601
|
+
|
|
602
|
+
const checkComplete = () => {
|
|
603
|
+
completed++;
|
|
604
|
+
if (completed === queries.length && !hasError) {
|
|
605
|
+
resolve(results);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const handleError = (err) => {
|
|
610
|
+
if (!hasError) {
|
|
611
|
+
hasError = true;
|
|
612
|
+
reject(err);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Total count
|
|
617
|
+
db.get(queries[0], [], (err, row) => {
|
|
618
|
+
if (err) {
|
|
619
|
+
handleError(err);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
results.totalValueSets = row.total;
|
|
623
|
+
checkComplete();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Status breakdown
|
|
627
|
+
db.all(queries[1], [], (err, rows) => {
|
|
628
|
+
if (err) {
|
|
629
|
+
handleError(err);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
results.byStatus = {};
|
|
633
|
+
for (const row of rows) {
|
|
634
|
+
results.byStatus[row.status || 'null'] = row.count;
|
|
635
|
+
}
|
|
636
|
+
checkComplete();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// System count
|
|
640
|
+
db.get(queries[2], [], (err, row) => {
|
|
641
|
+
if (err) {
|
|
642
|
+
handleError(err);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
results.totalSystems = row.systems;
|
|
646
|
+
checkComplete();
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Build SQL query for search parameters
|
|
653
|
+
* @param {Array<{name: string, value: string}>} searchParams - Search parameters
|
|
654
|
+
* @param {Array<string>|null} elements - If provided, select only these columns (optimization)
|
|
655
|
+
* @returns {{query: string, params: Array}} Query and parameters
|
|
656
|
+
* @private
|
|
657
|
+
*/
|
|
658
|
+
_buildSearchQuery(searchParams, elements = null) {
|
|
659
|
+
const conditions = [];
|
|
660
|
+
const params = [];
|
|
661
|
+
const joins = new Set();
|
|
662
|
+
|
|
663
|
+
for (const param of searchParams) {
|
|
664
|
+
const { name, value } = param;
|
|
665
|
+
|
|
666
|
+
switch (name.toLowerCase()) {
|
|
667
|
+
case 'url':
|
|
668
|
+
conditions.push('v.url = ?');
|
|
669
|
+
params.push(value);
|
|
670
|
+
break;
|
|
671
|
+
|
|
672
|
+
case 'version':
|
|
673
|
+
conditions.push('v.version LIKE ?');
|
|
674
|
+
params.push(`%${value}%`);
|
|
675
|
+
break;
|
|
676
|
+
|
|
677
|
+
case 'name':
|
|
678
|
+
conditions.push('v.name LIKE ?');
|
|
679
|
+
params.push(`%${value}%`);
|
|
680
|
+
break;
|
|
681
|
+
|
|
682
|
+
case 'title':
|
|
683
|
+
conditions.push('v.title LIKE ?');
|
|
684
|
+
params.push(`%${value}%`);
|
|
685
|
+
break;
|
|
686
|
+
|
|
687
|
+
case 'status':
|
|
688
|
+
conditions.push('v.status LIKE ?');
|
|
689
|
+
params.push(`%${value}%`);
|
|
690
|
+
break;
|
|
691
|
+
|
|
692
|
+
case 'publisher':
|
|
693
|
+
conditions.push('v.publisher LIKE ?');
|
|
694
|
+
params.push(`%${value}%`);
|
|
695
|
+
break;
|
|
696
|
+
|
|
697
|
+
case 'description':
|
|
698
|
+
conditions.push('v.description LIKE ?');
|
|
699
|
+
params.push(`%${value}%`);
|
|
700
|
+
break;
|
|
701
|
+
|
|
702
|
+
case 'date':
|
|
703
|
+
conditions.push('v.date LIKE ?');
|
|
704
|
+
params.push(`%${value}%`);
|
|
705
|
+
break;
|
|
706
|
+
|
|
707
|
+
case 'identifier':
|
|
708
|
+
joins.add('JOIN valueset_identifiers vi ON v.id = vi.valueset_id');
|
|
709
|
+
conditions.push('(vi.system = ? OR vi.value LIKE ?)');
|
|
710
|
+
params.push(value, `%${value}%`);
|
|
711
|
+
break;
|
|
712
|
+
|
|
713
|
+
case 'jurisdiction':
|
|
714
|
+
joins.add('JOIN valueset_jurisdictions vj ON v.id = vj.valueset_id');
|
|
715
|
+
conditions.push('(vj.system = ? OR vj.code LIKE ?)');
|
|
716
|
+
params.push(value, `%${value}%`);
|
|
717
|
+
break;
|
|
718
|
+
|
|
719
|
+
case 'system':
|
|
720
|
+
joins.add('JOIN valueset_systems vs ON v.id = vs.valueset_id');
|
|
721
|
+
conditions.push('vs.system = ?');
|
|
722
|
+
params.push(value);
|
|
723
|
+
break;
|
|
724
|
+
|
|
725
|
+
default:
|
|
726
|
+
// For unknown parameters, try to search in the JSON content
|
|
727
|
+
conditions.push('v.content LIKE ?');
|
|
728
|
+
params.push(`%${value}%`);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const joinClause = Array.from(joins).join(' ');
|
|
734
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
735
|
+
|
|
736
|
+
// Select columns based on optimization
|
|
737
|
+
let selectClause;
|
|
738
|
+
if (elements) {
|
|
739
|
+
// Optimized: select only the columns we need
|
|
740
|
+
const columns = elements.map(e => `v.${e}`).join(', ');
|
|
741
|
+
selectClause = `SELECT DISTINCT ${columns}`;
|
|
742
|
+
} else {
|
|
743
|
+
// Full content needed
|
|
744
|
+
selectClause = 'SELECT DISTINCT v.id';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const query = `
|
|
748
|
+
${selectClause}
|
|
749
|
+
FROM valuesets v
|
|
750
|
+
${joinClause}
|
|
751
|
+
${whereClause}
|
|
752
|
+
ORDER BY v.url
|
|
753
|
+
`;
|
|
754
|
+
|
|
755
|
+
return { query, params };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// eslint-disable-next-line no-unused-vars
|
|
759
|
+
assignIds(ids) {
|
|
760
|
+
// nothing - we don't do any assigning.
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Get a list of all ValueSet URLs in the database
|
|
765
|
+
* @returns {Promise<string[]>} Array of ValueSet URLs
|
|
766
|
+
*/
|
|
767
|
+
async listAllValueSets() {
|
|
768
|
+
const db = await this._getReadConnection();
|
|
769
|
+
|
|
770
|
+
return new Promise((resolve, reject) => {
|
|
771
|
+
db.all('SELECT url FROM valuesets ORDER BY url', [], (err, rows) => {
|
|
772
|
+
if (err) {
|
|
773
|
+
reject(new Error(`Failed to list value sets: ${err.message}`));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const urls = rows.map(row => row.url);
|
|
778
|
+
resolve(urls);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
module.exports = {
|
|
785
|
+
ValueSetDatabase
|
|
786
|
+
};
|