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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to the Health Intersections Node Server will be documented i
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.7.4] - 2026-03-19

### Changed

- XIG: show using resource package explicitly
- TX: Check conformance statement production at start up

### Fixed
- TX: Load URI provider on tx.fhir.org
- TX: fix error getting SCT version for html format

### Tx Conformance Statement

FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.9.0)

## [v0.7.3] - 2026-03-19

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fhirsmith",
"version": "0.7.3",
"version": "0.7.4",
"description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
"main": "server.js",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion tx/cs/cs-snomed.js
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {

id() {
const match = this.version().match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
return "SCT-"+match[1]+"-"+match[2];
return match && match[1] && match[2] ? "SCT-"+match[1]+"-"+match[2] : null;
}

describeVersion(version) {
Expand Down
6 changes: 3 additions & 3 deletions tx/cs/cs-uri.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class UriServices extends CodeSystemProvider {
}

version() {
return 'n/a';
return null;
}

description() {
Expand Down Expand Up @@ -182,15 +182,15 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
}

defaultVersion() {
return 'n/a';
return null;
}

system() {
return 'urn:ietf:rfc:3986'; // URI_URIs constant equivalent
}

version() {
return 'n/a';
return null;
}

// eslint-disable-next-line no-unused-vars
Expand Down
7 changes: 7 additions & 0 deletions tx/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const {VSACValueSetProvider} = require("./vs/vs-vsac");
const { OCLCodeSystemProvider, OCLSourceCodeSystemFactory } = require('./ocl/cs-ocl');
const { OCLValueSetProvider } = require('./ocl/vs-ocl');
const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
const {UriServicesFactory} = require("./cs/cs-uri");

/**
* This class holds all the loaded content ready for processing
Expand Down Expand Up @@ -454,6 +455,12 @@ class Library {
this.registerProvider('internal', hgvs);
break;
}
case "urls" : {
const urls = new UriServicesFactory(this.i18n);
await urls.load();
this.registerProvider('internal', urls);
break;
}
case "vsac" : {
if (!this.vsacCfg || !this.vsacCfg.apiKey) {
throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
Expand Down
1 change: 1 addition & 0 deletions tx/tx.fhir.org.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sources:
- internal:areacode
- internal:mimetypes
- internal:usstates
- internal:urls
- internal:hgvs
- ucum:tx/data/ucum-essence.xml
- loinc:loinc-2.77-a.db
Expand Down
112 changes: 94 additions & 18 deletions tx/tx.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ class TXModule {
}

this.log.info(`TX module initialized with ${config.endpoints.length} endpoint(s)`);

// Self-test: verify metadata generation works for each endpoint before accepting traffic
await this.selfTest();
}

/**
Expand Down Expand Up @@ -388,8 +391,8 @@ class TXModule {
}

if (contentType.includes('application/json') ||
contentType.includes('application/fhir+json') ||
contentType.includes('application/json+fhir')) {
contentType.includes('application/fhir+json') ||
contentType.includes('application/json+fhir')) {

// If body is a Buffer, parse it
if (Buffer.isBuffer(req.body)) {
Expand Down Expand Up @@ -731,11 +734,11 @@ class TXModule {
router.get('/CodeSystem/:id/\\$validate-code', async (req, res) => {
const start = Date.now();
try {
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
await worker.handleCodeSystemInstance(req, res, this.log);
} finally {
this.countRequest('$validate', Date.now() - start);
}
} finally {
this.countRequest('$validate', Date.now() - start);
}
});
router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
const start = Date.now();
Expand All @@ -745,7 +748,7 @@ class TXModule {
} finally {
this.countRequest('$validate', Date.now() - start);
}

});

// ValueSet/[id]/$validate-code
Expand Down Expand Up @@ -964,6 +967,79 @@ class TXModule {
});
}

/**
* Self-test: exercise CapabilityStatement and TerminologyCapabilities generation
* for each endpoint immediately after startup, throwing on any failure.
*/
async selfTest() {
this.log.info('Running startup self-test for metadata endpoints...');

for (const endpointInfo of this.endpoints) {
const label = `${endpointInfo.path} (FHIR v${endpointInfo.fhirVersion})`;

// Build a minimal mock req/res that captures what metadataHandler.handle() produces
const makeMockReqRes = (mode) => {
const captured = { data: null, status: 200 };

const req = {
method: 'GET',
query: { mode },
headers: {},
// eslint-disable-next-line no-unused-vars
get: (name) => null,
txEndpoint: endpointInfo,
txProvider: endpointInfo.provider,
};

const res = {
statusCode: 200,
status(code) { captured.status = code; return this; },
setHeader() { return this; },
json(data) { captured.data = data; return this; },
send(data) { captured.data = data; return this; },
};

return { req, res, captured };
};

// Test 1: CapabilityStatement (/metadata with no mode, or mode=full)
try {
const { req, res, captured } = makeMockReqRes(undefined);
await this.metadataHandler.handle(req, res);
if (!captured.data) {
throw new Error('No response data returned');
}
const rt = captured.data.resourceType;
if (rt !== 'CapabilityStatement') {
throw new Error(`Expected CapabilityStatement, got ${rt}`);
}
this.log.info(` [OK] CapabilityStatement for ${label}`);
} catch (err) {
this.log.error(` [FAIL] CapabilityStatement for ${label}: ${err.message}`);
throw new Error(`Startup self-test failed (CapabilityStatement, ${label}): ${err.message}`);
}

// Test 2: TerminologyCapabilities (/metadata?mode=terminology)
try {
const { req, res, captured } = makeMockReqRes('terminology');
await this.metadataHandler.handle(req, res);
if (!captured.data) {
throw new Error('No response data returned');
}
const rt = captured.data.resourceType;
if (rt !== 'TerminologyCapabilities') {
throw new Error(`Expected TerminologyCapabilities, got ${rt}`);
}
this.log.info(` [OK] TerminologyCapabilities for ${label}`);
} catch (err) {
this.log.error(` [FAIL] TerminologyCapabilities for ${label}: ${err.message}`);
throw new Error(`Startup self-test failed (TerminologyCapabilities, ${label}): ${err.message}`);
}
}

this.log.info('Startup self-test passed.');
}

/**
* Build an OperationOutcome for errors
*/
Expand Down Expand Up @@ -1077,21 +1153,21 @@ class TXModule {
ec = 0;

checkProperJson() { // jsonStr) {
// const errors = [];
// if (jsonStr.includes("[]")) errors.push("Found [] in json");
// if (jsonStr.includes('""')) errors.push('Found "" in json');
//
// if (errors.length > 0) {
// this.ec++;
// const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
// writeFileSync(filename, jsonStr);
// throw new Error(errors.join('; '));
// }
// const errors = [];
// if (jsonStr.includes("[]")) errors.push("Found [] in json");
// if (jsonStr.includes('""')) errors.push('Found "" in json');
//
// if (errors.length > 0) {
// this.ec++;
// const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
// writeFileSync(filename, jsonStr);
// throw new Error(errors.join('; '));
// }
}

transformResourceForVersion(data, fhirVersion) {
if (fhirVersion == "5.0" || !data.resourceType) {
return data;
return data;
}
switch (data.resourceType) {
case "CodeSystem": return codeSystemFromR5(data, fhirVersion);
Expand Down
8 changes: 6 additions & 2 deletions xig/xig.js
Original file line number Diff line number Diff line change
Expand Up @@ -1241,13 +1241,14 @@ function buildAdditionalForm(queryParams) {
}
}


// Add text search field and package filter field
html += `Text: <input type="text" name="text" value="${escape(text || '')}" class="" style="width: 200px;"/> `;
html += `Package: <input type="text" name="pkg" value="${escape(pkg || '')}" placeholder="e.g. hl7.fhir.us" class="" style="width: 200px;"/> `;

// Add submit button with 'only used' checkbox immediately before it
const onlyUsedChecked = onlyUsed === 'true' ? ' checked' : '';
html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Used `;
html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Show Used `;
html += '<input type="submit" value="Search" style="color:rgb(89, 137, 241)"/>';

html += '</form>';
Expand Down Expand Up @@ -2747,11 +2748,14 @@ function buildDependencyTable(dependencies) {
}
currentType = dep.ResourceType;
html += '<table class="table table-bordered">';
html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${escape(currentType)}</strong></td></tr>`;
html += `<tr style="background-color: #eeeeee"><td colspan="3"><strong>${escape(currentType)}</strong></td></tr>`;
}

html += '<tr>';

// Package column
html += `<td>${escape(dep.PID || '')}</td>`;

// Build the link to the resource detail page
const packagePid = dep.PID.replace(/#/g, '|'); // Convert # to | for URL
const resourceUrl = `/xig/resource/${encodeURIComponent(packagePid)}/${encodeURIComponent(dep.ResourceType)}/${encodeURIComponent(dep.Id)}`;
Expand Down
Loading