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,846 @@
1
+ //
2
+ // Copyright 2025, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
3
+ //
4
+ // Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
5
+ //
6
+
7
+ const axios = require('axios');
8
+ const {XMLParser} = require('fast-xml-parser');
9
+ const crypto = require('crypto');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ class PackageCrawler {
14
+ log;
15
+
16
+ constructor(config, db) {
17
+ this.config = config;
18
+ this.db = db;
19
+ this.totalBytes = 0;
20
+ this.crawlerLog = {};
21
+ this.errors = '';
22
+ this.db.run('PRAGMA journal_mode = WAL');
23
+ this.db.run('PRAGMA busy_timeout = 5000');
24
+ }
25
+
26
+ async crawl(log) {
27
+ this.log = log;
28
+
29
+ const startTime = Date.now();
30
+ this.crawlerLog = {
31
+ startTime: new Date().toISOString(),
32
+ master: this.config.masterUrl,
33
+ feeds: [],
34
+ totalBytes: 0,
35
+ errors: ''
36
+ };
37
+
38
+ this.log.info('Running web crawler for packages using master URL: '+ this.config.masterUrl);
39
+
40
+ try {
41
+ // Fetch the master JSON file
42
+ const masterResponse = await this.fetchJson(this.config.masterUrl);
43
+
44
+ if (!masterResponse.feeds || !Array.isArray(masterResponse.feeds)) {
45
+ throw new Error('Invalid master JSON: missing feeds array');
46
+ }
47
+
48
+ // Process package restrictions if available
49
+ const packageRestrictions = masterResponse['package-restrictions'] || [];
50
+
51
+ // Process each feed
52
+ for (const feedConfig of masterResponse.feeds) {
53
+ if (!feedConfig.url) {
54
+ this.log.info('Skipping feed with no URL: '+ feedConfig);
55
+ continue;
56
+ }
57
+
58
+ try {
59
+ await this.updateTheFeed(
60
+ this.fixUrl(feedConfig.url),
61
+ this.config.masterUrl,
62
+ feedConfig.errors ? feedConfig.errors.replace(/\|/g, '@').replace(/_/g, '.') : '',
63
+ packageRestrictions
64
+ );
65
+ } catch (feedError) {
66
+ this.log.error(`Failed to process feed ${feedConfig.url}: `+ feedError.message);
67
+ // Continue with next feed even if this one fails
68
+ }
69
+ }
70
+
71
+ const runTime = Date.now() - startTime;
72
+ this.crawlerLog.runTime = `${runTime}ms`;
73
+ this.crawlerLog.endTime = new Date().toISOString();
74
+ this.crawlerLog.totalBytes = this.totalBytes;
75
+
76
+ this.log.info(`Web crawler completed successfully in ${runTime}ms`);
77
+ this.log.info(`Total bytes processed: ${this.totalBytes}`);
78
+
79
+ return this.crawlerLog;
80
+
81
+ } catch (error) {
82
+ const runTime = Date.now() - startTime;
83
+ this.crawlerLog.runTime = `${runTime}ms`;
84
+ this.crawlerLog.fatalException = error.message;
85
+ this.crawlerLog.endTime = new Date().toISOString();
86
+
87
+ this.log.error('Web crawler failed: '+ error);
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ fixUrl(url) {
93
+ return url.replace(/^http:/, 'https:');
94
+ }
95
+
96
+ async fetchJson(url) {
97
+ try {
98
+ const response = await axios.get(url, {
99
+ timeout: 30000,
100
+ headers: {
101
+ 'User-Agent': 'FHIR Package Crawler/1.0'
102
+ }
103
+ });
104
+ return response.data;
105
+ } catch (error) {
106
+ if (error.response && error.response.status === 429) {
107
+ throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
108
+ }
109
+ throw new Error(`Failed to fetch JSON from ${url}: ${error.message}`);
110
+ }
111
+ }
112
+
113
+ async fetchXml(url) {
114
+ try {
115
+ const response = await axios.get(url, {
116
+ timeout: 30000,
117
+ headers: {
118
+ 'User-Agent': 'FHIR Package Crawler/1.0'
119
+ }
120
+ });
121
+
122
+ const parser = new XMLParser({
123
+ ignoreAttributes: false,
124
+ attributeNamePrefix: '@_',
125
+ textNodeName: '#text'
126
+ });
127
+
128
+ return parser.parse(response.data);
129
+ } catch (error) {
130
+ if (error.response && error.response.status === 429) {
131
+ throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
132
+ }
133
+ throw new Error(`Failed to fetch XML from ${url}: ${error.message}`);
134
+ }
135
+ }
136
+
137
+ async fetchUrl(url) {
138
+ try {
139
+ const response = await axios.get(url, {
140
+ timeout: 60000,
141
+ responseType: 'arraybuffer',
142
+ headers: {
143
+ 'User-Agent': 'FHIR Package Crawler/1.0'
144
+ }
145
+ });
146
+
147
+ this.totalBytes += response.data.byteLength;
148
+ return Buffer.from(response.data);
149
+ } catch (error) {
150
+ if (error.response && error.response.status === 429) {
151
+ throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
152
+ }
153
+ throw new Error(`Failed to fetch ${url}: ${error.message}`);
154
+ }
155
+ }
156
+
157
+ async updateTheFeed(url, source, email, packageRestrictions) {
158
+ const feedLog = {
159
+ url: url,
160
+ items: []
161
+ };
162
+ this.crawlerLog.feeds.push(feedLog);
163
+
164
+ this.log.info('Processing feed: '+ url);
165
+ const startTime = Date.now();
166
+
167
+ try {
168
+ const xmlData = await this.fetchXml(url);
169
+ feedLog.fetchTime = `${Date.now() - startTime}ms`;
170
+
171
+ // Navigate the RSS structure
172
+ let items = [];
173
+ if (xmlData.rss && xmlData.rss.channel) {
174
+ const channel = xmlData.rss.channel;
175
+ items = Array.isArray(channel.item) ? channel.item : [channel.item].filter(Boolean);
176
+ }
177
+
178
+ this.log.info(`Found ${items.length} items in feed`);
179
+
180
+ for (let i = 0; i < items.length; i++) {
181
+ try {
182
+ await this.updateItem(url, items[i], i, packageRestrictions, feedLog);
183
+ } catch (itemError) {
184
+ // Check if this is a 429 error on package download
185
+ if (itemError.message.includes('RATE_LIMITED')) {
186
+ this.log.info(`Rate limited while downloading package from ${url}, stopping feed processing`);
187
+ feedLog.rateLimited = true;
188
+ feedLog.rateLimitedAt = `item ${i}`;
189
+ feedLog.rateLimitMessage = itemError.message;
190
+ break; // Stop processing this feed
191
+ }
192
+ // For other errors, log and continue with next item
193
+ this.log.error(`Error processing item ${i} from ${url}:`+ itemError.message);
194
+ }
195
+ }
196
+
197
+ // TODO: Send email if there were errors and email is provided
198
+ if (this.errors && email && !feedLog.rateLimited) {
199
+ this.log.info(`Would send error email to ${email} for feed ${url}`);
200
+ }
201
+
202
+ } catch (error) {
203
+ // Check if this is a 429 error on feed fetch
204
+ if (error.message.includes('RATE_LIMITED')) {
205
+ this.log.info(`Rate limited while fetching feed ${url}, skipping this feed`);
206
+ feedLog.rateLimited = true;
207
+ feedLog.rateLimitMessage = error.message;
208
+ feedLog.failTime = `${Date.now() - startTime}ms`;
209
+ return; // Skip this feed entirely
210
+ }
211
+
212
+ feedLog.exception = error.message;
213
+ feedLog.failTime = `${Date.now() - startTime}ms`;
214
+ this.log.error(`Exception processing feed ${url}:`+ error.message);
215
+
216
+ // TODO: Send email notification for non-rate-limit errors
217
+ if (email) {
218
+ this.log.info(`Would send exception email to ${email} for feed ${url}`);
219
+ }
220
+ }
221
+ }
222
+
223
+ async updateItem(source, item, index, packageRestrictions, feedLog) {
224
+ const itemLog = {
225
+ status: '??'
226
+ };
227
+ feedLog.items.push(itemLog);
228
+
229
+ try {
230
+ // Extract GUID
231
+ if (!item.guid || !item.guid['#text']) {
232
+ const error = `Error processing item from ${source}#item[${index}]: no guid provided`;
233
+ this.log.info(error);
234
+ itemLog.error = 'no guid provided';
235
+ itemLog.status = 'error';
236
+ return;
237
+ }
238
+
239
+ const guid = item.guid['#text'];
240
+ itemLog.guid = guid;
241
+
242
+ // Extract title (package ID)
243
+ const id = item.title;
244
+ itemLog.id = id;
245
+
246
+ if (!id) {
247
+ itemLog.error = 'no title/id provided';
248
+ itemLog.status = 'error';
249
+ return;
250
+ }
251
+
252
+ // Check if not for publication
253
+ if (item.notForPublication && item.notForPublication['#text'] === 'true') {
254
+ itemLog.status = 'not for publication';
255
+ itemLog.error = 'not for publication';
256
+ return;
257
+ }
258
+
259
+ // Check package restrictions
260
+ if (!this.isPackageAllowed(id, source, packageRestrictions)) {
261
+ if (!source.includes('simplifier.net')) {
262
+ const error = `The package ${id} is not allowed to come from ${source}`;
263
+ this.log.info(error);
264
+ itemLog.error = error;
265
+ itemLog.status = 'prohibited source';
266
+ } else {
267
+ itemLog.status = 'ignored';
268
+ itemLog.error = `The package ${id} is published through another source`;
269
+ }
270
+ return;
271
+ }
272
+
273
+ // Check if already processed
274
+ if (await this.hasStored(guid)) {
275
+ itemLog.status = 'Already Processed';
276
+ return;
277
+ }
278
+
279
+ // Parse publication date
280
+ let pubDate;
281
+ try {
282
+ let pd = item.pubDate;
283
+ pubDate = this.parsePubDate(pd);
284
+ } catch (error) {
285
+ itemLog.error = `Invalid date format '{pd}': ${error.message}`;
286
+ itemLog.status = 'error';
287
+ return;
288
+ }
289
+
290
+ // Extract URL and fetch package
291
+ const url = this.fixUrl(item.link);
292
+ if (!url) {
293
+ itemLog.error = 'no link provided';
294
+ itemLog.status = 'error';
295
+ return;
296
+ }
297
+
298
+ itemLog.url = url;
299
+ this.log.info('Fetching package: '+ url);
300
+
301
+ const packageContent = await this.fetchUrl(url, 'application/tar+gzip');
302
+ await this.store(source, url, guid, pubDate, packageContent, id, itemLog);
303
+
304
+ itemLog.status = 'Fetched';
305
+
306
+ } catch (error) {
307
+ this.log.error(`Exception processing item ${itemLog.guid || index}:`+ error.message);
308
+ itemLog.status = 'Exception';
309
+ itemLog.error = error.message;
310
+ if (error.message.includes('RATE_LIMITED')) {
311
+ throw error;
312
+ }
313
+ }
314
+ }
315
+
316
+ isPackageAllowed(packageId, source, restrictions) {
317
+ if (!restrictions || !Array.isArray(restrictions)) {
318
+ return { allowed: true, allowedFeeds: '' };
319
+ }
320
+
321
+ // Convert URLs to https for consistent comparison
322
+ const fixUrl = (url) => url.replace(/^http:/, 'https:');
323
+
324
+ const fixedPackageId = fixUrl(packageId);
325
+ const fixedSource = fixUrl(source);
326
+
327
+ for (const restriction of restrictions) {
328
+ if (!restriction.mask || !restriction.feeds) continue;
329
+
330
+ const fixedMask = fixUrl(restriction.mask);
331
+
332
+ if (this.matchesPattern(fixedPackageId, fixedMask)) {
333
+ // This package matches a restriction - check if source is allowed
334
+ const allowedFeeds = restriction.feeds.map(feed => feed);
335
+ const feedList = allowedFeeds.join(', ');
336
+
337
+ for (const allowedFeed of restriction.feeds) {
338
+ const fixedFeed = fixUrl(allowedFeed);
339
+ if (fixedSource === fixedFeed) {
340
+ return { allowed: true, allowedFeeds: feedList };
341
+ }
342
+ }
343
+
344
+ // Package matches restriction but source is not in allowed feeds
345
+ return { allowed: false, allowedFeeds: feedList };
346
+ }
347
+ }
348
+
349
+ // No restrictions matched - package is allowed from any source
350
+ return { allowed: true, allowedFeeds: '' };
351
+ }
352
+
353
+ matchesPattern(packageId, mask) {
354
+ if (mask.includes('*')) {
355
+ const starIndex = mask.indexOf('*');
356
+ const maskPrefix = mask.substring(0, starIndex);
357
+ const packagePrefix = packageId.substring(0, starIndex);
358
+ return packagePrefix === maskPrefix;
359
+ } else {
360
+ return mask === packageId;
361
+ }
362
+ }
363
+
364
+ async hasStored(guid) {
365
+ return new Promise((resolve, reject) => {
366
+ this.db.get('SELECT COUNT(*) as count FROM PackageVersions WHERE GUID = ?', [guid], (err, row) => {
367
+ if (err) {
368
+ reject(err);
369
+ } else {
370
+ resolve(row.count > 0);
371
+ }
372
+ });
373
+ });
374
+ }
375
+
376
+ parsePubDate(dateStr) {
377
+ // Handle various RSS date formats
378
+ let cleanDate = dateStr.toLowerCase().replace(/\s+/g, ' ').trim();
379
+
380
+ // Remove day of week if present
381
+ if (cleanDate.includes(',')) {
382
+ cleanDate = cleanDate.substring(cleanDate.indexOf(',') + 1).trim();
383
+ } else if (/^(mon|tue|wed|thu|fri|sat|sun)/.test(cleanDate)) {
384
+ cleanDate = cleanDate.substring(cleanDate.indexOf(' ') + 1).trim();
385
+ }
386
+
387
+ // Pad single digit day
388
+ if (cleanDate.length > 2 && cleanDate[1] === ' ' && /^\d$/.test(cleanDate[0])) {
389
+ cleanDate = '0' + cleanDate;
390
+ }
391
+
392
+ // Try to parse the date
393
+ const date = new Date(cleanDate);
394
+ if (isNaN(date.getTime())) {
395
+ throw new Error(`Cannot parse date: ${dateStr}`);
396
+ }
397
+
398
+ return date;
399
+ }
400
+
401
+ async store(source, url, guid, date, packageBuffer, idver, itemLog) {
402
+ try {
403
+ // Extract and parse the NPM package
404
+ const npmPackage = await this.extractNpmPackage(packageBuffer, `${source}#${guid}`);
405
+
406
+ const {id, version} = npmPackage;
407
+
408
+ if (`${id}#${version}` !== idver) {
409
+ const warning = `Warning processing ${idver}: actually found ${id}#${version} in the package`;
410
+ this.log.info(warning);
411
+ itemLog.warning = warning;
412
+ }
413
+
414
+ // Save to mirror if configured
415
+ if (this.config.mirrorPath) {
416
+ const filename = `${id}-${version}.tgz`;
417
+ const filepath = path.join(this.config.mirrorPath, filename);
418
+ fs.writeFileSync(filepath, packageBuffer);
419
+ }
420
+
421
+ // Validate package data
422
+ if (!this.isValidPackageId(id)) {
423
+ throw new Error(`NPM Id "${id}" is not valid from ${source}`);
424
+ }
425
+
426
+ if (!this.isValidSemVersion(version)) {
427
+ throw new Error(`NPM Version "${version}" is not valid from ${source}`);
428
+ }
429
+
430
+ let canonical = npmPackage.canonical || `http://simplifier.net/packages/${id}`;
431
+ if (!this.isAbsoluteUrl(canonical)) {
432
+ throw new Error(`NPM Canonical "${canonical}" is not valid from ${source}`);
433
+ }
434
+
435
+ // Extract URLs from package
436
+ const urls = this.processPackageUrls(npmPackage);
437
+
438
+ // Commit to database
439
+ await this.commit(packageBuffer, npmPackage, date, guid, id, version, canonical, urls);
440
+
441
+ } catch (error) {
442
+ this.log.error(`Error storing package ${guid}:`+ error.message);
443
+ throw error;
444
+ }
445
+ }
446
+
447
+ async extractNpmPackage(packageBuffer, source) {
448
+ try {
449
+ const files = {};
450
+ const zlib = require('zlib');
451
+
452
+ // First decompress the gzip
453
+ const decompressed = zlib.gunzipSync(packageBuffer);
454
+
455
+ // Parse tar manually without any file system operations
456
+ let offset = 0;
457
+
458
+ while (offset < decompressed.length) {
459
+ // Read tar header (512 bytes)
460
+ if (offset + 512 > decompressed.length) break;
461
+
462
+ const header = decompressed.slice(offset, offset + 512);
463
+
464
+ // Check if this is the end (null header)
465
+ if (header[0] === 0) break;
466
+
467
+ // Extract filename (first 100 bytes, null-terminated)
468
+ let filename = '';
469
+ for (let i = 0; i < 100; i++) {
470
+ if (header[i] === 0) break;
471
+ filename += String.fromCharCode(header[i]);
472
+ }
473
+
474
+ // Extract file size (12 bytes starting at offset 124, octal)
475
+ let sizeStr = '';
476
+ for (let i = 124; i < 136; i++) {
477
+ if (header[i] === 0 || header[i] === 32) break; // null or space
478
+ sizeStr += String.fromCharCode(header[i]);
479
+ }
480
+ const fileSize = parseInt(sizeStr, 8) || 0;
481
+
482
+ // Move past header
483
+ offset += 512;
484
+
485
+ // Extract file content if we need this file
486
+ if (fileSize > 0) {
487
+ const cleanFilename = filename.replace(/^package\//, ''); // Remove package/ prefix
488
+
489
+ const fileContent = decompressed.slice(offset, offset + fileSize);
490
+ files[cleanFilename] = fileContent.toString('utf8');
491
+ }
492
+
493
+ // Move to next file (files are padded to 512-byte boundaries)
494
+ const paddedSize = Math.ceil(fileSize / 512) * 512;
495
+ offset += paddedSize;
496
+ }
497
+
498
+ // Parse package.json (required)
499
+ if (!files['package.json']) {
500
+ throw new Error('package.json not found in extracted package');
501
+ }
502
+
503
+ const packageJson = JSON.parse(files['package.json']);
504
+
505
+ // Extract basic NPM fields
506
+ const id = packageJson.name || '';
507
+ const version = packageJson.version || '';
508
+ const description = packageJson.description || '';
509
+ const author = this.extractAuthor(packageJson.author);
510
+ const license = packageJson.license || '';
511
+ const homepage = packageJson.homepage || packageJson.url || '';
512
+
513
+ // Extract dependencies
514
+ const dependencies = [];
515
+ if (packageJson.dependencies) {
516
+ for (const [dep, ver] of Object.entries(packageJson.dependencies)) {
517
+ dependencies.push(`${dep}@${ver}`);
518
+ }
519
+ }
520
+
521
+ // Extract FHIR-specific metadata
522
+ let fhirVersion = '';
523
+ let fhirVersionList = '';
524
+ let canonical = '';
525
+ let kind = 1; // Default to IG
526
+ let notForPublication = false;
527
+
528
+ // Check for FHIR metadata in package.json
529
+ if (packageJson.fhirVersions) {
530
+ if (Array.isArray(packageJson.fhirVersions)) {
531
+ fhirVersionList = packageJson.fhirVersions.join(',');
532
+ fhirVersion = packageJson.fhirVersions[0] || '';
533
+ } else {
534
+ fhirVersion = packageJson.fhirVersions;
535
+ fhirVersionList = packageJson.fhirVersions;
536
+ }
537
+ } else if (packageJson['fhir-version']) {
538
+ fhirVersion = packageJson['fhir-version'];
539
+ fhirVersionList = packageJson['fhir-version'];
540
+ }
541
+
542
+ if (packageJson.canonical) {
543
+ canonical = packageJson.canonical;
544
+ }
545
+
546
+ if (packageJson.type === 'fhir.core') {
547
+ kind = 0; // Core
548
+ } else if (packageJson.type === 'fhir.template') {
549
+ kind = 2; // Template
550
+ } else {
551
+ kind = 1; // IG (Implementation Guide)
552
+ }
553
+
554
+ if (packageJson.notForPublication === true) {
555
+ notForPublication = true;
556
+ }
557
+
558
+ // Parse .index.json if present
559
+ if (files['.index.json']) {
560
+ try {
561
+ const indexJson = JSON.parse(files['.index.json']);
562
+
563
+ // Extract additional metadata from .index.json
564
+ if (indexJson['fhir-version'] && !fhirVersion) {
565
+ fhirVersion = indexJson['fhir-version'];
566
+ fhirVersionList = indexJson['fhir-version'];
567
+ }
568
+
569
+ if (indexJson.canonical && !canonical) {
570
+ canonical = indexJson.canonical;
571
+ }
572
+ } catch (indexError) {
573
+ this.log.warn(`Warning: Could not parse .index.json for ${id}: ${indexError.message}`);
574
+ }
575
+ }
576
+
577
+ // Parse ig.ini if present
578
+ if (files['ig.ini']) {
579
+ try {
580
+ const iniData = this.parseIniFile(files['ig.ini']);
581
+
582
+ if (iniData.IG && iniData.IG.canonical && !canonical) {
583
+ canonical = iniData.IG.canonical;
584
+ }
585
+
586
+ if (iniData.IG && iniData.IG['fhir-version'] && !fhirVersion) {
587
+ fhirVersion = iniData.IG['fhir-version'];
588
+ fhirVersionList = iniData.IG['fhir-version'];
589
+ }
590
+ } catch (iniError) {
591
+ this.log.warn(`Warning: Could not parse ig.ini for ${id}: ${iniError.message}`);
592
+ }
593
+ }
594
+
595
+ // Default fhirVersion if not found
596
+ if (!fhirVersion) {
597
+ fhirVersion = '4.0.1'; // Default to R4
598
+ fhirVersionList = '4.0.1';
599
+ }
600
+
601
+ return {
602
+ id,
603
+ version,
604
+ description,
605
+ canonical,
606
+ fhirVersion,
607
+ fhirVersionList,
608
+ author,
609
+ license,
610
+ url: homepage,
611
+ dependencies,
612
+ kind,
613
+ notForPublication,
614
+ files
615
+ };
616
+
617
+ } catch (error) {
618
+ throw new Error(`Failed to extract NPM package from ${source}: ${error.message}`);
619
+ }
620
+ }
621
+
622
+ extractAuthor(author) {
623
+ if (typeof author === 'string') {
624
+ return author;
625
+ } else if (typeof author === 'object' && author.name) {
626
+ return author.name;
627
+ }
628
+ return '';
629
+ }
630
+
631
+ parseIniFile(content) {
632
+ const result = {};
633
+ let currentSection = null;
634
+
635
+ const lines = content.split('\n');
636
+ for (const line of lines) {
637
+ const trimmed = line.trim();
638
+
639
+ // Skip comments and empty lines
640
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
641
+ continue;
642
+ }
643
+
644
+ // Check for section header
645
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
646
+ if (sectionMatch) {
647
+ currentSection = sectionMatch[1];
648
+ result[currentSection] = {};
649
+ continue;
650
+ }
651
+
652
+ // Check for key=value pair
653
+ const keyValueMatch = trimmed.match(/^([^=]+)=(.*)$/);
654
+ if (keyValueMatch && currentSection) {
655
+ const key = keyValueMatch[1].trim();
656
+ const value = keyValueMatch[2].trim();
657
+ result[currentSection][key] = value;
658
+ }
659
+ }
660
+
661
+ return result;
662
+ }
663
+
664
+ isValidPackageId(id) {
665
+ // Simple package ID validation
666
+ return /^[a-z0-9][a-z0-9._-]*$/.test(id);
667
+ }
668
+
669
+ isValidSemVersion(version) {
670
+ // Simple semantic version validation
671
+ return /^\d+\.\d+\.\d+/.test(version);
672
+ }
673
+
674
+ isAbsoluteUrl(url) {
675
+ try {
676
+ new URL(url);
677
+ return true;
678
+ } catch {
679
+ return false;
680
+ }
681
+ }
682
+
683
+ processPackageUrls(npmPackage) {
684
+ const urls = [];
685
+
686
+ try {
687
+
688
+ for (const filename of Object.keys(npmPackage.files)) {
689
+ try {
690
+ const bytes = npmPackage.files[filename];
691
+ if (filename.endsWith('.json')) {
692
+ try {
693
+ const jsonContent = JSON.parse(bytes);
694
+
695
+ if (jsonContent.url && jsonContent.resourceType) {
696
+ urls.push(jsonContent.url);
697
+ }
698
+ } catch (fileError) {
699
+ // this.log.warn(`Error processing package file ${npmPackage.name}#${npmPackage.version}/package/${filename}: ${fileError.message}`);
700
+ }
701
+ }
702
+ } catch (fileError) {
703
+ this.log.warn(`Error processing package file ${npmPackage.name}#${npmPackage.version}/package/${filename}: ${fileError.message}`);
704
+ }
705
+ }
706
+ } catch (error) {
707
+ this.log.warn(`Error processing package URLs for ${npmPackage.name}#${npmPackage.version}:`, error.message);
708
+ }
709
+
710
+ // Include main package URL
711
+ if (npmPackage.url) {
712
+ urls.push(npmPackage.url);
713
+ }
714
+
715
+ return urls;
716
+ }
717
+
718
+ genHash(data) {
719
+ return crypto.createHash('sha1').update(data).digest('hex');
720
+ }
721
+
722
+ async commit(packageBuffer, npmPackage, date, guid, id, version, canonical, urls) {
723
+ return new Promise((resolve, reject) => {
724
+ // Get next version key
725
+ this.db.get('SELECT MAX(PackageVersionKey) as maxKey FROM PackageVersions', (err, row) => {
726
+ if (err) {
727
+ reject(err);
728
+ return;
729
+ }
730
+
731
+ const vkey = (row?.maxKey || 0) + 1;
732
+ const hash = this.genHash(packageBuffer);
733
+
734
+ // Insert package version
735
+ const insertVersionSql = `
736
+ INSERT INTO PackageVersions
737
+ (PackageVersionKey, GUID, PubDate, Indexed, Id, Version, Kind, DownloadCount,
738
+ Canonical, FhirVersions, UploadCount, Description, ManualToken, Hash,
739
+ Author, License, HomePage, Content)
740
+ VALUES (?, ?, ?, datetime('now'), ?, ?, ?, 0, ?, ?, 1, ?, '', ?, ?, ?, ?, ?)
741
+ `;
742
+
743
+ this.db.run(insertVersionSql, [
744
+ vkey, guid, date.toISOString(), id, version, npmPackage.kind,
745
+ canonical, npmPackage.fhirVersionList, npmPackage.description,
746
+ hash, npmPackage.author, npmPackage.license, npmPackage.url,
747
+ packageBuffer
748
+ ], (err) => {
749
+ if (err) {
750
+ reject(err);
751
+ return;
752
+ }
753
+
754
+ // Insert FHIR versions, dependencies, and URLs
755
+ this.insertRelatedData(vkey, npmPackage, urls).then(() => {
756
+ // Handle package table (insert or update)
757
+ this.upsertPackage(id, vkey, canonical).then(resolve).catch(reject);
758
+ }).catch(reject);
759
+ });
760
+ });
761
+ });
762
+ }
763
+
764
+ async insertRelatedData(vkey, npmPackage, urls) {
765
+ const promises = [];
766
+
767
+ // Insert FHIR versions
768
+ if (npmPackage.fhirVersionList) {
769
+ const fhirVersions = npmPackage.fhirVersionList.split(',');
770
+ for (const fver of fhirVersions) {
771
+ promises.push(new Promise((resolve, reject) => {
772
+ this.db.run('INSERT INTO PackageFHIRVersions (PackageVersionKey, Version) VALUES (?, ?)',
773
+ [vkey, fver.trim()], (err) => err ? reject(err) : resolve());
774
+ }));
775
+ }
776
+ }
777
+
778
+ // Insert dependencies
779
+ for (const dep of npmPackage.dependencies) {
780
+ promises.push(new Promise((resolve, reject) => {
781
+ this.db.run('INSERT INTO PackageDependencies (PackageVersionKey, Dependency) VALUES (?, ?)',
782
+ [vkey, dep], (err) => err ? reject(err) : resolve());
783
+ }));
784
+ }
785
+
786
+ // Insert URLs
787
+ for (const url of urls) {
788
+ promises.push(new Promise((resolve, reject) => {
789
+ this.db.run('INSERT INTO PackageURLs (PackageVersionKey, URL) VALUES (?, ?)',
790
+ [vkey, url], (err) => err ? reject(err) : resolve());
791
+ }));
792
+ }
793
+
794
+ return Promise.all(promises);
795
+ }
796
+
797
+ async upsertPackage(id, vkey, canonical) {
798
+ return new Promise((resolve, reject) => {
799
+ // Check if package exists
800
+ this.db.get('SELECT MAX(PackageKey) as pkey FROM Packages WHERE Id = ?', [id], (err, row) => {
801
+ if (err) {
802
+ reject(err);
803
+ return;
804
+ }
805
+
806
+ if (!row?.pkey) {
807
+ // Insert new package
808
+ this.db.get('SELECT MAX(PackageKey) as maxKey FROM Packages', (err, maxRow) => {
809
+ if (err) {
810
+ reject(err);
811
+ return;
812
+ }
813
+
814
+ const pkey = (maxRow?.maxKey || 0) + 1;
815
+ this.db.run('INSERT INTO Packages (PackageKey, Id, CurrentVersion, DownloadCount, Canonical) VALUES (?, ?, ?, 0, ?)',
816
+ [pkey, id, vkey, canonical], (err) => err ? reject(err) : resolve());
817
+ });
818
+ } else {
819
+ // Update existing package - check if this is the most recent version
820
+ this.db.get(`
821
+ SELECT PackageVersionKey
822
+ FROM PackageVersions
823
+ WHERE Id = ?
824
+ AND Version != 'current'
825
+ ORDER BY PubDate DESC, Version DESC LIMIT 1
826
+ `, [id], (err, latestRow) => {
827
+ if (err) {
828
+ reject(err);
829
+ return;
830
+ }
831
+
832
+ if (latestRow?.PackageVersionKey === vkey) {
833
+ // This is the most recent version, update the package
834
+ this.db.run('UPDATE Packages SET Canonical = ?, CurrentVersion = ? WHERE Id = ?',
835
+ [canonical, vkey, id], (err) => err ? reject(err) : resolve());
836
+ } else {
837
+ resolve(); // Not the most recent, no update needed
838
+ }
839
+ });
840
+ }
841
+ });
842
+ });
843
+ }
844
+ }
845
+
846
+ module.exports = PackageCrawler;