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,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
+ };