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,637 @@
1
+ // registry/crawler.js
2
+ // Crawler for gathering server information from terminology servers
3
+
4
+ const axios = require('axios');
5
+ const {
6
+ ServerRegistries,
7
+ ServerRegistry,
8
+ ServerInformation,
9
+ ServerVersionInformation,
10
+ } = require('./model');
11
+
12
+ const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
13
+
14
+ class RegistryCrawler {
15
+ log;
16
+
17
+ constructor(config = {}) {
18
+ this.config = {
19
+ timeout: config.timeout || 30000, // 30 seconds default
20
+ masterUrl: config.masterUrl || MASTER_URL,
21
+ userAgent: config.userAgent || 'HealthIntersections/FhirServer',
22
+ crawlInterval: config.crawlInterval || 5 * 60 * 1000, // 5 minutes default
23
+ apiKeys: config.apiKeys || {} // Map of server URL or code to API key
24
+ };
25
+
26
+ this.currentData = new ServerRegistries();
27
+ this.crawlTimer = null;
28
+ this.isCrawling = false;
29
+ this.errors = [];
30
+ this.totalBytes = 0;
31
+ this.log = console;
32
+ }
33
+
34
+ useLog(logv) {
35
+ this.log = logv;
36
+ }
37
+
38
+ /**
39
+ * Start the crawler with periodic updates
40
+ */
41
+ start() {
42
+ if (this.crawlTimer) {
43
+ return; // Already running
44
+ }
45
+
46
+ // Initial crawl
47
+ this.crawl();
48
+
49
+ // Set up periodic crawling
50
+ this.crawlTimer = setInterval(() => {
51
+ this.crawl();
52
+ }, this.config.crawlInterval);
53
+ }
54
+
55
+ /**
56
+ * Stop the crawler
57
+ */
58
+ stop() {
59
+ if (this.crawlTimer) {
60
+ clearInterval(this.crawlTimer);
61
+ this.crawlTimer = null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Main entry point - crawl the registry starting from the master URL
67
+ * @param {string} masterUrl - Optional override for the master URL
68
+ * @returns {Promise<ServerRegistries>} The populated registry data
69
+ */
70
+ async crawl(masterUrl = null) {
71
+ if (this.isCrawling) {
72
+ this.addLogEntry('warn', 'Crawl already in progress, skipping...');
73
+ return this.currentData;
74
+ }
75
+
76
+ this.isCrawling = true;
77
+ const startTime = new Date();
78
+ this.errors = [];
79
+ this.totalBytes = 0;
80
+
81
+ const url = masterUrl || this.config.masterUrl;
82
+
83
+ try {
84
+ this.addLogEntry('info', `Starting scan from ${url}`);
85
+
86
+ const newData = new ServerRegistries();
87
+ newData.address = url;
88
+ newData.lastRun = startTime;
89
+
90
+ // Fetch the master registry list
91
+ const masterJson = await this.fetchJson(url, 'master');
92
+
93
+ if (masterJson.formatVersion !== '1') {
94
+ throw new Error(`Unable to proceed: registries version is ${masterJson.formatVersion} not "1"`);
95
+ }
96
+
97
+ newData.doco = masterJson.documentation || '';
98
+
99
+ // Process each registry
100
+ const registries = masterJson.registries || [];
101
+ for (const registryConfig of registries) {
102
+ const registry = await this.processRegistry(registryConfig);
103
+ if (registry) {
104
+ newData.registries.push(registry);
105
+ }
106
+ }
107
+
108
+ newData.outcome = `Processed OK - ${this.formatBytes(this.totalBytes)}`;
109
+
110
+ // Update the current data
111
+ this.currentData = newData;
112
+ } catch (error) {
113
+ this.addLogEntry('error', 'Exception Scanning:', error);
114
+ this.currentData.outcome = `Error: ${error.message}`;
115
+ this.errors.push({
116
+ source: url,
117
+ error: error.message,
118
+ timestamp: new Date()
119
+ });
120
+ } finally {
121
+ this.isCrawling = false;
122
+ }
123
+
124
+ return this.currentData;
125
+ }
126
+
127
+ /**
128
+ * Process a single registry
129
+ */
130
+ async processRegistry(registryConfig) {
131
+ const registry = new ServerRegistry();
132
+ registry.code = registryConfig.code;
133
+ registry.name = registryConfig.name;
134
+ registry.authority = registryConfig.authority || '';
135
+ registry.address = registryConfig.url;
136
+
137
+ if (!registry.name) {
138
+ this.addLogEntry('error', 'No name provided for registry', registryConfig.url);
139
+ return registry;
140
+ }
141
+
142
+ if (!registry.address) {
143
+ this.addLogEntry('error', `No url provided for ${registry.name, registry.name}`, '');
144
+ return registry;
145
+ }
146
+
147
+ try {
148
+ this.addLogEntry('info', ` Registry ${registry.name} from ${registry.address}`);
149
+
150
+ const registryJson = await this.fetchJson(registry.address, registry.code);
151
+
152
+ if (registryJson.formatVersion !== '1') {
153
+ throw new Error(`Registry version at ${registry.address} is ${registryJson.formatVersion} not "1"`);
154
+ }
155
+
156
+ // Process each server in the registry
157
+ const servers = registryJson.servers || [];
158
+ for (const serverConfig of servers) {
159
+ const server = await this.processServer(serverConfig, registry.address);
160
+ if (server) {
161
+ registry.servers.push(server);
162
+ }
163
+ }
164
+
165
+ } catch (error) {
166
+ registry.error = error.message;
167
+ this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
168
+ }
169
+
170
+ return registry;
171
+ }
172
+
173
+ /**
174
+ * Process a single server
175
+ */
176
+ async processServer(serverConfig, source) {
177
+ const server = new ServerInformation();
178
+ server.code = serverConfig.code;
179
+ server.name = serverConfig.name;
180
+ server.address = serverConfig.url || '';
181
+ server.accessInfo = serverConfig.access_info || '';
182
+
183
+ if (!server.name) {
184
+ this.addLogEntry('error', 'No name provided for server', source);
185
+ return server;
186
+ }
187
+
188
+ if (!server.address) {
189
+ this.addLogEntry('error', `No url provided for ${server.name}`, source);
190
+ return server;
191
+ }
192
+
193
+ // Parse authoritative lists
194
+ server.authCSList = (serverConfig.authoritative || []).sort();
195
+ server.authVSList = (serverConfig['authoritative-valuesets'] || []).sort();
196
+ server.usageList = (serverConfig.usage || []).sort();
197
+
198
+ // Process each FHIR version
199
+ const fhirVersions = serverConfig.fhirVersions || [];
200
+ for (const versionConfig of fhirVersions) {
201
+ const version = await this.processServerVersion(versionConfig, server);
202
+ if (version) {
203
+ server.versions.push(version);
204
+ }
205
+ }
206
+
207
+ return server;
208
+ }
209
+
210
+ /**
211
+ * Process a single server version
212
+ */
213
+ async processServerVersion(versionConfig, server) {
214
+ const version = new ServerVersionInformation();
215
+ version.version = versionConfig.version;
216
+ version.address = versionConfig.url;
217
+ version.security = this.getApiKey(server.code) == null ? "open" : "api-key";
218
+
219
+ if (!version.address) {
220
+ this.addLogEntry('error', `No URL for version ${version.version} of ${server.name}`, server.address);
221
+ return version;
222
+ }
223
+
224
+ const startTime = Date.now();
225
+
226
+ try {
227
+ // this.addLogEntry('info', ` Server ${version.address} (${server.name})`);
228
+
229
+ // Determine FHIR version from version string
230
+ const majorVersion = this.getMajorVersion(version.version);
231
+
232
+ switch (majorVersion) {
233
+ case 3:
234
+ await this.processServerVersionR3(version, server);
235
+ break;
236
+ case 4:
237
+ await this.processServerVersionR4(version, server);
238
+ break;
239
+ case 5:
240
+ await this.processServerVersionR5(version, server);
241
+ break;
242
+ default:
243
+ throw new Error(`Version ${version.version} not supported`);
244
+ }
245
+
246
+ // Sort and deduplicate
247
+ version.codeSystems = [...new Set(version.codeSystems)].sort();
248
+ version.valueSets = [...new Set(version.valueSets)].sort();
249
+ version.lastSuccess = new Date();
250
+ version.lastTat = `${Date.now() - startTime}ms`;
251
+
252
+ this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
253
+
254
+ } catch (error) {
255
+ const elapsed = Date.now() - startTime;
256
+ this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
257
+ version.error = error.message;
258
+ version.lastTat = `${elapsed}ms`;
259
+ }
260
+
261
+ return version;
262
+ }
263
+
264
+ /**
265
+ * Process an R3 server
266
+ */
267
+ async processServerVersionR3(version, server) {
268
+ // Get capability statement
269
+ const capabilityUrl = `${version.address}/metadata`;
270
+ const capability = await this.fetchJson(capabilityUrl, server.name);
271
+
272
+ version.version = capability.fhirVersion || '3.0.2';
273
+ version.software = capability.software ? capability.software.name : "unknown";
274
+
275
+ // Get terminology capabilities (R3 uses Parameters resource)
276
+ try {
277
+ const termCapUrl = `${version.address}/metadata?mode=terminology`;
278
+ const termCap = await this.fetchJson(termCapUrl, server.name);
279
+
280
+ if (termCap.parameter) {
281
+ termCap.parameter.forEach(param => {
282
+ if (param.name === 'system') {
283
+ const uri = param.valueUri || param.valueString;
284
+ if (uri) {
285
+ version.codeSystems.push(uri);
286
+ // Look for version parts
287
+ if (param.part) {
288
+ param.part.forEach(part => {
289
+ if (part.name === 'version' && part.valueString) {
290
+ version.codeSystems.push(`${uri}|${part.valueString}`);
291
+ }
292
+ });
293
+ }
294
+ }
295
+ }
296
+ });
297
+ }
298
+ } catch (error) {
299
+ this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
300
+ }
301
+
302
+ // Search for value sets
303
+ await this.fetchValueSets(version, server);
304
+ }
305
+
306
+ /**
307
+ * Process an R4 server
308
+ */
309
+ async processServerVersionR4(version, server) {
310
+ // Get capability statement
311
+ const capabilityUrl = `${version.address}/metadata`;
312
+ const capability = await this.fetchJson(capabilityUrl, server.code);
313
+
314
+ version.version = capability.fhirVersion || '4.0.1';
315
+ version.software = capability.software ? capability.software.name : "unknown";
316
+
317
+ // Get terminology capabilities
318
+ try {
319
+ const termCapUrl = `${version.address}/metadata?mode=terminology`;
320
+ const termCap = await this.fetchJson(termCapUrl, server.code);
321
+
322
+ if (termCap.codeSystem) {
323
+ termCap.codeSystem.forEach(cs => {
324
+ if (cs.uri) {
325
+ version.codeSystems.push(cs.uri);
326
+ if (cs.version) {
327
+ cs.version.forEach(v => {
328
+ if (v.code) {
329
+ version.codeSystems.push(`${cs.uri}|${v.code}`);
330
+ }
331
+ });
332
+ }
333
+ }
334
+ });
335
+ }
336
+ } catch (error) {
337
+ this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
338
+ }
339
+
340
+ // Search for value sets
341
+ await this.fetchValueSets(version, server);
342
+ }
343
+
344
+ /**
345
+ * Process an R5 server
346
+ */
347
+ async processServerVersionR5(version, server) {
348
+ // R5 is essentially the same as R4 for our purposes
349
+ await this.processServerVersionR4(version, server);
350
+ version.version = version.version || '5.0.0';
351
+ }
352
+
353
+ /**
354
+ * Fetch value sets from the server
355
+ */
356
+ /**
357
+ * Fetch value sets with pagination support
358
+ * @param {Object} version - The server version information
359
+ * @param {Object} server - The server information
360
+ */
361
+ async fetchValueSets(version, server) {
362
+ // Initial search URL
363
+ let searchUrl = `${version.address}/ValueSet?_elements=url,version`;
364
+ try {
365
+ // Set of URLs to avoid duplicates
366
+ const valueSetUrls = new Set();
367
+
368
+
369
+ // Continue fetching while we have a URL
370
+ while (searchUrl) {
371
+ this.log.debug(`Fetching value sets from ${searchUrl}`);
372
+ const bundle = await this.fetchJson(searchUrl, server.code);
373
+
374
+ // Process entries in this page
375
+ if (bundle.entry) {
376
+ bundle.entry.forEach(entry => {
377
+ if (entry.resource) {
378
+ const vs = entry.resource;
379
+ if (vs.url) {
380
+ valueSetUrls.add(vs.url);
381
+ if (vs.version) {
382
+ valueSetUrls.add(`${vs.url}|${vs.version}`);
383
+ }
384
+ }
385
+ }
386
+ });
387
+ }
388
+
389
+ // Look for next link
390
+ searchUrl = null;
391
+ if (bundle.link) {
392
+ const nextLink = bundle.link.find(link => link.relation === 'next');
393
+ if (nextLink && nextLink.url) {
394
+ searchUrl = this.resolveUrl(nextLink.url, version.address);
395
+ }
396
+ }
397
+ }
398
+
399
+ // Convert set to array and sort
400
+ version.valueSets = Array.from(valueSetUrls).sort();
401
+
402
+ } catch (error) {
403
+ this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
404
+ }
405
+ }
406
+
407
+ resolveUrl(url, baseUrl) {
408
+ // Check if the URL is already absolute
409
+ if (url.startsWith('http://') || url.startsWith('https://')) {
410
+ return url;
411
+ }
412
+
413
+ // Get the base URL without any path
414
+ const baseUrlObj = new URL(baseUrl);
415
+ const base = `${baseUrlObj.protocol}//${baseUrlObj.host}`;
416
+
417
+ // If URL starts with a slash, it's relative to the root
418
+ if (url.startsWith('/')) {
419
+ return `${base}${url}`;
420
+ }
421
+
422
+ // Otherwise, it's relative to the base URL path
423
+ // Remove any query parameters or fragments from the base URL
424
+ const basePath = baseUrl.split('?')[0].split('#')[0];
425
+
426
+ // If base path ends with a slash, just append the URL
427
+ if (basePath.endsWith('/')) {
428
+ return `${basePath}${url}`;
429
+ }
430
+
431
+ // Otherwise, replace the last segment of the path
432
+ const basePathSegments = basePath.split('/');
433
+ basePathSegments.pop(); // Remove the last segment
434
+ return `${basePathSegments.join('/')}/${url}`;
435
+ }
436
+
437
+ /**
438
+ * Fetch JSON from a URL
439
+ */
440
+ async fetchJson(url, serverName) {
441
+ try {
442
+ // Add timestamp to bypass cache
443
+ const fetchUrl = url.includes('?')
444
+ ? `${url}&_ts=${Date.now()}`
445
+ : `${url}?_ts=${Date.now()}`;
446
+
447
+ // Get API key if configured
448
+ const apiKey = this.getApiKey(serverName);
449
+ const headers = {
450
+ 'Accept': 'application/json, application/fhir+json',
451
+ 'User-Agent': this.config.userAgent
452
+ };
453
+
454
+ if (apiKey) {
455
+ headers['Api-Key'] = apiKey;
456
+ }
457
+
458
+ const response = await axios.get(fetchUrl, {
459
+ timeout: this.config.timeout,
460
+ headers: headers,
461
+ validateStatus: (status) => status < 500 // Don't throw on 4xx
462
+ });
463
+
464
+ if (response.status >= 400) {
465
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
466
+ }
467
+
468
+ // Track bytes downloaded
469
+ const contentLength = response.headers['content-length'];
470
+ if (contentLength) {
471
+ this.totalBytes += parseInt(contentLength);
472
+ } else if (response.data) {
473
+ this.totalBytes += JSON.stringify(response.data).length;
474
+ }
475
+
476
+ return response.data;
477
+
478
+ } catch (error) {
479
+ if (error.response) {
480
+ throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
481
+ } else if (error.request) {
482
+ throw new Error(`No response from server: ${error.message}`);
483
+ } else {
484
+ throw error;
485
+ }
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Get API key for a given URL
491
+ */
492
+ getApiKey(name) {
493
+ // Check for exact URL match
494
+ if (this.config.apiKeys[name]) {
495
+ return this.config.apiKeys[name];
496
+ }
497
+
498
+ return null;
499
+ }
500
+
501
+ /**
502
+ * Get major version from version string
503
+ * Handles formats like:
504
+ * - 3.0.1, 4.0, 5.0.0
505
+ * - 3, 4, 5
506
+ * - R3, R4, R4B, R5
507
+ * - r3, r4, r4b, r5
508
+ */
509
+ getMajorVersion(versionString) {
510
+ if (!versionString) return 0;
511
+
512
+ // Convert to string and uppercase for consistent handling
513
+ const version = String(versionString).toUpperCase();
514
+
515
+ // Case 1: Check for R followed by a digit (e.g., R3, R4, R4B)
516
+ const rMatch = version.match(/^R(\d+)/);
517
+ if (rMatch) {
518
+ return parseInt(rMatch[1]);
519
+ }
520
+
521
+ // Case 2: Check for digits at the start, possibly followed by period
522
+ const numMatch = version.match(/^(\d+)(?:\.|\b)/);
523
+ if (numMatch) {
524
+ return parseInt(numMatch[1]);
525
+ }
526
+
527
+ // No valid version found
528
+ return 0;
529
+ }
530
+
531
+ /**
532
+ * Format bytes for display
533
+ */
534
+ formatBytes(bytes) {
535
+ if (bytes < 1024) return `${bytes} bytes`;
536
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
537
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
538
+ }
539
+
540
+ /**
541
+ * Get the current registry data
542
+ */
543
+ getData() {
544
+ return this.currentData;
545
+ }
546
+
547
+ /**
548
+ * Get crawl metadata
549
+ */
550
+ getMetadata() {
551
+ return {
552
+ lastRun: this.currentData.lastRun,
553
+ outcome: this.currentData.outcome,
554
+ errors: this.errors,
555
+ totalBytes: this.totalBytes,
556
+ isCrawling: this.isCrawling
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Load data from JSON
562
+ */
563
+ loadData(json) {
564
+ this.currentData = ServerRegistries.fromJSON(json);
565
+ }
566
+
567
+ /**
568
+ * Save data to JSON
569
+ */
570
+ saveData() {
571
+ return this.currentData.toJSON();
572
+ }
573
+
574
+ /**
575
+ * Add log entry to the crawler's log history
576
+ * @param {string} level - Log level (info, error, warn, debug)
577
+ * @param {string} message - Log message
578
+ * @param {string} source - Source of the log
579
+ */
580
+ addLogEntry(level, message, source = '') {
581
+ // Create log entry
582
+ const entry = {
583
+ timestamp: new Date(),
584
+ level,
585
+ message,
586
+ source
587
+ };
588
+
589
+ // Initialize logs array if it doesn't exist
590
+ if (!this.logs) {
591
+ this.logs = [];
592
+ }
593
+
594
+ // Add to logs
595
+ this.logs.push(entry);
596
+
597
+ // Keep only the latest 1000 entries to avoid memory issues
598
+ if (this.logs.length > 1000) {
599
+ this.logs = this.logs.slice(-1000);
600
+ }
601
+
602
+ // Also output to the configured logger
603
+ if (this.log) {
604
+ if (level === 'error') {
605
+ this.log.error(message, source);
606
+ } else if (level === 'warn') {
607
+ this.log.warn(message, source);
608
+ } else if (level === 'debug') {
609
+ this.log.debug(message, source);
610
+ } else {
611
+ this.log.info(message, source);
612
+ }
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Get the log history
618
+ * @param {number} limit - Maximum number of entries to return
619
+ * @param {string} level - Filter by log level
620
+ * @returns {Array} Array of log entries
621
+ */
622
+ getLogs(limit = 100)
623
+ {
624
+ if (!this.logs) {
625
+ return [];
626
+ }
627
+
628
+ // Filter by level if specified
629
+ let filteredLogs = this.logs;
630
+
631
+ // Get the latest entries up to the limit
632
+ return filteredLogs.slice(-limit);
633
+ }
634
+
635
+ }
636
+
637
+ module.exports = RegistryCrawler;