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.
- package/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- 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;
|