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
+ // Enhanced registry-api.js with resolver and HTML rendering functions
2
+
3
+ const { ServerRegistryUtilities } = require('./model');
4
+
5
+ class RegistryAPI {
6
+ constructor(crawler) {
7
+ this.crawler = crawler;
8
+ }
9
+
10
+ /**
11
+ * Build rows for code system queries
12
+ * Matches the Pascal buildRowsCS functionality
13
+ */
14
+ buildRowsForCodeSystem(params = {}) {
15
+ const {
16
+ registryCode = '',
17
+ serverCode = '',
18
+ version = '',
19
+ codeSystem = ''
20
+ } = params;
21
+
22
+ const rows = [];
23
+ const data = this.crawler.getData();
24
+
25
+ // Lock for thread safety during read
26
+ data.lock('buildRowsCS');
27
+ try {
28
+ data.registries.forEach(registry => {
29
+ if (registryCode && registry.code !== registryCode) return;
30
+
31
+ registry.servers.forEach(server => {
32
+ if (serverCode && server.code !== serverCode) return;
33
+
34
+ // Check if server is authoritative for this code system
35
+ const isAuth = codeSystem ? ServerRegistryUtilities.hasMatchingCodeSystem(
36
+ codeSystem,
37
+ server.authCSList,
38
+ true // support wildcards
39
+ ) : false;
40
+
41
+ server.versions.forEach(versionInfo => {
42
+ if (version && !ServerRegistryUtilities.versionMatches(version, versionInfo.version)) {
43
+ return;
44
+ }
45
+
46
+ // Always skip servers with errors - they can't serve requests
47
+ if (versionInfo.error) {
48
+ return;
49
+ }
50
+
51
+ // Include if:
52
+ // 1. Authoritative for the requested code system
53
+ // 2. No filter specified
54
+ // 3. Has the code system in its list
55
+ if (isAuth ||
56
+ !codeSystem ||
57
+ (codeSystem && ServerRegistryUtilities.hasMatchingCodeSystem(
58
+ codeSystem,
59
+ versionInfo.codeSystems,
60
+ false // no wildcards for actual content
61
+ ))) {
62
+ const row = ServerRegistryUtilities.createRow(
63
+ registry,
64
+ server,
65
+ versionInfo,
66
+ isAuth
67
+ );
68
+ rows.push(row);
69
+ }
70
+ });
71
+ });
72
+ });
73
+ } finally {
74
+ data.unlock();
75
+ }
76
+
77
+ return this._sortAndRankRows(rows);
78
+ }
79
+
80
+ /**
81
+ * Build rows for value set queries
82
+ * Matches the Pascal buildRowsVS functionality
83
+ */
84
+ buildRowsForValueSet(params = {}) {
85
+ const {
86
+ registryCode = '',
87
+ serverCode = '',
88
+ version = '',
89
+ valueSet = ''
90
+ } = params;
91
+
92
+ const rows = [];
93
+ const data = this.crawler.getData();
94
+
95
+ data.lock('buildRowsVS');
96
+ try {
97
+ data.registries.forEach(registry => {
98
+ if (registryCode && registry.code !== registryCode) return;
99
+
100
+ registry.servers.forEach(server => {
101
+ if (serverCode && server.code !== serverCode) return;
102
+
103
+ // Check if server is authoritative for this value set
104
+ const isAuth = valueSet ? ServerRegistryUtilities.hasMatchingValueSet(
105
+ valueSet,
106
+ server.authVSList,
107
+ true // support wildcards
108
+ ) : false;
109
+
110
+ server.versions.forEach(versionInfo => {
111
+ if (version && !ServerRegistryUtilities.versionMatches(version, versionInfo.version)) {
112
+ return;
113
+ }
114
+
115
+ // Always skip servers with errors - they can't serve requests
116
+ if (versionInfo.error) {
117
+ return;
118
+ }
119
+
120
+ // Include if:
121
+ // 1. No filter specified
122
+ // 2. Authoritative for the value set (even via wildcard)
123
+ // 3. Has the value set in its list
124
+ let includeRow = false;
125
+
126
+ if (!valueSet) {
127
+ // No filter, include all working servers
128
+ includeRow = true;
129
+ } else {
130
+ // Check if actually has the value set
131
+ const hasValueSet = ServerRegistryUtilities.hasMatchingValueSet(
132
+ valueSet,
133
+ versionInfo.valueSets,
134
+ false // no wildcards for actual content
135
+ );
136
+
137
+ // Include if authoritative OR has the value set
138
+ // This matches the Pascal logic: if auth or hasMatchingValueSet
139
+ if (isAuth || hasValueSet) {
140
+ includeRow = true;
141
+ }
142
+ }
143
+
144
+ if (includeRow) {
145
+ const row = ServerRegistryUtilities.createRow(
146
+ registry,
147
+ server,
148
+ versionInfo,
149
+ isAuth
150
+ );
151
+ rows.push(row);
152
+ }
153
+ });
154
+ });
155
+ });
156
+ } finally {
157
+ data.unlock();
158
+ }
159
+
160
+ return this._sortAndRankRows(rows);
161
+ }
162
+
163
+ /**
164
+ * Get all available registries
165
+ */
166
+ getRegistries() {
167
+ const data = this.crawler.getData();
168
+ return data.registries.map(r => ({
169
+ code: r.code,
170
+ name: r.name,
171
+ address: r.address,
172
+ authority: r.authority,
173
+ error: r.error,
174
+ serverCount: r.servers.length
175
+ }));
176
+ }
177
+
178
+ /**
179
+ * Get all servers for a registry
180
+ */
181
+ getServers(registryCode) {
182
+ const data = this.crawler.getData();
183
+ const registry = data.getRegistry(registryCode);
184
+
185
+ if (!registry) {
186
+ return null;
187
+ }
188
+
189
+ return registry.servers.map(s => ({
190
+ code: s.code,
191
+ name: s.name,
192
+ address: s.address,
193
+ description: s.getDescription(),
194
+ details: s.getDetails(),
195
+ versionCount: s.versions.length,
196
+ authCSCount: s.authCSList.length,
197
+ authVSCount: s.authVSList.length,
198
+ usageTags: s.usageList
199
+ }));
200
+ }
201
+
202
+ /**
203
+ * Get server details
204
+ */
205
+ getServerDetails(registryCode, serverCode) {
206
+ const data = this.crawler.getData();
207
+ const registry = data.getRegistry(registryCode);
208
+
209
+ if (!registry) {
210
+ return null;
211
+ }
212
+
213
+ const server = registry.getServer(serverCode);
214
+ if (!server) {
215
+ return null;
216
+ }
217
+
218
+ return {
219
+ ...server.toJSON(),
220
+ versions: server.versions.map(v => ({
221
+ ...v.toJSON(),
222
+ details: v.getDetails(),
223
+ csList: v.getCsListHtml(),
224
+ vsList: v.getVsListHtml()
225
+ }))
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Get statistics about the registry
231
+ */
232
+ getStatistics() {
233
+ const data = this.crawler.getData();
234
+
235
+ let totalServers = 0;
236
+ let totalVersions = 0;
237
+ let totalCodeSystems = new Set();
238
+ let totalValueSets = new Set();
239
+ let errorCount = 0;
240
+ let workingVersions = 0;
241
+
242
+ data.registries.forEach(registry => {
243
+ if (registry.error) errorCount++;
244
+
245
+ registry.servers.forEach(server => {
246
+ totalServers++;
247
+
248
+ server.versions.forEach(version => {
249
+ totalVersions++;
250
+ if (version.error) {
251
+ errorCount++;
252
+ } else {
253
+ workingVersions++;
254
+ }
255
+
256
+ version.codeSystems.forEach(cs => totalCodeSystems.add(cs));
257
+ version.valueSets.forEach(vs => totalValueSets.add(vs));
258
+ });
259
+ });
260
+ });
261
+
262
+ return {
263
+ lastRun: data.lastRun,
264
+ outcome: data.outcome,
265
+ registryCount: data.registries.length,
266
+ serverCount: totalServers,
267
+ versionCount: totalVersions,
268
+ workingVersions: workingVersions,
269
+ uniqueCodeSystems: totalCodeSystems.size,
270
+ uniqueValueSets: totalValueSets.size,
271
+ errorCount: errorCount
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Sort and rank rows based on various criteria
277
+ */
278
+ _sortAndRankRows(rows) {
279
+ return rows.sort((a, b) => {
280
+ // 1. Authoritative servers first
281
+ if (a.authoritative !== b.authoritative) {
282
+ return a.authoritative ? -1 : 1;
283
+ }
284
+
285
+ // 2. No errors before errors
286
+ const aHasError = a.error !== '';
287
+ const bHasError = b.error !== '';
288
+ if (aHasError !== bHasError) {
289
+ return aHasError ? 1 : -1;
290
+ }
291
+
292
+ // 3. More recent success first (smaller lastSuccess value)
293
+ if (a.lastSuccess !== b.lastSuccess) {
294
+ // If one has never succeeded, put it last
295
+ if (a.lastSuccess === 0) return 1;
296
+ if (b.lastSuccess === 0) return -1;
297
+ return a.lastSuccess - b.lastSuccess;
298
+ }
299
+
300
+ // 4. More resources is better
301
+ const aResources = a.systems + a.sets;
302
+ const bResources = b.systems + b.sets;
303
+ if (aResources !== bResources) {
304
+ return bResources - aResources;
305
+ }
306
+
307
+ // 5. Prefer newer versions
308
+ const versionCompare = this._compareVersions(b.version, a.version);
309
+ if (versionCompare !== 0) {
310
+ return versionCompare;
311
+ }
312
+
313
+ // 6. Alphabetical by server name as tie-breaker
314
+ return a.serverName.localeCompare(b.serverName);
315
+ });
316
+ }
317
+
318
+ _normalizeFhirVersion(version) {
319
+ if (!version) return version;
320
+
321
+ // Convert R4 or r4 to 4.0, R5 or r5 to 5.0, etc.
322
+ const rMatch = /^[rR](\d+)$/.exec(version);
323
+ if (rMatch) {
324
+ return `${rMatch[1]}.0`;
325
+ }
326
+
327
+ return version;
328
+ }
329
+
330
+ /**
331
+ * Compare semantic versions
332
+ */
333
+ _compareVersions(v1, v2) {
334
+ const parts1 = v1.split('.').map(p => parseInt(p) || 0);
335
+ const parts2 = v2.split('.').map(p => parseInt(p) || 0);
336
+
337
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
338
+ const p1 = parts1[i] || 0;
339
+ const p2 = parts2[i] || 0;
340
+ if (p1 !== p2) {
341
+ return p1 - p2;
342
+ }
343
+ }
344
+
345
+ return 0;
346
+ }
347
+
348
+ /**
349
+ * Find best server for a given code system/value set
350
+ */
351
+ findBestServer(type, url, version) {
352
+ let rows;
353
+
354
+ if (type === 'codesystem') {
355
+ rows = this.buildRowsForCodeSystem({ codeSystem: url, version });
356
+ } else if (type === 'valueset') {
357
+ rows = this.buildRowsForValueSet({ valueSet: url, version });
358
+ } else {
359
+ throw new Error(`Unknown type: ${type}`);
360
+ }
361
+
362
+ if (rows.length === 0) {
363
+ return null;
364
+ }
365
+
366
+ // Return the top-ranked server
367
+ return rows[0];
368
+ }
369
+
370
+ /**
371
+ * Get the current data (for direct access)
372
+ */
373
+ getData() {
374
+ return this.crawler.getData();
375
+ }
376
+
377
+ /**
378
+ * Express middleware for handling API requests
379
+ */
380
+ expressMiddleware() {
381
+ return (req, res, next) => {
382
+ // Attach API instance to request
383
+ req.registryAPI = this;
384
+ next();
385
+ };
386
+ }
387
+
388
+ /**
389
+ * NEW FUNCTION: Resolve the best server for a code system
390
+ * Based on Pascal resolveCS function
391
+ */
392
+ resolveCodeSystem(fhirVersion, codeSystem, authoritativeOnly, usage = '') {
393
+ if (!fhirVersion) {
394
+ throw new Error('A FHIR version is required');
395
+ }
396
+ if (!codeSystem) {
397
+ throw new Error('A code system URL is required');
398
+ }
399
+ const normalizedVersion = this._normalizeFhirVersion(fhirVersion);
400
+
401
+ const result = {
402
+ formatVersion: '1',
403
+ 'registry-url': this.getData().address,
404
+ authoritative: [],
405
+ candidates: []
406
+ };
407
+
408
+ const matchedServers = [];
409
+ const data = this.crawler.getData();
410
+
411
+ // Extract base code system URL (before any pipe)
412
+ let baseCodeSystem = codeSystem;
413
+ if (codeSystem.includes('|')) {
414
+ baseCodeSystem = codeSystem.substring(0, codeSystem.indexOf('|'));
415
+ }
416
+
417
+ // Lock for thread safety during read
418
+ data.lock('resolveCS');
419
+ try {
420
+ data.registries.forEach(registry => {
421
+ registry.servers.forEach(server => {
422
+ let added = false;
423
+
424
+ // Check if server supports the requested usage tag
425
+ if (server.usageList.length === 0 ||
426
+ (usage && server.usageList.includes(usage))) {
427
+
428
+ // Check if server is authoritative for this code system
429
+ const isAuth = server.isAuthCS(codeSystem);
430
+
431
+ server.versions.forEach(version => {
432
+ if (ServerRegistryUtilities.versionMatches(normalizedVersion, version.version)) {
433
+ // Check if the server has the code system
434
+ // Test against both the full URL and the base URL
435
+ const hasMatchingCS =
436
+ ServerRegistryUtilities.hasMatchingCodeSystem(baseCodeSystem, version.codeSystems, false) ||
437
+ (baseCodeSystem !== codeSystem &&
438
+ ServerRegistryUtilities.hasMatchingCodeSystem(codeSystem, version.codeSystems, false));
439
+
440
+ if (hasMatchingCS) {
441
+ if (isAuth) {
442
+ result.authoritative.push(this.createServerEntry(server, version));
443
+ } else if (!authoritativeOnly) {
444
+ result.candidates.push(this.createServerEntry(server, version));
445
+ }
446
+ added = true;
447
+ }
448
+ }
449
+ });
450
+
451
+ if (added) {
452
+ matchedServers.push(server.code);
453
+ }
454
+ }
455
+ });
456
+ });
457
+
458
+ // NEW: Fallback - if no matches found, check for authoritative pattern matches
459
+ if (result.authoritative.length === 0 && result.candidates.length === 0) {
460
+ data.registries.forEach(registry => {
461
+ registry.servers.forEach(server => {
462
+ // Check if server supports the requested usage tag
463
+ if (server.usageList.length === 0 ||
464
+ (usage && server.usageList.includes(usage))) {
465
+
466
+ // Check if server is authoritative for this code system
467
+ const isAuth = server.isAuthCS(codeSystem);
468
+
469
+ if (isAuth) {
470
+ server.versions.forEach(version => {
471
+ if (ServerRegistryUtilities.versionMatches(normalizedVersion, version.version)) {
472
+ result.authoritative.push(this.createServerEntry(server, version));
473
+ if (!matchedServers.includes(server.code)) {
474
+ matchedServers.push(server.code);
475
+ }
476
+ }
477
+ });
478
+ }
479
+ }
480
+ });
481
+ });
482
+ }
483
+ } finally {
484
+ data.unlock();
485
+ }
486
+
487
+ return {
488
+ result : this._cleanEmptyArrays(result),
489
+ matches: matchedServers.length > 0 ? matchedServers.join(',') : '--'
490
+ };
491
+ }
492
+
493
+ /**
494
+ * NEW FUNCTION: Resolve the best server for a value set
495
+ * Based on Pascal resolveVS function
496
+ */
497
+ resolveValueSet(fhirVersion, valueSet, authoritativeOnly, usage = '') {
498
+ if (!fhirVersion) {
499
+ throw new Error('A FHIR version is required');
500
+ }
501
+ if (!valueSet) {
502
+ throw new Error('A value set URL is required');
503
+ }
504
+
505
+ const normalizedVersion = this._normalizeFhirVersion(fhirVersion);
506
+
507
+ const result = {
508
+ formatVersion: '1',
509
+ 'registry-url': this.getData().address,
510
+ authoritative: [],
511
+ candidates: []
512
+ };
513
+
514
+ const matchedServers = [];
515
+ const data = this.crawler.getData();
516
+
517
+ // Extract base value set URL (before any pipe)
518
+ let baseValueSet = valueSet;
519
+ if (valueSet.includes('|')) {
520
+ baseValueSet = valueSet.substring(0, valueSet.indexOf('|'));
521
+ }
522
+
523
+ // Lock for thread safety during read
524
+ data.lock('resolveVS');
525
+ try {
526
+ data.registries.forEach(registry => {
527
+ registry.servers.forEach(server => {
528
+ let added = false;
529
+
530
+ // Check if server supports the requested usage tag
531
+ if (server.usageList.length === 0 ||
532
+ (usage && server.usageList.includes(usage))) {
533
+
534
+ // Check if server is authoritative for this value set
535
+ const isAuth = server.isAuthVS(baseValueSet);
536
+
537
+ server.versions.forEach(version => {
538
+ if (ServerRegistryUtilities.versionMatches(normalizedVersion, version.version)) {
539
+ // For authoritative servers, we don't need to check if they have the value set
540
+ if (isAuth) {
541
+ result.authoritative.push(this.createServerEntry(server, version));
542
+ added = true;
543
+ }
544
+ // For non-authoritative servers, check if they have the value set
545
+ else if (ServerRegistryUtilities.hasMatchingValueSet(baseValueSet, version.valueSets, false) ||
546
+ (baseValueSet !== valueSet &&
547
+ ServerRegistryUtilities.hasMatchingValueSet(valueSet, version.valueSets, false))) {
548
+ if (!authoritativeOnly) {
549
+ result.candidates.push(this.createServerEntry(server, version));
550
+ }
551
+ added = true;
552
+ }
553
+ }
554
+ });
555
+
556
+ if (added) {
557
+ matchedServers.push(server.code);
558
+ }
559
+ }
560
+ });
561
+ });
562
+ } finally {
563
+ data.unlock();
564
+ }
565
+
566
+ return {
567
+ result : this._cleanEmptyArrays(result),
568
+ matches: matchedServers.length > 0 ? matchedServers.join(',') : '--'
569
+ };
570
+ }
571
+
572
+ _cleanEmptyArrays(result) {
573
+ const cleanedResult = { ...result };
574
+
575
+ // Remove empty arrays
576
+ Object.keys(cleanedResult).forEach(key => {
577
+ if (Array.isArray(cleanedResult[key]) && cleanedResult[key].length === 0) {
578
+ delete cleanedResult[key];
579
+ }
580
+ });
581
+
582
+ return cleanedResult;
583
+ }
584
+
585
+ /**
586
+ * Helper function to create a server entry for resolve results
587
+ */
588
+ createServerEntry(server, version) {
589
+ const entry = {
590
+ 'server-name': server.name,
591
+ url: version.address
592
+ };
593
+
594
+ if (version.security) {
595
+ entry.security = version.security;
596
+ }
597
+ if (server.accessInfo) {
598
+ entry.access_info = server.accessInfo;
599
+ }
600
+
601
+ return entry;
602
+ }
603
+
604
+ /**
605
+ * NEW FUNCTION: Render a JSON result as an HTML table
606
+ * Based on Pascal renderJson function
607
+ */
608
+ renderJsonToHtml(json, path, regCode = '', serverCode = '', versionCode = '') {
609
+ let html = '<table class="grid">\n';
610
+ html += '<tr>\n';
611
+
612
+ if (!regCode) {
613
+ html += '<td><b>Registry</b></td>\n';
614
+ }
615
+ if (!serverCode) {
616
+ html += '<td><b>Server</b></td>\n';
617
+ }
618
+ if (!versionCode) {
619
+ html += '<td><b>FHIR Version</b></td>\n';
620
+ }
621
+
622
+ html += '<td><b>Url</b></td>\n';
623
+ html += '<td><b>Status</b></td>\n';
624
+ html += '<td><b>Content</b></td>\n';
625
+ html += '<td><b>Authoritative</b></td>\n';
626
+ html += '<td><b>Security</b></td>\n';
627
+ html += '</tr>\n';
628
+
629
+ const results = json.results || [];
630
+ for (const row of results) {
631
+ html += '<tr>\n';
632
+
633
+ if (!regCode) {
634
+ html += `<td><a href="${path}&registry=${row['registry-code']}">${this._escapeHtml(row['registry-name'])}</a></td>\n`;
635
+ }
636
+ if (!serverCode) {
637
+ html += `<td><a href="${path}&server=${row['server-code']}">${this._escapeHtml(row['server-name'])}</a></td>\n`;
638
+ }
639
+ if (!versionCode) {
640
+ html += `<td><a href="${path}&fhirVersion=${row.fhirVersion}">${row.fhirVersion}</a></td>\n`;
641
+ }
642
+
643
+ html += `<td><a href="${this._escapeHtml(row.url)}">${this._escapeHtml(row.url)}</a></td>\n`;
644
+
645
+ if (row.error) {
646
+ html += `<td><span style="color: maroon">Error: ${this._escapeHtml(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
647
+ } else {
648
+ html += `<td>Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
649
+ }
650
+
651
+ html += `<td>${row.systems} systems</td>\n`;
652
+
653
+ html += '<td>';
654
+ if (row['is-authoritative']) {
655
+ html += 'true';
656
+ }
657
+ html += '</td>\n';
658
+
659
+ html += `<td>${row.security}/td>\n`;
660
+
661
+ html += '</tr>\n';
662
+ }
663
+
664
+ html += '</table>\n';
665
+ return html;
666
+ }
667
+
668
+ /**
669
+ * NEW FUNCTION: Render registry info as HTML
670
+ * Based on Pascal renderInfo function
671
+ */
672
+ renderInfoToHtml() {
673
+ const data = this.crawler.getData();
674
+ let html = '<table class="grid">';
675
+
676
+ html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif">&nbsp;Registries</td><td>${data.address} (${this._escapeHtml(data.outcome)})</td></tr>`;
677
+
678
+ data.registries.forEach(registry => {
679
+ if (registry.error) {
680
+ html += `<tr><td title="${this._escapeHtml(registry.name)}">&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a>. Error: ${this._escapeHtml(registry.error)}</td></tr>`;
681
+ } else {
682
+ html += `<tr><td title="${this._escapeHtml(registry.name)}">&nbsp;&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a></td></tr>`;
683
+ }
684
+
685
+ registry.servers.forEach(server => {
686
+ if (server.authCSList.length > 0 || server.authVSList.length > 0 || server.usageList.length > 0) {
687
+ html += `<tr><td title="${this._escapeHtml(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a>. ${server.description}</td></tr>`;
688
+ } else {
689
+ html += `<tr><td title="${this._escapeHtml(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a></td></tr>`;
690
+ }
691
+
692
+ server.versions.forEach(version => {
693
+ // Get major.minor version only
694
+ const versionParts = version.version.split('.');
695
+ const majorMinor = versionParts.slice(0, 2).join('.');
696
+
697
+ html += `<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-version.png">&nbsp;v${majorMinor}</td><td><a href="${this._escapeHtml(version.address)}">${this._escapeHtml(version.address)}</a>. Status: ${this._escapeHtml(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
698
+ });
699
+ });
700
+ });
701
+
702
+ html += '</table>';
703
+ return html;
704
+ }
705
+
706
+ /**
707
+ * Helper function to format a duration in seconds to a human-readable string
708
+ */
709
+ _formatDuration(seconds) {
710
+ if (seconds < 60) {
711
+ return `${seconds} seconds`;
712
+ } else if (seconds < 3600) {
713
+ return `${Math.floor(seconds / 60)} minutes`;
714
+ } else if (seconds < 86400) {
715
+ return `${Math.floor(seconds / 3600)} hours`;
716
+ } else {
717
+ return `${Math.floor(seconds / 86400)} days`;
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Helper function to escape HTML special characters
723
+ */
724
+ _escapeHtml(text) {
725
+ if (!text) return '';
726
+ return text
727
+ .replace(/&/g, '&amp;')
728
+ .replace(/</g, '&lt;')
729
+ .replace(/>/g, '&gt;')
730
+ .replace(/"/g, '&quot;')
731
+ .replace(/'/g, '&#039;');
732
+ }
733
+ }
734
+
735
+ module.exports = RegistryAPI;