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,876 @@
1
+ /**
2
+ * PackageManager - FHIR Package management with caching
3
+ * Fetches and caches FHIR packages from package servers
4
+ */
5
+
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+ const https = require('https');
9
+ const http = require('http');
10
+ const { URL } = require('url');
11
+ const zlib = require('zlib');
12
+ const tar = require('tar');
13
+ const axios = require('axios');
14
+ const { VersionUtilities } = require('../library/version-utilities');
15
+
16
+ const DEFAULT_ROOT_URL = 'https://build.fhir.org';
17
+ const DEFAULT_CI_QUERY_INTERVAL = 1000 * 60 * 60; // 1 hour
18
+
19
+ class CIBuildClient {
20
+ /**
21
+ * @param {string} rootUrl - Base URL for CI build server
22
+ * @param {number} ciQueryInterval - Interval between server queries in ms
23
+ */
24
+ constructor(rootUrl = DEFAULT_ROOT_URL, ciQueryInterval = DEFAULT_CI_QUERY_INTERVAL) {
25
+ this.rootUrl = rootUrl;
26
+ this.ciQueryInterval = ciQueryInterval;
27
+ this.ciLastQueriedTimeStamp = 0;
28
+ this.ciBuildInfo = null;
29
+
30
+ // key = packageId, value = url of built package on build.fhir.org/ig/
31
+ this.ciPackageUrls = new Map();
32
+ }
33
+
34
+ /**
35
+ * Get package ID from canonical URL
36
+ * @param {string} canonical - Canonical URL
37
+ * @returns {Promise<string|null>} Package ID or null
38
+ */
39
+ async getPackageId(canonical) {
40
+ if (!canonical) {
41
+ return null;
42
+ }
43
+
44
+ await this.checkCIServerQueried();
45
+
46
+ if (this.ciBuildInfo) {
47
+ // First pass: exact match
48
+ for (const o of this.ciBuildInfo) {
49
+ if (canonical === o.url) {
50
+ return o['package-id'];
51
+ }
52
+ }
53
+
54
+ // Second pass: starts with canonical + /ImplementationGuide/
55
+ for (const o of this.ciBuildInfo) {
56
+ if (o.url && o.url.startsWith(canonical + '/ImplementationGuide/')) {
57
+ return o['package-id'];
58
+ }
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Get package URL from package ID
67
+ * @param {string} packageId - Package ID
68
+ * @returns {Promise<string|null>} Package URL or null
69
+ */
70
+ async getPackageUrl(packageId) {
71
+ await this.checkCIServerQueried();
72
+
73
+ for (const o of this.ciBuildInfo || []) {
74
+ if (packageId === o['package-id']) {
75
+ return o.url;
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Check if local package is current with CI build
84
+ * @param {string} id - Package ID
85
+ * @param {Object} npmPackage - Local npm package with date() method
86
+ * @returns {Promise<boolean>} True if current
87
+ */
88
+ async isCurrent(id, npmPackage) {
89
+ await this.checkCIServerQueried();
90
+
91
+ const packageManifestUrl = this.ciPackageUrls.get(id);
92
+ if (!packageManifestUrl) {
93
+ return false;
94
+ }
95
+
96
+ const manifestUrl = this.pathURL(packageManifestUrl, 'package.manifest.json');
97
+ const packageManifestJson = await this.fetchJson(manifestUrl);
98
+ const currentDate = packageManifestJson.date;
99
+ const packageDate = typeof npmPackage.date === 'function' ? npmPackage.date() : npmPackage.date;
100
+
101
+ return currentDate === packageDate;
102
+ }
103
+
104
+ /**
105
+ * Load package from CI build
106
+ * @param {string} id - Package ID
107
+ * @param {string} branch - Branch name (optional)
108
+ * @returns {Promise<{stream: Buffer, url: string, version: string}>}
109
+ */
110
+ async loadFromCIBuild(id, branch = null) {
111
+ await this.checkCIServerQueried();
112
+
113
+ if (this.ciPackageUrls.has(id)) {
114
+ const packageBaseUrl = this.ciPackageUrls.get(id);
115
+
116
+ if (!branch) {
117
+ let stream;
118
+ let url = this.pathURL(packageBaseUrl, 'package.tgz');
119
+
120
+ try {
121
+ stream = await this.fetchFromUrlSpecific(url);
122
+ } catch (e) {
123
+ url = this.pathURL(packageBaseUrl, 'branches', 'main', 'package.tgz');
124
+ stream = await this.fetchFromUrlSpecific(url);
125
+ }
126
+
127
+ return {
128
+ stream,
129
+ url: this.pathURL(packageBaseUrl, 'package.tgz'),
130
+ version: 'current'
131
+ };
132
+ } else {
133
+ const url = this.pathURL(packageBaseUrl, 'branches', branch, 'package.tgz');
134
+ const stream = await this.fetchFromUrlSpecific(url);
135
+
136
+ return {
137
+ stream,
138
+ url,
139
+ version: 'current$' + branch
140
+ };
141
+ }
142
+ } else if (id.startsWith('hl7.fhir.r6')) {
143
+ const url = this.pathURL(this.rootUrl, id + '.tgz');
144
+ const stream = await this.fetchFromUrlSpecific(url);
145
+
146
+ return {
147
+ stream,
148
+ url,
149
+ version: 'current'
150
+ };
151
+ } else if (this.endsWithInList(id, '.r3', '.r4', '.r4b', '.r5', '.r6')) {
152
+ const npid = id.substring(0, id.lastIndexOf('.'));
153
+ const baseUrl = this.ciPackageUrls.get(npid);
154
+
155
+ if (!baseUrl) {
156
+ throw new Error(`The package '${id}' has no entry on the current build server`);
157
+ }
158
+
159
+ const url = this.pathURL(baseUrl, id + '.tgz');
160
+ const stream = await this.fetchFromUrlSpecific(url);
161
+
162
+ return {
163
+ stream,
164
+ url,
165
+ version: 'current'
166
+ };
167
+ } else {
168
+ throw new Error(`The package '${id}' has no entry on the current build server`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Fetch content from URL
174
+ * @param {string} source - URL to fetch
175
+ * @returns {Promise<Buffer>}
176
+ * @private
177
+ */
178
+ async fetchFromUrlSpecific(source) {
179
+ return new Promise((resolve, reject) => {
180
+ const protocol = source.startsWith('https') ? https : http;
181
+
182
+ const request = protocol.get(source, (response) => {
183
+ // Handle redirects
184
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
185
+ this.fetchFromUrlSpecific(response.headers.location)
186
+ .then(resolve)
187
+ .catch(reject);
188
+ return;
189
+ }
190
+
191
+ if (response.statusCode !== 200) {
192
+ reject(new Error(`Unable to fetch ${source}: HTTP ${response.statusCode}`));
193
+ return;
194
+ }
195
+
196
+ const chunks = [];
197
+ response.on('data', (chunk) => chunks.push(chunk));
198
+ response.on('end', () => resolve(Buffer.concat(chunks)));
199
+ response.on('error', reject);
200
+ });
201
+
202
+ request.on('error', (e) => reject(new Error(`Unable to fetch ${source}: ${e.message}`)));
203
+ request.setTimeout(30000, () => {
204
+ request.destroy();
205
+ reject(new Error(`Timeout fetching ${source}`));
206
+ });
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Fetch JSON from URL
212
+ * @param {string} url - URL to fetch
213
+ * @returns {Promise<Object>}
214
+ * @private
215
+ */
216
+ async fetchJson(url) {
217
+ const buffer = await this.fetchFromUrlSpecific(url);
218
+ return JSON.parse(buffer.toString('utf8'));
219
+ }
220
+
221
+ /**
222
+ * Check if CI server needs to be queried and update if needed
223
+ * @private
224
+ */
225
+ async checkCIServerQueried() {
226
+ if (Date.now() - this.ciLastQueriedTimeStamp > this.ciQueryInterval) {
227
+ try {
228
+ await this.updateFromCIServer();
229
+ } catch (e) {
230
+ // Pause and retry once - most common reason is file being changed on server
231
+ await this.sleep(1000);
232
+ try {
233
+ await this.updateFromCIServer();
234
+ } catch (e2) {
235
+ console.debug(`Error connecting to build server - running without build (${e2.message})`);
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Update package information from CI server
243
+ * @private
244
+ */
245
+ async updateFromCIServer() {
246
+ try {
247
+ const url = `${this.rootUrl}/ig/qas.json?nocache=${Date.now()}`;
248
+ const buffer = await this.fetchFromUrlSpecific(url);
249
+ this.ciBuildInfo = JSON.parse(buffer.toString('utf8'));
250
+
251
+ const builds = [];
252
+
253
+ for (const j of this.ciBuildInfo) {
254
+ if (j.url && j['package-id'] && j['package-id'].includes('.')) {
255
+ let packageUrl = j.url;
256
+ if (packageUrl.includes('/ImplementationGuide/')) {
257
+ packageUrl = packageUrl.substring(0, packageUrl.indexOf('/ImplementationGuide/'));
258
+ }
259
+ builds.push({
260
+ url: packageUrl,
261
+ packageId: j['package-id'],
262
+ repo: this.getRepo(j.repo),
263
+ date: this.readDate(j.date)
264
+ });
265
+ }
266
+ }
267
+
268
+ // Sort by date descending (newest first)
269
+ builds.sort((a, b) => b.date.getTime() - a.date.getTime());
270
+
271
+ for (const build of builds) {
272
+ if (!this.ciPackageUrls.has(build.packageId)) {
273
+ this.ciPackageUrls.set(build.packageId, `${this.rootUrl}/ig/${build.repo}`);
274
+ }
275
+ }
276
+ } finally {
277
+ this.ciLastQueriedTimeStamp = Date.now();
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Extract repo path from full path
283
+ * @param {string} path - Full path
284
+ * @returns {string} Repo path (org/repo)
285
+ * @private
286
+ */
287
+ getRepo(path) {
288
+ if (!path) return '';
289
+ const p = path.split('/');
290
+ return p[0] + '/' + p[1];
291
+ }
292
+
293
+ /**
294
+ * Parse date string from CI server
295
+ * @param {string} s - Date string in format "EEE, dd MMM, yyyy HH:mm:ss Z"
296
+ * @returns {Date}
297
+ * @private
298
+ */
299
+ readDate(s) {
300
+ if (!s) return new Date();
301
+
302
+ try {
303
+ // Parse format like "Mon, 15 Jan, 2024 10:30:00 +0000"
304
+ return new Date(s);
305
+ } catch (e) {
306
+ console.error('Error parsing date:', e);
307
+ return new Date();
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Join URL path segments
313
+ * @param {...string} parts - Path parts
314
+ * @returns {string}
315
+ * @private
316
+ */
317
+ pathURL(...parts) {
318
+ return parts
319
+ .map((part, index) => {
320
+ if (index === 0) {
321
+ return part.replace(/\/+$/, '');
322
+ }
323
+ return part.replace(/^\/+|\/+$/g, '');
324
+ })
325
+ .filter(part => part.length > 0)
326
+ .join('/');
327
+ }
328
+
329
+ /**
330
+ * Check if string ends with any of the given suffixes
331
+ * @param {string} str - String to check
332
+ * @param {...string} suffixes - Suffixes to check
333
+ * @returns {boolean}
334
+ * @private
335
+ */
336
+ endsWithInList(str, ...suffixes) {
337
+ return suffixes.some(suffix => str.endsWith(suffix));
338
+ }
339
+
340
+ /**
341
+ * Sleep for specified milliseconds
342
+ * @param {number} ms - Milliseconds to sleep
343
+ * @returns {Promise<void>}
344
+ * @private
345
+ */
346
+ sleep(ms) {
347
+ return new Promise(resolve => setTimeout(resolve, ms));
348
+ }
349
+ }
350
+
351
+
352
+ class PackageManager {
353
+ totalDownloaded = 0;
354
+
355
+ /**
356
+ * @param {string[]} packageServers - Ordered list of package server URLs
357
+ * @param {string} cacheFolder - Local folder for cached content
358
+ */
359
+ constructor(packageServers, cacheFolder) {
360
+ if (!packageServers || packageServers.length === 0) {
361
+ throw new Error('At least one package server must be provided');
362
+ }
363
+ this.packageServers = packageServers;
364
+ this.cacheFolder = cacheFolder;
365
+ }
366
+
367
+ /**
368
+ * Fetch a package, either from cache or from servers
369
+ * @param {string} packageId - Package identifier (e.g., 'hl7.fhir.us.core')
370
+ * @param {string} version - Version string (may contain wildcards)
371
+ * @returns {Promise<string>} Path to extracted package folder
372
+ */
373
+ async fetch(packageId, version) {
374
+ // First, resolve the version if it contains wildcards
375
+ const resolvedVersion = await this.resolveVersion(packageId, version);
376
+
377
+ // Check cache first
378
+ const cachedPath = await this.checkCache(packageId, resolvedVersion);
379
+ if (cachedPath) {
380
+ return cachedPath;
381
+ }
382
+
383
+ console.log("Fetch Package "+packageId+"#"+version);
384
+ // Not in cache, fetch from servers
385
+ const packageData = await this.fetchFromServers(packageId, resolvedVersion);
386
+
387
+ this.totalDownloaded = this.totalDownloaded + packageData.length;
388
+ // Extract to cache
389
+ const extractedPath = await this.extractToCache(packageId, resolvedVersion, packageData);
390
+
391
+ return extractedPath;
392
+ }
393
+
394
+ /**
395
+ * Resolve version with wildcards to a specific version
396
+ * @param {string} packageId - Package identifier
397
+ * @param {string} version - Version string (may contain wildcards)
398
+ * @returns {Promise<string>} Resolved specific version
399
+ */
400
+ async resolveVersion(packageId, version) {
401
+ // If no wildcards, return as-is
402
+ if (!VersionUtilities.versionHasWildcards(version) && version != null) {
403
+ return version;
404
+ }
405
+
406
+ // Need to get version list and find best match
407
+ for (const server of this.packageServers) {
408
+ try {
409
+ const versions = await this.getPackageVersions(server, packageId);
410
+ const resolvedVersion = this.selectBestVersion(versions, version);
411
+ if (resolvedVersion) {
412
+ return resolvedVersion;
413
+ }
414
+ } catch (error) {
415
+ // Try next server
416
+ continue;
417
+ }
418
+ }
419
+
420
+ throw new Error(`Could not resolve version ${version} for package ${packageId}`);
421
+ }
422
+
423
+ /**
424
+ * Get list of available versions for a package from a server
425
+ * @param {string} server - Server URL
426
+ * @param {string} packageId - Package identifier
427
+ * @returns {Promise<string[]>} Array of version strings
428
+ */
429
+ async getPackageVersions(server, packageId) {
430
+ const url = `${server}/${packageId}`;
431
+
432
+ return new Promise((resolve, reject) => {
433
+ const parsedUrl = new URL(url);
434
+ const client = parsedUrl.protocol === 'https:' ? https : http;
435
+
436
+ const req = client.get(url, {
437
+ headers: {
438
+ 'Accept': 'application/json'
439
+ }
440
+ }, (res) => {
441
+ if (res.statusCode === 404) {
442
+ reject(new Error(`Package ${packageId} not found on ${server}`));
443
+ return;
444
+ }
445
+
446
+ if (res.statusCode !== 200) {
447
+ reject(new Error(`HTTP ${res.statusCode} from ${server}`));
448
+ return;
449
+ }
450
+
451
+ let data = '';
452
+ res.on('data', chunk => data += chunk);
453
+ res.on('end', () => {
454
+ try {
455
+ const json = JSON.parse(data);
456
+ const versions = Object.keys(json.versions || {});
457
+ resolve(versions);
458
+ } catch (error) {
459
+ reject(new Error(`Invalid JSON from ${server}: ${error.message}`));
460
+ }
461
+ });
462
+ });
463
+
464
+ req.on('error', reject);
465
+ req.end();
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Select the best matching version from available versions
471
+ * @param {string[]} availableVersions - List of available versions
472
+ * @param {string} criteria - Version criteria (may contain wildcards)
473
+ * @returns {string|null} Best matching version or null if none match
474
+ */
475
+ selectBestVersion(availableVersions, criteria) {
476
+ const sortedVersions = [...availableVersions].sort((a, b) => {
477
+ try {
478
+ return -VersionUtilities.compareVersions(a, b);
479
+ } catch (error) {
480
+ return 0;
481
+ }
482
+ });
483
+
484
+ if (criteria == null) {
485
+ return sortedVersions.length == 0 ? null : sortedVersions[0];
486
+ }
487
+ // Filter versions that match the criteria
488
+ const matchingVersions = sortedVersions.filter(v => {
489
+ try {
490
+ return VersionUtilities.versionMatches(criteria, v);
491
+ } catch (error) {
492
+ return false;
493
+ }
494
+ });
495
+
496
+ if (matchingVersions.length === 0) {
497
+ return null;
498
+ }
499
+
500
+ // Sort by version (newest first) using compareVersions
501
+ matchingVersions.sort((a, b) => {
502
+ try {
503
+ return -VersionUtilities.compareVersions(a, b);
504
+ } catch (error) {
505
+ return 0;
506
+ }
507
+ });
508
+
509
+ return matchingVersions[0];
510
+ }
511
+
512
+ /**
513
+ * Check if package exists in cache
514
+ * @param {string} packageId - Package identifier
515
+ * @param {string} version - Specific version
516
+ * @returns {Promise<string|null>} Path to cached package or null if not found
517
+ */
518
+ async checkCache(packageId, version) {
519
+ const packageName = `${packageId}#${version}`;
520
+ const packagePath = path.join(this.cacheFolder, packageName);
521
+
522
+ try {
523
+ const stats = await fs.stat(packagePath);
524
+ if (stats.isDirectory()) {
525
+ return packageName;
526
+ }
527
+ } catch (error) {
528
+ // Not found or not accessible
529
+ }
530
+
531
+ return null;
532
+ }
533
+
534
+ /**
535
+ * Fetch package data from servers
536
+ * @param {string} packageId - Package identifier
537
+ * @param {string} version - Specific version
538
+ * @returns {Promise<Buffer>} Package tar.gz data
539
+ */
540
+ async fetchFromServers(packageId, version) {
541
+ let lastError = null;
542
+
543
+ if (version == "current") {
544
+ const result = await new CIBuildClient().loadFromCIBuild(packageId);
545
+ return result.stream;
546
+ }
547
+ for (const server of this.packageServers) {
548
+ try {
549
+ const packageData = await this.fetchFromServer(server, packageId, version);
550
+ return packageData;
551
+ } catch (error) {
552
+ lastError = error;
553
+ // Try next server
554
+ continue;
555
+ }
556
+ }
557
+
558
+ throw new Error(`Failed to fetch ${packageId}#${version} from any server. Last error: ${lastError?.message}`);
559
+ }
560
+
561
+ /**
562
+ * Fetch package data from a specific server
563
+ * @param {string} server - Server URL
564
+ * @param {string} packageId - Package identifier
565
+ * @param {string} version - Specific version
566
+ * @returns {Promise<Buffer>} Package tar.gz data
567
+ */
568
+ async fetchFromServer(server, packageId, version) {
569
+ const url = `${server}/${packageId}/${version}`;
570
+
571
+ try {
572
+ const response = await axios.get(url, {
573
+ headers: {
574
+ 'Accept': 'application/tar+gzip'
575
+ },
576
+ responseType: 'arraybuffer',
577
+ maxRedirects: 5
578
+ });
579
+
580
+ return Buffer.from(response.data);
581
+ } catch (error) {
582
+ if (error.response?.status === 404) {
583
+ throw new Error(`Package ${packageId}#${version} not found on ${server}`);
584
+ }
585
+ throw new Error(`HTTP ${error.response?.status || 'error'} from ${server}: ${error.message}`);
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Extract package to cache folder
591
+ * @param {string} packageId - Package identifier
592
+ * @param {string} version - Specific version
593
+ * @param {Buffer} packageData - Package tar.gz data
594
+ * @returns {Promise<string>} Path to extracted package
595
+ */
596
+ async extractToCache(packageId, version, packageData) {
597
+ const packageName = `${packageId}#${version}`;
598
+ const packagePath = path.join(this.cacheFolder, packageName);
599
+
600
+ // Ensure cache folder exists
601
+ await fs.mkdir(this.cacheFolder, { recursive: true });
602
+
603
+ // Create package folder
604
+ await fs.mkdir(packagePath, { recursive: true });
605
+
606
+ // Extract tar.gz
607
+ return new Promise((resolve, reject) => {
608
+ const gunzip = zlib.createGunzip();
609
+ const extract = tar.extract({
610
+ cwd: packagePath,
611
+ strict: true
612
+ });
613
+
614
+ gunzip.on('error', reject);
615
+ extract.on('error', reject);
616
+ extract.on('finish', () => resolve(packageName));
617
+
618
+ // Create a readable stream from the buffer and pipe through gunzip to tar
619
+ const stream = require('stream');
620
+ const bufferStream = new stream.PassThrough();
621
+ bufferStream.end(packageData);
622
+
623
+ bufferStream
624
+ .pipe(gunzip)
625
+ .pipe(extract);
626
+ });
627
+ }
628
+ }
629
+
630
+ class PackageContentLoader {
631
+ /**
632
+ * @param {string} packageFolder - Path to the extracted NPM package folder
633
+ */
634
+ constructor(packageFolder) {
635
+ this.packageFolder = packageFolder;
636
+ this.packageSubfolder = path.join(packageFolder, 'package');
637
+ this.indexPath = path.join(this.packageSubfolder, '.index.json');
638
+ this.index = null;
639
+ this.indexByTypeAndId = new Map();
640
+ this.indexByCanonical = new Map();
641
+ this.loaded = false;
642
+ }
643
+
644
+ /**
645
+ * Initialize the loader by reading and parsing the index
646
+ * @returns {Promise<void>}
647
+ */
648
+ async initialize() {
649
+ if (this.loaded) {
650
+ return;
651
+ }
652
+
653
+ const packageSource = path.join(this.packageFolder, 'package', 'package.json');
654
+ const packageContent = await fs.readFile(packageSource, 'utf8');
655
+ this.package = JSON.parse(packageContent);
656
+
657
+ try {
658
+ const indexContent = await fs.readFile(this.indexPath, 'utf8');
659
+ this.index = JSON.parse(indexContent);
660
+
661
+ if (!this.index.files || !Array.isArray(this.index.files)) {
662
+ throw new Error('Invalid index file: missing or invalid files array');
663
+ }
664
+
665
+ // Build lookup structures
666
+ this.buildIndexes();
667
+ this.loaded = true;
668
+ } catch (error) {
669
+ throw new Error(`Failed to load package index from ${this.indexPath}: ${error.message}`);
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Build internal indexes for efficient lookups
675
+ */
676
+ buildIndexes() {
677
+ for (const entry of this.index.files) {
678
+ // Index by resourceType and id
679
+ if (entry.resourceType && entry.id) {
680
+ const key = `${entry.resourceType}/${entry.id}`;
681
+ this.indexByTypeAndId.set(key, entry);
682
+ }
683
+
684
+ // Index by canonical URL (with and without version)
685
+ if (entry.url) {
686
+ // Index without version
687
+ this.indexByCanonical.set(entry.url, entry);
688
+
689
+ // Index with version if present
690
+ if (entry.version) {
691
+ const versionedUrl = `${entry.url}|${entry.version}`;
692
+ this.indexByCanonical.set(versionedUrl, entry);
693
+ }
694
+ }
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Load a resource by reference
700
+ * @param {Object} reference - Reference object
701
+ * @param {string} [reference.resourceType] - Resource type
702
+ * @param {string} [reference.id] - Resource id
703
+ * @param {string} [reference.url] - Canonical URL
704
+ * @param {string} [reference.version] - Version (optional)
705
+ * @returns {Promise<Object|null>} Loaded resource or null if not found
706
+ */
707
+ async loadByReference(reference) {
708
+ await this.initialize();
709
+
710
+ let entry = null;
711
+
712
+ // Try to find by resourceType and id
713
+ if (reference.resourceType && reference.id) {
714
+ const key = `${reference.resourceType}/${reference.id}`;
715
+ entry = this.indexByTypeAndId.get(key);
716
+ }
717
+
718
+ // Try to find by canonical URL
719
+ if (!entry && reference.url) {
720
+ if (reference.version) {
721
+ // Try with version first
722
+ const versionedUrl = `${reference.url}|${reference.version}`;
723
+ entry = this.indexByCanonical.get(versionedUrl);
724
+ }
725
+
726
+ // Try without version if not found
727
+ if (!entry) {
728
+ entry = this.indexByCanonical.get(reference.url);
729
+ }
730
+ }
731
+
732
+ if (!entry) {
733
+ return null;
734
+ }
735
+
736
+ return await this.loadFile(entry);
737
+ }
738
+
739
+ /**
740
+ * Get a list of resources of a given type
741
+ * @param {string} resourceType - The resource type to filter by
742
+ * @returns {Promise<Array>} Array of index entries for the given type
743
+ */
744
+ async getResourcesByType(resourceType) {
745
+ await this.initialize();
746
+
747
+ return this.index.files.filter(entry => entry.resourceType === resourceType);
748
+ }
749
+
750
+ /**
751
+ * Load all files that pass a given filter
752
+ * @param {Function} filterFn - Filter function that takes an index entry and returns boolean
753
+ * @returns {Promise<Array>} Array of loaded resources that pass the filter
754
+ */
755
+ async loadByFilter(filterFn) {
756
+ await this.initialize();
757
+
758
+ const filteredEntries = this.index.files.filter(filterFn);
759
+ const loadPromises = filteredEntries.map(entry => this.loadFile(entry));
760
+
761
+ return await Promise.all(loadPromises);
762
+ }
763
+
764
+ /**
765
+ * Load a single file based on its index entry
766
+ * @param {Object} entry - Index entry
767
+ * @returns {Promise<Object>} Loaded resource
768
+ */
769
+ async loadFile(entry) {
770
+ if (!entry.filename) {
771
+ throw new Error('Index entry missing filename');
772
+ }
773
+
774
+ const filePath = path.join(this.packageSubfolder, entry.filename);
775
+
776
+ try {
777
+ const content = await fs.readFile(filePath, 'utf8');
778
+ return JSON.parse(content);
779
+ } catch (error) {
780
+ throw new Error(`Failed to load file ${entry.filename}: ${error.message}`);
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Get the raw index data
786
+ * @returns {Promise<Object>} The index object
787
+ */
788
+ async getIndex() {
789
+ await this.initialize();
790
+ return this.index;
791
+ }
792
+
793
+ /**
794
+ * Get all resources (index entries only, not loaded)
795
+ * @returns {Promise<Array>} All index entries
796
+ */
797
+ async getAllResources() {
798
+ await this.initialize();
799
+ return this.index.files;
800
+ }
801
+
802
+ /**
803
+ * Check if a resource exists by reference
804
+ * @param {Object} reference - Reference object (same as loadByReference)
805
+ * @returns {Promise<boolean>} True if resource exists
806
+ */
807
+ async exists(reference) {
808
+ await this.initialize();
809
+
810
+ // Check by resourceType and id
811
+ if (reference.resourceType && reference.id) {
812
+ const key = `${reference.resourceType}/${reference.id}`;
813
+ if (this.indexByTypeAndId.has(key)) {
814
+ return true;
815
+ }
816
+ }
817
+
818
+ // Check by canonical URL
819
+ if (reference.url) {
820
+ if (reference.version) {
821
+ const versionedUrl = `${reference.url}|${reference.version}`;
822
+ if (this.indexByCanonical.has(versionedUrl)) {
823
+ return true;
824
+ }
825
+ }
826
+
827
+ if (this.indexByCanonical.has(reference.url)) {
828
+ return true;
829
+ }
830
+ }
831
+
832
+ return false;
833
+ }
834
+
835
+ /**
836
+ * Get statistics about the package content
837
+ * @returns {Promise<Object>} Statistics object
838
+ */
839
+ async getStatistics() {
840
+ await this.initialize();
841
+
842
+ const stats = {
843
+ totalResources: this.index.files.length,
844
+ indexVersion: this.index['index-version'],
845
+ resourceTypes: {}
846
+ };
847
+
848
+ for (const entry of this.index.files) {
849
+ if (entry.resourceType) {
850
+ stats.resourceTypes[entry.resourceType] =
851
+ (stats.resourceTypes[entry.resourceType] || 0) + 1;
852
+ }
853
+ }
854
+
855
+ return stats;
856
+ }
857
+
858
+ fhirVersion() {
859
+ return this.package.fhirVersions[0];
860
+ }
861
+
862
+ id() {
863
+ return this.package.name;
864
+ }
865
+
866
+ version() {
867
+ return this.package.version;
868
+ }
869
+
870
+ pid() {
871
+ return this.id()+"#"+this.version();
872
+ }
873
+ }
874
+
875
+
876
+ module.exports = { PackageManager, PackageContentLoader };