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,358 @@
1
+ const path = require('path');
2
+ const { AbstractValueSetProvider } = require('./vs-api');
3
+ const { PackageContentLoader } = require('../../library/package-manager');
4
+ const { ValueSetDatabase } = require('./vs-database');
5
+ const { VersionUtilities } = require('../../library/version-utilities');
6
+ const {validateParameter} = require("../../library/utilities");
7
+
8
+ /**
9
+ * Package-based ValueSet provider using shared database layer
10
+ */
11
+ class PackageValueSetProvider extends AbstractValueSetProvider {
12
+ USE_DATABASE_SEARCH = true;
13
+
14
+ /**
15
+ * @param {PackageContentLoader} packageLoader - Path to the extracted package folder
16
+ */
17
+ constructor(packageLoader) {
18
+ super();
19
+ validateParameter(packageLoader, "packageLoader", PackageContentLoader);
20
+ this.packageLoader = packageLoader;
21
+ this.dbPath = path.join(packageLoader.packageFolder, '.valuesets.db');
22
+ this.database = new ValueSetDatabase(this.dbPath);
23
+ this.valueSetMap = new Map();
24
+ this.initialized = false;
25
+ this.count = 0;
26
+ }
27
+
28
+ /**
29
+ * Initialize the provider - check/create database and load value sets into memory
30
+ * @returns {Promise<void>}
31
+ */
32
+ async initialize() {
33
+ if (this.initialized) {
34
+ return;
35
+ }
36
+ await this.packageLoader.initialize();
37
+
38
+ const dbExists = await this.database.exists();
39
+
40
+ if (!dbExists) {
41
+ await this.database.create();
42
+ await this._populateDatabase();
43
+ }
44
+
45
+ this.valueSetMap = await this.database.loadAllValueSets(this.packageLoader.pid());
46
+ this.initialized = true;
47
+ }
48
+
49
+ async close() {
50
+ await this.database.close();
51
+ }
52
+ /**
53
+ * Populate the database with value sets from the package
54
+ * @returns {Promise<void>}
55
+ * @private
56
+ */
57
+ async _populateDatabase() {
58
+ // Get all ValueSet resources
59
+ const valueSetEntries = await this.packageLoader.getResourcesByType('ValueSet');
60
+
61
+ if (valueSetEntries.length === 0) {
62
+ return; // No value sets in this package
63
+ }
64
+
65
+ const valueSets = [];
66
+ for (const entry of valueSetEntries) {
67
+ const valueSet = await this.packageLoader.loadFile(entry);
68
+ if (valueSet.url) {
69
+ valueSets.push(valueSet);
70
+ }
71
+ }
72
+
73
+ if (valueSets.length > 0) {
74
+ await this.database.batchUpsertValueSets(valueSets);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Fetches a value set by URL and version
80
+ * @param {string} url - The canonical URL of the value set
81
+ * @param {string} version - The version of the value set
82
+ * @returns {Promise<Object>} The requested value set
83
+ */
84
+ async fetchValueSet(url, version) {
85
+ await this.initialize();
86
+ this._validateFetchParams(url, version);
87
+
88
+ // Try exact match first: url|version
89
+ let key = `${url}|${version}`;
90
+ if (this.valueSetMap.has(key)) {
91
+ return this.valueSetMap.get(key);
92
+ }
93
+
94
+ // If version is semver, try url|major.minor
95
+ try {
96
+ if (VersionUtilities.isSemVer(version)) {
97
+ const majorMinor = VersionUtilities.getMajMin(version);
98
+ if (majorMinor) {
99
+ key = `${url}|${majorMinor}`;
100
+ if (this.valueSetMap.has(key)) {
101
+ return this.valueSetMap.get(key);
102
+ }
103
+ }
104
+ }
105
+ } catch (error) {
106
+ // Ignore version parsing errors
107
+ }
108
+
109
+ // Finally try just the URL
110
+ if (this.valueSetMap.has(url)) {
111
+ return this.valueSetMap.get(url);
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Searches for value sets based on criteria
119
+ * @param {Array<{name: string, value: string}>} searchParams - Search criteria
120
+ * @returns {Promise<Array<Object>>} List of matching value sets
121
+ */
122
+ async searchValueSets(searchParams, elements = null) {
123
+ await this.initialize();
124
+ this._validateSearchParams(searchParams);
125
+
126
+ if (this.USE_DATABASE_SEARCH) {
127
+ return await this.database.search(this.spaceId, this.valueSetMap, searchParams, elements);
128
+ } else {
129
+ const matches = [];
130
+ const seen = new Set(); // Track by URL to avoid duplicates from versioned keys
131
+
132
+ // Convert array format to object for easier access
133
+ const params = {};
134
+ for (const {name, value} of searchParams) {
135
+ params[name] = value.toLowerCase();
136
+ }
137
+
138
+ const hasSearchParams = Object.keys(params).length > 0;
139
+
140
+ for (const vs of this.valueSetMap.values()) {
141
+ const json = vs.jsonObj || vs;
142
+
143
+ // Only process each ValueSet once (use URL to deduplicate)
144
+ const vsUrl = json.url;
145
+ if (seen.has(vsUrl)) {
146
+ continue;
147
+ }
148
+
149
+ if (!hasSearchParams) {
150
+ seen.add(vsUrl);
151
+ matches.push(vs);
152
+ continue;
153
+ }
154
+
155
+ // Check each search parameter
156
+ let isMatch = true;
157
+ for (const [param, searchValue] of Object.entries(params)) {
158
+ // Ignore content-mode and supplements for ValueSet search
159
+ if (param === 'content-mode' || param === 'supplements') {
160
+ continue;
161
+ }
162
+
163
+ if (param === 'system') {
164
+ // Special handling: match against compose.include[].system
165
+ if (!this._matchSystem(json, searchValue)) {
166
+ isMatch = false;
167
+ break;
168
+ }
169
+ } else if (param === 'jurisdiction') {
170
+ // Special handling for jurisdiction - array of CodeableConcept
171
+ if (!this._matchJurisdiction(json.jurisdiction, searchValue)) {
172
+ isMatch = false;
173
+ break;
174
+ }
175
+ } else if (param === 'identifier') {
176
+ // Special handling for identifier
177
+ if (!this._matchIdentifier(json.identifier, searchValue)) {
178
+ isMatch = false;
179
+ break;
180
+ }
181
+ } else if (param === 'url') {
182
+ const propValue = json[param];
183
+ if (!this._matchValueFull(propValue, searchValue)) {
184
+ isMatch = false;
185
+ break;
186
+ }
187
+ } else {
188
+ // Standard partial text match on property
189
+ const propValue = json[param];
190
+ if (!this._matchValue(propValue, searchValue)) {
191
+ isMatch = false;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+
197
+ if (isMatch) {
198
+ seen.add(vsUrl);
199
+ // Return with prefixed id
200
+ matches.push(json);
201
+ }
202
+ }
203
+
204
+ return matches;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if a value matches the search term (partial, case-insensitive)
210
+ */
211
+ _matchValue(propValue, searchValue) {
212
+ if (propValue === undefined || propValue === null) {
213
+ return false;
214
+ }
215
+ const strValue = String(propValue).toLowerCase();
216
+ return strValue.includes(searchValue);
217
+ }
218
+
219
+ /**
220
+ * Check if a value matches the search term (partial, case-insensitive)
221
+ */
222
+ _matchValueFull(propValue, searchValue) {
223
+ if (propValue === undefined || propValue === null) {
224
+ return false;
225
+ }
226
+ const strValue = String(propValue).toLowerCase();
227
+ return strValue === searchValue;
228
+ }
229
+
230
+ /**
231
+ * Check if system matches any compose.include[].system
232
+ */
233
+ _matchSystem(json, searchValue) {
234
+ if (!json.compose?.include || !Array.isArray(json.compose.include)) {
235
+ return false;
236
+ }
237
+ for (const include of json.compose.include) {
238
+ if (include.system && include.system.toLowerCase().includes(searchValue)) {
239
+ return true;
240
+ }
241
+ }
242
+ return false;
243
+ }
244
+
245
+ /**
246
+ * Check if jurisdiction matches - jurisdiction is an array of CodeableConcept
247
+ */
248
+ _matchJurisdiction(jurisdictions, searchValue) {
249
+ if (!jurisdictions || !Array.isArray(jurisdictions)) {
250
+ return false;
251
+ }
252
+ for (const cc of jurisdictions) {
253
+ if (cc.coding && Array.isArray(cc.coding)) {
254
+ for (const coding of cc.coding) {
255
+ if (coding.code && coding.code.toLowerCase().includes(searchValue)) {
256
+ return true;
257
+ }
258
+ if (coding.display && coding.display.toLowerCase().includes(searchValue)) {
259
+ return true;
260
+ }
261
+ }
262
+ }
263
+ if (cc.text && cc.text.toLowerCase().includes(searchValue)) {
264
+ return true;
265
+ }
266
+ }
267
+ return false;
268
+ }
269
+
270
+ /**
271
+ * Check if identifier matches
272
+ */
273
+ _matchIdentifier(identifiers, searchValue) {
274
+ if (!identifiers) {
275
+ return false;
276
+ }
277
+ const idArray = Array.isArray(identifiers) ? identifiers : [identifiers];
278
+ for (const id of idArray) {
279
+ if (id.system && id.system.toLowerCase().includes(searchValue)) {
280
+ return true;
281
+ }
282
+ if (id.value && id.value.toLowerCase().includes(searchValue)) {
283
+ return true;
284
+ }
285
+ }
286
+ return false;
287
+ }
288
+
289
+ /**
290
+ * Get statistics about the loaded value sets
291
+ * @returns {Promise<Object>} Statistics object
292
+ */
293
+ async getStatistics() {
294
+ await this.initialize();
295
+ return await this.database.getStatistics();
296
+ }
297
+
298
+ /**
299
+ * Get the number of value sets loaded into memory
300
+ * @returns {number} Number of unique value sets in map
301
+ */
302
+ getMapSize() {
303
+ const uniqueUrls = new Set();
304
+ for (const [key, valueSet] of this.valueSetMap.entries()) {
305
+ if (!key.includes('|')) { // Only count base URL keys
306
+ uniqueUrls.add(valueSet.url);
307
+ }
308
+ }
309
+ return uniqueUrls.size;
310
+ }
311
+
312
+ async fetchValueSetById(id) {
313
+ return this.valueSetMap.get(id);
314
+ }
315
+
316
+ // eslint-disable-next-line no-unused-vars
317
+ assignIds(ids) {
318
+ if (!this.spaceId) {
319
+ return;
320
+ }
321
+
322
+ const prefix = this.spaceId + '-';
323
+ const alreadyPrefixed = new Set();
324
+
325
+ // Get all current entries - we'll iterate and modify
326
+ const entries = Array.from(this.valueSetMap.entries());
327
+
328
+ for (const [key, vs] of entries) {
329
+ // Skip if we've already processed this ValueSet instance
330
+ if (alreadyPrefixed.has(vs)) {
331
+ continue;
332
+ }
333
+
334
+ // Update the id on the ValueSet itself
335
+ if (vs.id && !vs.id.startsWith(prefix)) {
336
+ const oldId = vs.id;
337
+ vs.id = prefix + oldId;
338
+
339
+ // Add to map under the new id as well
340
+ this.valueSetMap.set(vs.id, vs);
341
+ }
342
+
343
+ alreadyPrefixed.add(vs);
344
+ }
345
+ }
346
+
347
+ vsCount() {
348
+ return this.database.vsCount;
349
+ }
350
+
351
+ async listAllValueSets() {
352
+ return await this.database.listAllValueSets();
353
+ }
354
+ }
355
+
356
+ module.exports = {
357
+ PackageValueSetProvider
358
+ };
@@ -0,0 +1,366 @@
1
+ const path = require('path');
2
+ const axios = require('axios');
3
+ const { AbstractValueSetProvider } = require('./vs-api');
4
+ const { ValueSetDatabase } = require('./vs-database');
5
+ const { VersionUtilities } = require('../../library/version-utilities');
6
+
7
+ /**
8
+ * VSAC (Value Set Authority Center) ValueSet provider
9
+ * Fetches and caches ValueSets from the NLM VSAC FHIR server
10
+ */
11
+ class VSACValueSetProvider extends AbstractValueSetProvider {
12
+ /**
13
+ * @param {Object} config - Configuration object
14
+ * @param {string} config.apiKey - API key for VSAC authentication
15
+ * @param {string} config.cacheFolder - Local folder for cached database
16
+ * @param {number} [config.refreshIntervalHours=24] - Hours between refresh scans
17
+ * @param {string} [config.baseUrl='http://cts.nlm.nih.gov/fhir'] - Base URL for VSAC FHIR server
18
+ */
19
+ constructor(config) {
20
+ super();
21
+
22
+ if (!config.apiKey) {
23
+ throw new Error('API key is required');
24
+ }
25
+ if (!config.cacheFolder) {
26
+ throw new Error('Cache folder is required');
27
+ }
28
+
29
+ this.apiKey = config.apiKey;
30
+ this.cacheFolder = config.cacheFolder;
31
+ this.baseUrl = config.baseUrl || 'http://cts.nlm.nih.gov/fhir';
32
+ this.refreshIntervalHours = config.refreshIntervalHours || 24;
33
+
34
+ this.dbPath = path.join(config.cacheFolder, 'vsac-valuesets.db');
35
+ this.database = new ValueSetDatabase(this.dbPath);
36
+ this.valueSetMap = new Map();
37
+ this.initialized = false;
38
+ this.refreshTimer = null;
39
+ this.isRefreshing = false;
40
+ this.lastRefresh = null;
41
+
42
+ // HTTP client with authentication - manually create Basic auth header
43
+ const authString = Buffer.from(`apikey:${this.apiKey}`).toString('base64');
44
+ this.httpClient = axios.create({
45
+ baseURL: this.baseUrl,
46
+ timeout: 30000,
47
+ headers: {
48
+ 'Accept': 'application/fhir+json',
49
+ 'User-Agent': 'FHIR-ValueSet-Provider/1.0',
50
+ 'Authorization': `Basic ${authString}`
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Initialize the provider - setup database and start refresh cycle
57
+ * @returns {Promise<void>}
58
+ */
59
+ async initialize() {
60
+ if (this.initialized) {
61
+ return;
62
+ }
63
+
64
+ // Create database if it doesn't exist
65
+ if (!(await this.database.exists())) {
66
+ await this.database.create();
67
+ // Force initial refresh for new database
68
+ await this.refreshValueSets();
69
+ } else {
70
+ // Load existing data
71
+ await this._reloadMap();
72
+ }
73
+
74
+ // Start periodic refresh
75
+ this._startRefreshTimer();
76
+ this.initialized = true;
77
+ }
78
+
79
+ /**
80
+ * Start the periodic refresh timer
81
+ * @private
82
+ */
83
+ _startRefreshTimer() {
84
+ if (this.refreshTimer) {
85
+ clearInterval(this.refreshTimer);
86
+ }
87
+
88
+ const intervalMs = this.refreshIntervalHours * 60 * 60 * 1000;
89
+ this.refreshTimer = setInterval(async () => {
90
+ try {
91
+ await this.refreshValueSets();
92
+ } catch (error) {
93
+ this.log.error(error, 'Error during scheduled refresh:');
94
+ }
95
+ }, intervalMs);
96
+ }
97
+
98
+ /**
99
+ * Stop the refresh timer (for cleanup)
100
+ */
101
+ stopRefreshTimer() {
102
+ if (this.refreshTimer) {
103
+ clearInterval(this.refreshTimer);
104
+ this.refreshTimer = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Perform a full refresh of ValueSets from the server
110
+ * @returns {Promise<void>}
111
+ */
112
+ async refreshValueSets() {
113
+ if (this.isRefreshing) {
114
+ console.log('Refresh already in progress, skipping');
115
+ return;
116
+ }
117
+
118
+ this.isRefreshing = true;
119
+ const refreshStartTime = Math.floor(Date.now() / 1000);
120
+
121
+ try {
122
+ console.log('Starting VSAC ValueSet refresh...');
123
+
124
+ let totalFetched = 0;
125
+ let url = '/ValueSet?_offset=0&_count=100';
126
+
127
+ while (url) {
128
+ console.log(`Fetching page: ${url}`);
129
+ const bundle = await this._fetchBundle(url);
130
+
131
+ if (bundle.entry && bundle.entry.length > 0) {
132
+ // Extract ValueSets from bundle entries
133
+ const valueSets = bundle.entry
134
+ .filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
135
+ .map(entry => entry.resource);
136
+
137
+ if (valueSets.length > 0) {
138
+ await this.database.batchUpsertValueSets(valueSets);
139
+ totalFetched += valueSets.length;
140
+ console.log(`Processed ${valueSets.length} ValueSets (total: ${totalFetched})`);
141
+ }
142
+ }
143
+
144
+ // Find next link
145
+ url = this._getNextUrl(bundle);
146
+
147
+ // Safety check against infinite loops
148
+ if (bundle.total && totalFetched >= bundle.total) {
149
+ console.log(`Reached total count (${bundle.total}), stopping`);
150
+ break;
151
+ }
152
+ }
153
+
154
+ // Clean up old records
155
+ const deletedCount = await this.database.deleteOldValueSets(refreshStartTime);
156
+ if (deletedCount > 0) {
157
+ console.log(`Deleted ${deletedCount} old ValueSets`);
158
+ }
159
+
160
+ // Reload map with fresh data
161
+ await this._reloadMap();
162
+
163
+ this.lastRefresh = new Date();
164
+ console.log(`VSAC refresh completed. Total: ${totalFetched} ValueSets, Deleted: ${deletedCount}`);
165
+
166
+ } catch (error) {
167
+ console.log(error, 'Error during VSAC refresh:');
168
+ throw error;
169
+ } finally {
170
+ this.isRefreshing = false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Fetch a FHIR Bundle from the server
176
+ * @param {string} url - Relative URL to fetch
177
+ * @returns {Promise<Object>} FHIR Bundle
178
+ * @private
179
+ */
180
+ async _fetchBundle(url) {
181
+ try {
182
+ const response = await this.httpClient.get(url);
183
+
184
+ if (response.data && response.data.resourceType === 'Bundle') {
185
+ return response.data;
186
+ } else {
187
+ throw new Error('Response is not a FHIR Bundle');
188
+ }
189
+ } catch (error) {
190
+ if (error.response) {
191
+ throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
192
+ } else if (error.request) {
193
+ throw new Error('Network error: No response received');
194
+ } else {
195
+ throw new Error(`Request error: ${error.message}`);
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Extract the next URL from a FHIR Bundle's link array
202
+ * @param {Object} bundle - FHIR Bundle
203
+ * @returns {string|null} Next URL or null if no more pages
204
+ * @private
205
+ */
206
+ _getNextUrl(bundle) {
207
+ if (!bundle.link || !Array.isArray(bundle.link)) {
208
+ return null;
209
+ }
210
+
211
+ const nextLink = bundle.link.find(link => link.relation === 'next');
212
+ if (!nextLink || !nextLink.url) {
213
+ return null;
214
+ }
215
+
216
+ // Extract relative path from full URL
217
+ let s = nextLink.url;
218
+ s = s.replace(this.baseUrl, '');
219
+ return s;
220
+ }
221
+
222
+ /**
223
+ * Reload the in-memory map from database (thread-safe)
224
+ * @returns {Promise<void>}
225
+ * @private
226
+ */
227
+ async _reloadMap() {
228
+ const newMap = await this.database.loadAllValueSets("vsac");
229
+
230
+ // Atomic replacement of the map
231
+ this.valueSetMap = newMap;
232
+ }
233
+
234
+ /**
235
+ * Fetches a value set by URL and version
236
+ * @param {string} url - The canonical URL of the value set
237
+ * @param {string} version - The version of the value set
238
+ * @returns {Promise<Object>} The requested value set
239
+ */
240
+ async fetchValueSet(url, version) {
241
+ await this.initialize();
242
+ this._validateFetchParams(url, version);
243
+
244
+ // Try exact match first: url|version
245
+ let key = `${url}|${version}`;
246
+ if (this.valueSetMap.has(key)) {
247
+ return this.valueSetMap.get(key);
248
+ }
249
+
250
+ // If version is semver, try url|major.minor
251
+ try {
252
+ if (VersionUtilities.isSemVer(version)) {
253
+ const majorMinor = VersionUtilities.getMajMin(version);
254
+ if (majorMinor) {
255
+ key = `${url}|${majorMinor}`;
256
+ if (this.valueSetMap.has(key)) {
257
+ return this.valueSetMap.get(key);
258
+ }
259
+ }
260
+ }
261
+ } catch (error) {
262
+ // Ignore version parsing errors
263
+ }
264
+
265
+ // Finally try just the URL
266
+ if (this.valueSetMap.has(url)) {
267
+ return this.valueSetMap.get(url);
268
+ }
269
+
270
+ throw new Error(`Value set not found: ${url} version ${version}`);
271
+ }
272
+
273
+ /**
274
+ * Searches for value sets based on criteria
275
+ * @param {Array<{name: string, value: string}>} searchParams - Search criteria
276
+ * @returns {Promise<Array<Object>>} List of matching value sets
277
+ */
278
+ async searchValueSets(searchParams, elements) {
279
+ await this.initialize();
280
+ this._validateSearchParams(searchParams);
281
+
282
+ if (searchParams.length === 0) {
283
+ return [];
284
+ }
285
+
286
+ return await this.database.search(this.spaceId, this.valueSetMap, searchParams, elements);
287
+ }
288
+
289
+ /**
290
+ * Get statistics about the cached ValueSets
291
+ * @returns {Promise<Object>} Statistics object including refresh info
292
+ */
293
+ async getStatistics() {
294
+ await this.initialize();
295
+
296
+ const dbStats = await this.database.getStatistics();
297
+
298
+ return {
299
+ ...dbStats,
300
+ refreshInfo: {
301
+ lastRefresh: this.lastRefresh,
302
+ isRefreshing: this.isRefreshing,
303
+ refreshIntervalHours: this.refreshIntervalHours,
304
+ nextRefresh: this.refreshTimer && this.lastRefresh
305
+ ? new Date(this.lastRefresh.getTime() + (this.refreshIntervalHours * 60 * 60 * 1000))
306
+ : null
307
+ }
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Get the number of value sets loaded into memory
313
+ * @returns {number} Number of unique value sets in map
314
+ */
315
+ getMapSize() {
316
+ const uniqueUrls = new Set();
317
+ for (const [key, valueSet] of this.valueSetMap.entries()) {
318
+ if (!key.includes('|')) { // Only count base URL keys
319
+ uniqueUrls.add(valueSet.url);
320
+ }
321
+ }
322
+ return uniqueUrls.size;
323
+ }
324
+
325
+ /**
326
+ * Force a refresh (useful for testing or manual updates)
327
+ * @returns {Promise<void>}
328
+ */
329
+ async forceRefresh() {
330
+ await this.refreshValueSets();
331
+ }
332
+
333
+ /**
334
+ * Check if the provider is currently refreshing
335
+ * @returns {boolean} True if refresh is in progress
336
+ */
337
+ isCurrentlyRefreshing() {
338
+ return this.isRefreshing;
339
+ }
340
+
341
+ /**
342
+ * Get the last refresh timestamp
343
+ * @returns {Date|null} Last refresh date or null if never refreshed
344
+ */
345
+ getLastRefreshTime() {
346
+ return this.lastRefresh;
347
+ }
348
+
349
+ count() {
350
+ return this.database.vsCount;
351
+ }
352
+
353
+ async listAllValueSets() {
354
+ return await this.database.listAllValueSets();
355
+ }
356
+
357
+ async close() {
358
+ await this.database.close();
359
+ }
360
+
361
+ }
362
+
363
+ // Usage examples:
364
+ module.exports = {
365
+ VSACValueSetProvider
366
+ };