Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
364 changes: 173 additions & 191 deletions registry/api.js

Large diffs are not rendered by default.

130 changes: 72 additions & 58 deletions registry/crawler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ServerInformation,
ServerVersionInformation,
} = require('./model');
const {Extensions} = require("../tx/library/extensions");

const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';

Expand Down Expand Up @@ -36,32 +37,6 @@
this.log = logv;
}

// /**
// * Start the crawler with periodic updates
// */
// start() {
// if (this.crawlTimer) {
// return; // Already running
// }
//
// // Initial crawl
// this.crawl();
//
// // Set up periodic crawling
// this.crawlTimer = setInterval(() => {
// this.crawl();
// }, this.config.crawlInterval);
// }
//
// /**
// * Stop the crawler
// */
// stop() {
// if (this.crawlTimer) {
// clearInterval(this.crawlTimer);
// this.crawlTimer = null;
// }
// }

/**
* Main entry point - crawl the registry starting from the master URL
Expand Down Expand Up @@ -111,6 +86,7 @@
// Update the current data
this.currentData = newData;
} catch (error) {
console.log(error);
this.addLogEntry('error', 'Exception Scanning:', error);
this.currentData.outcome = `Error: ${error.message}`;
this.errors.push({
Expand Down Expand Up @@ -165,6 +141,7 @@
}

} catch (error) {
console.log(error);
registry.error = error.message;
this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
}
Expand All @@ -181,7 +158,7 @@
server.name = serverConfig.name;
server.address = serverConfig.url || '';
server.accessInfo = serverConfig.access_info || '';

if (!server.name) {
this.addLogEntry('error', 'No name provided for server', source);
return server;
Expand All @@ -200,7 +177,7 @@
// Process each FHIR version
const fhirVersions = serverConfig.fhirVersions || [];
for (const versionConfig of fhirVersions) {
const version = await this.processServerVersion(versionConfig, server);
const version = await this.processServerVersion(versionConfig, server, serverConfig.exclusions);
if (version) {
server.versions.push(version);
}
Expand All @@ -212,7 +189,7 @@
/**
* Process a single server version
*/
async processServerVersion(versionConfig, server) {
async processServerVersion(versionConfig, server, exclusions) {
const version = new ServerVersionInformation();
version.version = versionConfig.version;
version.address = versionConfig.url;
Expand All @@ -233,27 +210,28 @@

switch (majorVersion) {
case 3:
await this.processServerVersionR3(version, server);
await this.processServerVersionR3(version, server, exclusions);
break;
case 4:
await this.processServerVersionR4(version, server);
await this.processServerVersionR4or5(version, server, '4.0.1', exclusions);
break;
case 5:
await this.processServerVersionR5(version, server);
await this.processServerVersionR4or5(version, server, '5.0.0', exclusions);
break;
default:
throw new Error(`Version ${version.version} not supported`);
}

// Sort and deduplicate
version.codeSystems = [...new Set(version.codeSystems)].sort();
version.codeSystems.sort((a, b) => this.compareCS(a, b));
version.valueSets = [...new Set(version.valueSets)].sort();
version.lastSuccess = new Date();
version.lastTat = `${Date.now() - startTime}ms`;

this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);

} catch (error) {
console.log(error);
const elapsed = Date.now() - startTime;
this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
version.error = error.message;
Expand All @@ -266,7 +244,7 @@
/**
* Process an R3 server
*/
async processServerVersionR3(version, server) {
async processServerVersionR3(version, server, exclusions) {
// Get capability statement
const capabilityUrl = `${version.address}/metadata`;
const capability = await this.fetchJson(capabilityUrl, server.name);
Expand All @@ -283,12 +261,12 @@
termCap.parameter.forEach(param => {
if (param.name === 'system') {
const uri = param.valueUri || param.valueString;
if (uri) {
if (uri && !this.isExcluded(uri, exclusions)) {
version.codeSystems.push(uri);
// Look for version parts
if (param.part) {
param.part.forEach(part => {
if (part.name === 'version' && part.valueString) {
if (part.name === 'version' && part.valueString && !this.isExcluded(uri+'|'+part.valueString, exclusions)) {
version.codeSystems.push(`${uri}|${part.valueString}`);
}
});
Expand All @@ -298,58 +276,60 @@
});
}
} catch (error) {
console.log(error);
this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
}

// Search for value sets
await this.fetchValueSets(version, server);
await this.fetchValueSets(version, server, exclusions);
}

/**
* Process an R4 server
*/
async processServerVersionR4(version, server) {
async processServerVersionR4or5(version, server, defVersion, exclusions) {
// Get capability statement
const capabilityUrl = `${version.address}/metadata`;
const capability = await this.fetchJson(capabilityUrl, server.code);

version.version = capability.fhirVersion || '4.0.1';
version.version = capability.fhirVersion || defVersion;
version.software = capability.software ? capability.software.name : "unknown";


let set = new Set();

// Get terminology capabilities
try {
const termCapUrl = `${version.address}/metadata?mode=terminology`;
const termCap = await this.fetchJson(termCapUrl, server.code);

if (termCap.codeSystem) {
termCap.codeSystem.forEach(cs => {
if (cs.uri) {
version.codeSystems.push(cs.uri);
let content = cs.content || Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
if (cs.uri && !this.isExcluded(cs.uri, exclusions)) {
if (!set.has(cs.uri)) {
set.add(cs.uri);
version.codeSystems.push(this.addContent({uri: cs.uri}, content));
}
if (cs.version) {
cs.version.forEach(v => {
if (v.code) {
version.codeSystems.push(`${cs.uri}|${v.code}`);
if (v.code && !this.isExcluded(cs.uri+"|"+v.code, exclusions)) {
if (!set.has(cs.uri+"|"+v.code)) {
version.codeSystems.push(this.addContent({uri: cs.uri, version: v.code}, content));
set.add(cs.uri+"|"+v.code);
}
}
});
}
}
});
}
} catch (error) {
console.log(error);
this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
}

// Search for value sets
await this.fetchValueSets(version, server);
}

/**
* Process an R5 server
*/
async processServerVersionR5(version, server) {
// R5 is essentially the same as R4 for our purposes
await this.processServerVersionR4(version, server);
version.version = version.version || '5.0.0';
await this.fetchValueSets(version, server, exclusions);
}

/**
Expand All @@ -360,9 +340,9 @@
* @param {Object} version - The server version information
* @param {Object} server - The server information
*/
async fetchValueSets(version, server) {
async fetchValueSets(version, server, exclusions) {
// Initial search URL
let searchUrl = `${version.address}/ValueSet?_elements=url,version`;
let searchUrl = `${version.address}/ValueSet?_elements=url,version`+(version.address.includes("fhir.org") ? "&_count=200" : "");
try {
// Set of URLs to avoid duplicates
const valueSetUrls = new Set();
Expand All @@ -378,9 +358,9 @@
bundle.entry.forEach(entry => {
if (entry.resource) {
const vs = entry.resource;
if (vs.url) {
if (vs.url && !this.isExcluded(vs.url, exclusions)) {
valueSetUrls.add(vs.url);
if (vs.version) {
if (vs.version && !this.isExcluded(vs.url+'|'+vs.version, exclusions)) {
valueSetUrls.add(`${vs.url}|${vs.version}`);
}
}
Expand All @@ -402,6 +382,7 @@
version.valueSets = Array.from(valueSetUrls).sort();

} catch (error) {
console.log(error);
this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
}
}
Expand Down Expand Up @@ -478,6 +459,7 @@
return response.data;

} catch (error) {
console.log(error);
if (error.response) {
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
} else if (error.request) {
Expand Down Expand Up @@ -634,6 +616,38 @@
return filteredLogs.slice(-limit);
}

addContent(param, content) {
if (content) {
param.content = content;
}
return param;
}

compareCS(a, b) {
if (a.version || b.version) {
let s = (a.uri+'|'+a.version) || '';
return s.localeCompare(b.uri+'|'+b.version);
} else {
return (a.uri || '').localeCompare(b.uri);
}
}

isExcluded(url, exclusions) {
for (let exclusion of exclusions || []) {
let match = false;
if (exclusion.endsWith('*')) {
const prefix = exclusion.slice(0, -1);
match = url.startsWith(prefix);
} else {
// Otherwise do exact matching on both full and base URL
match = url === exclusion;
}
if (match) {
return true;
}
}
return false;
}
}

module.exports = RegistryCrawler;
22 changes: 14 additions & 8 deletions registry/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ServerVersionInformation {
getCsListHtml() {
if (this.codeSystems.length === 0) return '<ul></ul>';
return '<ul>' + this.codeSystems.map(cs =>
`<li>${escape(cs)}</li>`
`<li>${escape(cs.uri+(cs.version ? '|'+cs.version : ''))}</li>`
).join('') + '</ul>';
}

Expand Down Expand Up @@ -392,7 +392,7 @@ class ServerRegistryUtilities {
return value === mask;
}

static hasMatchingCodeSystem(cs, list, supportMask) {
static hasMatchingCodeSystem(cs, list, supportMask, content) {
if (!cs || list.length === 0) return false;

// Handle URLs with pipes - extract base URL
Expand All @@ -403,13 +403,19 @@ class ServerRegistryUtilities {

return list.some(item => {
// If we support wildcards (masks) and the item ends with "*", do prefix matching
if (supportMask && item.endsWith('*')) {
const prefix = item.slice(0, -1);
return cs.startsWith(prefix) || baseCs.startsWith(prefix);
let vurl = item.uri ? item.version ? item.uri+"|"+item.version : item.uri : item;
let ok = false;
if (supportMask && vurl.endsWith('*')) {
const prefix = vurl.slice(0, -1);
ok = cs.startsWith(prefix) || baseCs.startsWith(prefix);
} else {
// Otherwise do exact matching on both full and base URL
ok = vurl === cs || vurl === baseCs;
}

// Otherwise do exact matching on both full and base URL
return item === cs || item === baseCs;
if (ok && content) {
content.content = item.content;
}
return ok;
});
}

Expand Down
2 changes: 2 additions & 0 deletions registry/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,7 @@ class RegistryModule {
html += '<th>URL</th>';
html += '<th>Security</th>';
html += '<th>Access Info</th>';
html += '<th>Content</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
Expand All @@ -1165,6 +1166,7 @@ class RegistryModule {
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
html += `<td>${this.renderSecurityTags(server)}</td>`;
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
html += `<td>${server.content ? escape(server.content) : ''}</td>`;
html += '</tr>';
});

Expand Down
Loading
Loading