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