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,735 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
3
|
+
const { VersionUtilities } = require('../../library/version-utilities');
|
|
4
|
+
const { ConceptMap } = require('../library/conceptmap');
|
|
5
|
+
// Columns that can be returned directly without parsing JSON
|
|
6
|
+
const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared database layer for ConceptMap providers
|
|
10
|
+
* Handles SQLite operations for indexing and searching ConceptMaps
|
|
11
|
+
*/
|
|
12
|
+
class ConceptMapDatabase {
|
|
13
|
+
cmCount;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} dbPath - Path to the SQLite database file
|
|
17
|
+
*/
|
|
18
|
+
constructor(dbPath) {
|
|
19
|
+
this.dbPath = dbPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the SQLite database already exists
|
|
24
|
+
* @returns {Promise<boolean>}
|
|
25
|
+
*/
|
|
26
|
+
async exists() {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(this.dbPath);
|
|
29
|
+
return true;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create the SQLite database with required schema
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
async create() {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const db = new sqlite3.Database(this.dbPath, (err) => {
|
|
42
|
+
if (err) {
|
|
43
|
+
reject(new Error(`Failed to create database: ${err.message}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create tables
|
|
48
|
+
db.serialize(() => {
|
|
49
|
+
// Main concept maps table
|
|
50
|
+
db.run(`
|
|
51
|
+
CREATE TABLE conceptmaps (
|
|
52
|
+
id TEXT PRIMARY KEY,
|
|
53
|
+
url TEXT,
|
|
54
|
+
version TEXT,
|
|
55
|
+
date TEXT,
|
|
56
|
+
description TEXT,
|
|
57
|
+
effectivePeriod_start TEXT,
|
|
58
|
+
effectivePeriod_end TEXT,
|
|
59
|
+
expansion_identifier TEXT,
|
|
60
|
+
name TEXT,
|
|
61
|
+
publisher TEXT,
|
|
62
|
+
status TEXT,
|
|
63
|
+
title TEXT,
|
|
64
|
+
content TEXT NOT NULL,
|
|
65
|
+
last_seen INTEGER DEFAULT (strftime('%s', 'now'))
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
// Identifiers table (0..* Identifier)
|
|
70
|
+
db.run(`
|
|
71
|
+
CREATE TABLE conceptmap_identifiers (
|
|
72
|
+
conceptmap_id TEXT,
|
|
73
|
+
system TEXT,
|
|
74
|
+
value TEXT,
|
|
75
|
+
use_code TEXT,
|
|
76
|
+
type_system TEXT,
|
|
77
|
+
type_code TEXT,
|
|
78
|
+
FOREIGN KEY (conceptmap_id) REFERENCES conceptmaps(url)
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
// Jurisdictions table (0..* CodeableConcept with 0..* Coding)
|
|
83
|
+
db.run(`
|
|
84
|
+
CREATE TABLE conceptmap_jurisdictions (
|
|
85
|
+
conceptmap_id TEXT,
|
|
86
|
+
system TEXT,
|
|
87
|
+
code TEXT,
|
|
88
|
+
display TEXT,
|
|
89
|
+
FOREIGN KEY (conceptmap_id) REFERENCES conceptmaps(url)
|
|
90
|
+
)
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
// Systems table (from compose.include[].system)
|
|
94
|
+
db.run(`
|
|
95
|
+
CREATE TABLE conceptmap_systems (
|
|
96
|
+
conceptmap_id TEXT,
|
|
97
|
+
system TEXT,
|
|
98
|
+
FOREIGN KEY (conceptmap_id) REFERENCES conceptmaps(url)
|
|
99
|
+
)
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
// Create indexes for better search performance
|
|
103
|
+
db.run('CREATE INDEX idx_conceptmaps_url ON conceptmaps(url, version)');
|
|
104
|
+
db.run('CREATE INDEX idx_conceptmaps_version ON conceptmaps(version)');
|
|
105
|
+
db.run('CREATE INDEX idx_conceptmaps_status ON conceptmaps(status)');
|
|
106
|
+
db.run('CREATE INDEX idx_conceptmaps_name ON conceptmaps(name)');
|
|
107
|
+
db.run('CREATE INDEX idx_conceptmaps_title ON conceptmaps(title)');
|
|
108
|
+
db.run('CREATE INDEX idx_conceptmaps_publisher ON conceptmaps(publisher)');
|
|
109
|
+
db.run('CREATE INDEX idx_conceptmaps_last_seen ON conceptmaps(last_seen)');
|
|
110
|
+
db.run('CREATE INDEX idx_identifiers_system ON conceptmap_identifiers(system)');
|
|
111
|
+
db.run('CREATE INDEX idx_identifiers_value ON conceptmap_identifiers(value)');
|
|
112
|
+
db.run('CREATE INDEX idx_jurisdictions_system ON conceptmap_jurisdictions(system)');
|
|
113
|
+
db.run('CREATE INDEX idx_jurisdictions_code ON conceptmap_jurisdictions(code)');
|
|
114
|
+
db.run('CREATE INDEX idx_systems_system ON conceptmap_systems(system)');
|
|
115
|
+
|
|
116
|
+
db.close((err) => {
|
|
117
|
+
if (err) {
|
|
118
|
+
reject(new Error(`Failed to close database after creation: ${err.message}`));
|
|
119
|
+
} else {
|
|
120
|
+
resolve();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Insert or update a single ConceptMap in the database
|
|
130
|
+
* @param {Object} conceptMap - The ConceptMap resource
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async upsertConceptMap(conceptMap) {
|
|
134
|
+
if (!conceptMap.url) {
|
|
135
|
+
throw new Error('ConceptMap must have a url property');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const db = new sqlite3.Database(this.dbPath, (err) => {
|
|
140
|
+
if (err) {
|
|
141
|
+
reject(new Error(`Failed to open database: ${err.message}`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Step 1: Delete existing related records
|
|
146
|
+
db.run('DELETE FROM conceptmap_identifiers WHERE conceptmap_id = ?', [conceptMap.id], (err) => {
|
|
147
|
+
if (err) {
|
|
148
|
+
db.close();
|
|
149
|
+
reject(new Error(`Failed to delete identifiers: ${err.message}`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
db.run('DELETE FROM conceptmap_jurisdictions WHERE conceptmap_id = ?', [conceptMap.id], (err) => {
|
|
154
|
+
if (err) {
|
|
155
|
+
db.close();
|
|
156
|
+
reject(new Error(`Failed to delete jurisdictions: ${err.message}`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
db.run('DELETE FROM conceptmap_systems WHERE conceptmap_id = ?', [conceptMap.id], (err) => {
|
|
161
|
+
if (err) {
|
|
162
|
+
db.close();
|
|
163
|
+
reject(new Error(`Failed to delete systems: ${err.message}`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Step 2: Insert main record
|
|
168
|
+
const effectiveStart = conceptMap.effectivePeriod?.start || null;
|
|
169
|
+
const effectiveEnd = conceptMap.effectivePeriod?.end || null;
|
|
170
|
+
const expansionId = conceptMap.expansion?.identifier || null;
|
|
171
|
+
|
|
172
|
+
db.run(`
|
|
173
|
+
INSERT OR REPLACE INTO conceptmaps (
|
|
174
|
+
id, url, version, date, description, effectivePeriod_start, effectivePeriod_end,
|
|
175
|
+
expansion_identifier, name, publisher, status, title, content, last_seen
|
|
176
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
|
177
|
+
`, [
|
|
178
|
+
conceptMap.id,
|
|
179
|
+
conceptMap.url,
|
|
180
|
+
conceptMap.version || null,
|
|
181
|
+
conceptMap.date || null,
|
|
182
|
+
conceptMap.description || null,
|
|
183
|
+
effectiveStart,
|
|
184
|
+
effectiveEnd,
|
|
185
|
+
expansionId,
|
|
186
|
+
conceptMap.name || null,
|
|
187
|
+
conceptMap.publisher || null,
|
|
188
|
+
conceptMap.status || null,
|
|
189
|
+
conceptMap.title || null,
|
|
190
|
+
JSON.stringify(conceptMap)
|
|
191
|
+
], (err) => {
|
|
192
|
+
if (err) {
|
|
193
|
+
db.close();
|
|
194
|
+
reject(new Error(`Failed to insert main record: ${err.message}`));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Step 3: Insert related records
|
|
199
|
+
this._insertRelatedRecords(db, conceptMap, resolve, reject);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Insert related records for a ConceptMap
|
|
210
|
+
* @param {sqlite3.Database} db - Database connection
|
|
211
|
+
* @param {Object} conceptMap - ConceptMap resource
|
|
212
|
+
* @param {Function} resolve - Promise resolve function
|
|
213
|
+
* @param {Function} reject - Promise reject function
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
_insertRelatedRecords(db, conceptMap, resolve, reject) {
|
|
217
|
+
let pendingOperations = 0;
|
|
218
|
+
let hasError = false;
|
|
219
|
+
|
|
220
|
+
const operationComplete = () => {
|
|
221
|
+
pendingOperations--;
|
|
222
|
+
if (pendingOperations === 0 && !hasError) {
|
|
223
|
+
db.close();
|
|
224
|
+
resolve();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const operationError = (err) => {
|
|
229
|
+
if (!hasError) {
|
|
230
|
+
hasError = true;
|
|
231
|
+
db.close();
|
|
232
|
+
reject(err);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Insert identifiers
|
|
237
|
+
if (conceptMap.identifier) {
|
|
238
|
+
const identifiers = Array.isArray(conceptMap.identifier) ? conceptMap.identifier : [conceptMap.identifier];
|
|
239
|
+
for (const id of identifiers) {
|
|
240
|
+
pendingOperations++;
|
|
241
|
+
const typeSystem = id.type?.coding?.[0]?.system || null;
|
|
242
|
+
const typeCode = id.type?.coding?.[0]?.code || null;
|
|
243
|
+
|
|
244
|
+
db.run(`
|
|
245
|
+
INSERT INTO conceptmap_identifiers (
|
|
246
|
+
conceptmap_id, system, value, use_code, type_system, type_code
|
|
247
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
248
|
+
`, [
|
|
249
|
+
conceptMap.id,
|
|
250
|
+
id.system || null,
|
|
251
|
+
id.value || null,
|
|
252
|
+
id.use || null,
|
|
253
|
+
typeSystem,
|
|
254
|
+
typeCode
|
|
255
|
+
], (err) => {
|
|
256
|
+
if (err) operationError(new Error(`Failed to insert identifier: ${err.message}`));
|
|
257
|
+
else operationComplete();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Insert jurisdictions
|
|
263
|
+
if (conceptMap.jurisdiction) {
|
|
264
|
+
for (const jurisdiction of conceptMap.jurisdiction) {
|
|
265
|
+
if (jurisdiction.coding) {
|
|
266
|
+
for (const coding of jurisdiction.coding) {
|
|
267
|
+
pendingOperations++;
|
|
268
|
+
db.run(`
|
|
269
|
+
INSERT INTO conceptmap_jurisdictions (
|
|
270
|
+
conceptmap_id, system, code, display
|
|
271
|
+
) VALUES (?, ?, ?, ?)
|
|
272
|
+
`, [
|
|
273
|
+
conceptMap.id,
|
|
274
|
+
coding.system || null,
|
|
275
|
+
coding.code || null,
|
|
276
|
+
coding.display || null
|
|
277
|
+
], (err) => {
|
|
278
|
+
if (err) operationError(new Error(`Failed to insert jurisdiction: ${err.message}`));
|
|
279
|
+
else operationComplete();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Insert systems from compose.include
|
|
287
|
+
if (conceptMap.compose?.include) {
|
|
288
|
+
for (const include of conceptMap.compose.include) {
|
|
289
|
+
if (include.system) {
|
|
290
|
+
pendingOperations++;
|
|
291
|
+
|
|
292
|
+
db.run(`
|
|
293
|
+
INSERT INTO conceptmap_systems (conceptmap_id, system) VALUES (?, ?)
|
|
294
|
+
`, [conceptMap.id, include.system], function(err) {
|
|
295
|
+
if (err) {
|
|
296
|
+
operationError(new Error(`Failed to insert system: ${err.message}`));
|
|
297
|
+
} else {
|
|
298
|
+
operationComplete();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// If no pending operations, close immediately
|
|
306
|
+
if (pendingOperations === 0) {
|
|
307
|
+
db.close();
|
|
308
|
+
resolve();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Insert multiple ConceptMaps in a batch operation
|
|
314
|
+
* @param {Array<Object>} conceptMaps - Array of ConceptMap resources
|
|
315
|
+
* @returns {Promise<void>}
|
|
316
|
+
*/
|
|
317
|
+
async batchUpsertConceptMaps(conceptMaps) {
|
|
318
|
+
if (conceptMaps.length === 0) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Process sequentially to avoid database locking
|
|
323
|
+
for (const conceptMap of conceptMaps) {
|
|
324
|
+
await this.upsertConceptMap(conceptMap.jsonObj);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Load all ConceptMaps from the database
|
|
330
|
+
* @returns {Promise<Map<string, Object>>} Map of all ConceptMaps keyed by various combinations
|
|
331
|
+
*/
|
|
332
|
+
async loadAllConceptMaps() {
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
335
|
+
if (err) {
|
|
336
|
+
reject(new Error(`Failed to open database for loading: ${err.message}`));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
db.all('SELECT id, url, version, content FROM conceptmaps', [], (err, rows) => {
|
|
341
|
+
if (err) {
|
|
342
|
+
db.close();
|
|
343
|
+
reject(new Error(`Failed to load concept maps: ${err.message}`));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const conceptMapMap = new Map();
|
|
349
|
+
this.cmCount = rows.length;
|
|
350
|
+
|
|
351
|
+
for (const row of rows) {
|
|
352
|
+
const conceptMap = new ConceptMap(JSON.parse(row.content));
|
|
353
|
+
|
|
354
|
+
// Store by URL and id alone
|
|
355
|
+
conceptMapMap.set(row.url, conceptMap);
|
|
356
|
+
conceptMapMap.set(row.id, conceptMap);
|
|
357
|
+
|
|
358
|
+
if (row.version) {
|
|
359
|
+
// Store by url|version
|
|
360
|
+
const versionKey = `${row.url}|${row.version}`;
|
|
361
|
+
conceptMapMap.set(versionKey, conceptMap);
|
|
362
|
+
|
|
363
|
+
// If version is semver, also store by url|major.minor
|
|
364
|
+
try {
|
|
365
|
+
if (VersionUtilities.isSemVer(row.version)) {
|
|
366
|
+
const majorMinor = VersionUtilities.getMajMin(row.version);
|
|
367
|
+
if (majorMinor) {
|
|
368
|
+
const majorMinorKey = `${row.url}|${majorMinor}`;
|
|
369
|
+
conceptMapMap.set(majorMinorKey, conceptMap);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// Ignore version parsing errors, just don't add major.minor key
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
db.close((err) => {
|
|
379
|
+
if (err) {
|
|
380
|
+
reject(new Error(`Failed to close database after loading: ${err.message}`));
|
|
381
|
+
} else {
|
|
382
|
+
resolve(conceptMapMap);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
db.close();
|
|
387
|
+
reject(new Error(`Failed to parse concept map content: ${error.message}`));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Search for ConceptMaps based on criteria
|
|
396
|
+
* @param {Array<{name: string, value: string}>} searchParams - Search criteria
|
|
397
|
+
* @param {Array<string>|null} elements - Optional list of elements to return (for optimization)
|
|
398
|
+
* @returns {Promise<Array<Object>>} List of matching ConceptMaps
|
|
399
|
+
*/
|
|
400
|
+
async search(spaceId, searchParams, elements = null) {
|
|
401
|
+
// Check if we can optimize by selecting only indexed columns
|
|
402
|
+
const canOptimize = elements && elements.length > 0 &&
|
|
403
|
+
elements.every(e => INDEXED_COLUMNS.includes(e));
|
|
404
|
+
|
|
405
|
+
// Always include 'id' in the columns to select when optimizing
|
|
406
|
+
const columnsToSelect = canOptimize
|
|
407
|
+
? (elements.includes('id') ? elements : ['id', ...elements])
|
|
408
|
+
: null;
|
|
409
|
+
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
412
|
+
if (err) {
|
|
413
|
+
reject(new Error(`Failed to open database for search: ${err.message}`));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const { query, params } = this._buildSearchQuery(searchParams, columnsToSelect);
|
|
418
|
+
|
|
419
|
+
db.all(query, params, (err, rows) => {
|
|
420
|
+
if (err) {
|
|
421
|
+
db.close();
|
|
422
|
+
reject(new Error(`Search query failed: ${err.message}`));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
let results;
|
|
428
|
+
if (canOptimize) {
|
|
429
|
+
// Construct objects directly from columns - much faster!
|
|
430
|
+
results = rows.map(row => {
|
|
431
|
+
const obj = { resourceType: 'ConceptMap' };
|
|
432
|
+
for (const elem of columnsToSelect) {
|
|
433
|
+
if (row[elem] !== null && row[elem] !== undefined) {
|
|
434
|
+
if (elem === 'id' && spaceId) {
|
|
435
|
+
obj[elem] = `${spaceId}-${row[elem]}`;
|
|
436
|
+
} else {
|
|
437
|
+
obj[elem] = row[elem];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return obj;
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
// Fall back to parsing JSON
|
|
445
|
+
results = rows.map(row => {
|
|
446
|
+
const parsed = JSON.parse(row.content);
|
|
447
|
+
// Prefix id with spaceId if provided
|
|
448
|
+
if (spaceId && parsed.id) {
|
|
449
|
+
parsed.id = `${spaceId}-${parsed.id}`;
|
|
450
|
+
}
|
|
451
|
+
return parsed;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
db.close((err) => {
|
|
456
|
+
if (err) {
|
|
457
|
+
reject(new Error(`Failed to close database after search: ${err.message}`));
|
|
458
|
+
} else {
|
|
459
|
+
resolve(results);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
db.close();
|
|
464
|
+
reject(new Error(`Failed to parse search results: ${error.message}`));
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Delete ConceptMaps that weren't seen in the latest scan
|
|
473
|
+
* @param {number} cutoffTimestamp - Unix timestamp, delete records older than this
|
|
474
|
+
* @returns {Promise<number>} Number of records deleted
|
|
475
|
+
*/
|
|
476
|
+
async deleteOldConceptMaps(cutoffTimestamp) {
|
|
477
|
+
return new Promise((resolve, reject) => {
|
|
478
|
+
const db = new sqlite3.Database(this.dbPath, (err) => {
|
|
479
|
+
if (err) {
|
|
480
|
+
reject(new Error(`Failed to open database for cleanup: ${err.message}`));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Get URLs to delete first
|
|
485
|
+
db.all('SELECT url FROM conceptmaps WHERE last_seen < ?', [cutoffTimestamp], (err, rows) => {
|
|
486
|
+
if (err) {
|
|
487
|
+
db.close();
|
|
488
|
+
reject(new Error(`Failed to find old records: ${err.message}`));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (rows.length === 0) {
|
|
493
|
+
db.close();
|
|
494
|
+
resolve(0);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const idsToDelete = rows.map(row => row.id);
|
|
499
|
+
let deletedCount = 0;
|
|
500
|
+
let pendingDeletes = 0;
|
|
501
|
+
let hasError = false;
|
|
502
|
+
|
|
503
|
+
const deleteComplete = () => {
|
|
504
|
+
pendingDeletes--;
|
|
505
|
+
if (pendingDeletes === 0 && !hasError) {
|
|
506
|
+
// Finally delete main records
|
|
507
|
+
db.run('DELETE FROM conceptmaps WHERE last_seen < ?', [cutoffTimestamp], function(err) {
|
|
508
|
+
if (err) {
|
|
509
|
+
db.close();
|
|
510
|
+
reject(new Error(`Failed to delete old records: ${err.message}`));
|
|
511
|
+
} else {
|
|
512
|
+
deletedCount = this.changes;
|
|
513
|
+
db.close();
|
|
514
|
+
resolve(deletedCount);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const deleteError = (err) => {
|
|
521
|
+
if (!hasError) {
|
|
522
|
+
hasError = true;
|
|
523
|
+
db.close();
|
|
524
|
+
reject(err);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Delete related records first
|
|
529
|
+
const placeholders = idsToDelete.map(() => '?').join(',');
|
|
530
|
+
|
|
531
|
+
pendingDeletes = 3; // identifiers, jurisdictions, systems
|
|
532
|
+
|
|
533
|
+
db.run(`DELETE FROM conceptmap_identifiers WHERE conceptmap_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
534
|
+
if (err) deleteError(new Error(`Failed to delete identifier records: ${err.message}`));
|
|
535
|
+
else deleteComplete();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
db.run(`DELETE FROM conceptmap_jurisdictions WHERE conceptmap_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
539
|
+
if (err) deleteError(new Error(`Failed to delete jurisdiction records: ${err.message}`));
|
|
540
|
+
else deleteComplete();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
db.run(`DELETE FROM conceptmap_systems WHERE conceptmap_id IN (${placeholders})`, idsToDelete, (err) => {
|
|
544
|
+
if (err) deleteError(new Error(`Failed to delete system records: ${err.message}`));
|
|
545
|
+
else deleteComplete();
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get statistics about the database
|
|
554
|
+
* @returns {Promise<Object>} Statistics object
|
|
555
|
+
*/
|
|
556
|
+
async getStatistics() {
|
|
557
|
+
return new Promise((resolve, reject) => {
|
|
558
|
+
const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
559
|
+
if (err) {
|
|
560
|
+
reject(new Error(`Failed to open database for statistics: ${err.message}`));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const queries = [
|
|
565
|
+
'SELECT COUNT(*) as total FROM conceptmaps',
|
|
566
|
+
'SELECT status, COUNT(*) as count FROM conceptmaps GROUP BY status',
|
|
567
|
+
'SELECT COUNT(DISTINCT system) as systems FROM conceptmap_systems'
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
const results = {};
|
|
571
|
+
let completed = 0;
|
|
572
|
+
|
|
573
|
+
const checkComplete = () => {
|
|
574
|
+
completed++;
|
|
575
|
+
if (completed === queries.length) {
|
|
576
|
+
db.close();
|
|
577
|
+
resolve(results);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// Total count
|
|
582
|
+
db.get(queries[0], [], (err, row) => {
|
|
583
|
+
if (err) {
|
|
584
|
+
db.close();
|
|
585
|
+
reject(err);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
results.totalConceptMaps = row.total;
|
|
589
|
+
checkComplete();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Status breakdown
|
|
593
|
+
db.all(queries[1], [], (err, rows) => {
|
|
594
|
+
if (err) {
|
|
595
|
+
db.close();
|
|
596
|
+
reject(err);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
results.byStatus = {};
|
|
600
|
+
for (const row of rows) {
|
|
601
|
+
results.byStatus[row.status || 'null'] = row.count;
|
|
602
|
+
}
|
|
603
|
+
checkComplete();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// System count
|
|
607
|
+
db.get(queries[2], [], (err, row) => {
|
|
608
|
+
if (err) {
|
|
609
|
+
db.close();
|
|
610
|
+
reject(err);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
results.totalSystems = row.systems;
|
|
614
|
+
checkComplete();
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Build SQL query for search parameters
|
|
622
|
+
* @param {Array<{name: string, value: string}>} searchParams - Search parameters
|
|
623
|
+
* @param {Array<string>|null} elements - If provided, select only these columns (optimization)
|
|
624
|
+
* @returns {{query: string, params: Array}} Query and parameters
|
|
625
|
+
* @private
|
|
626
|
+
*/
|
|
627
|
+
_buildSearchQuery(searchParams, elements = null) {
|
|
628
|
+
const conditions = [];
|
|
629
|
+
const params = [];
|
|
630
|
+
const joins = new Set();
|
|
631
|
+
|
|
632
|
+
for (const param of searchParams) {
|
|
633
|
+
const { name, value } = param;
|
|
634
|
+
|
|
635
|
+
switch (name.toLowerCase()) {
|
|
636
|
+
case 'url':
|
|
637
|
+
conditions.push('v.url LIKE ?');
|
|
638
|
+
params.push(`%${value}%`);
|
|
639
|
+
break;
|
|
640
|
+
|
|
641
|
+
case 'version':
|
|
642
|
+
conditions.push('v.version LIKE ?');
|
|
643
|
+
params.push(`%${value}%`);
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
case 'name':
|
|
647
|
+
conditions.push('v.name LIKE ?');
|
|
648
|
+
params.push(`%${value}%`);
|
|
649
|
+
break;
|
|
650
|
+
|
|
651
|
+
case 'title':
|
|
652
|
+
conditions.push('v.title LIKE ?');
|
|
653
|
+
params.push(`%${value}%`);
|
|
654
|
+
break;
|
|
655
|
+
|
|
656
|
+
case 'status':
|
|
657
|
+
conditions.push('v.status LIKE ?');
|
|
658
|
+
params.push(`%${value}%`);
|
|
659
|
+
break;
|
|
660
|
+
|
|
661
|
+
case 'publisher':
|
|
662
|
+
conditions.push('v.publisher LIKE ?');
|
|
663
|
+
params.push(`%${value}%`);
|
|
664
|
+
break;
|
|
665
|
+
|
|
666
|
+
case 'description':
|
|
667
|
+
conditions.push('v.description LIKE ?');
|
|
668
|
+
params.push(`%${value}%`);
|
|
669
|
+
break;
|
|
670
|
+
|
|
671
|
+
case 'date':
|
|
672
|
+
conditions.push('v.date LIKE ?');
|
|
673
|
+
params.push(`%${value}%`);
|
|
674
|
+
break;
|
|
675
|
+
|
|
676
|
+
case 'identifier':
|
|
677
|
+
joins.add('JOIN conceptmap_identifiers vi ON v.id = vi.conceptmap_id');
|
|
678
|
+
conditions.push('(vi.system = ? OR vi.value LIKE ?)');
|
|
679
|
+
params.push(value, `%${value}%`);
|
|
680
|
+
break;
|
|
681
|
+
|
|
682
|
+
case 'jurisdiction':
|
|
683
|
+
joins.add('JOIN conceptmap_jurisdictions vj ON v.id = vj.conceptmap_id');
|
|
684
|
+
conditions.push('(vj.system = ? OR vj.code LIKE ?)');
|
|
685
|
+
params.push(value, `%${value}%`);
|
|
686
|
+
break;
|
|
687
|
+
|
|
688
|
+
case 'system':
|
|
689
|
+
joins.add('JOIN conceptmap_systems vs ON v.id = vs.conceptmap_id');
|
|
690
|
+
conditions.push('vs.system LIKE ?');
|
|
691
|
+
params.push(`%${value}%`);
|
|
692
|
+
break;
|
|
693
|
+
|
|
694
|
+
default:
|
|
695
|
+
// For unknown parameters, try to search in the JSON content
|
|
696
|
+
conditions.push('v.content LIKE ?');
|
|
697
|
+
params.push(`%"${name}"%${value}%`);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const joinClause = Array.from(joins).join(' ');
|
|
703
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
704
|
+
|
|
705
|
+
// Select columns based on optimization
|
|
706
|
+
let selectClause;
|
|
707
|
+
if (elements) {
|
|
708
|
+
// Optimized: select only the columns we need
|
|
709
|
+
const columns = elements.map(e => `v.${e}`).join(', ');
|
|
710
|
+
selectClause = `SELECT DISTINCT ${columns}`;
|
|
711
|
+
} else {
|
|
712
|
+
// Full content needed
|
|
713
|
+
selectClause = 'SELECT DISTINCT v.content';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const query = `
|
|
717
|
+
${selectClause}
|
|
718
|
+
FROM conceptmaps v
|
|
719
|
+
${joinClause}
|
|
720
|
+
${whereClause}
|
|
721
|
+
ORDER BY v.url
|
|
722
|
+
`;
|
|
723
|
+
|
|
724
|
+
return { query, params };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// eslint-disable-next-line no-unused-vars
|
|
728
|
+
assignIds(ids) {
|
|
729
|
+
// nothing - we don't do any assigning.
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
module.exports = {
|
|
734
|
+
ConceptMapDatabase
|
|
735
|
+
};
|