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.
Files changed (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. 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
+ };