fhirsmith 0.7.3 → 0.7.4
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 +15 -0
- package/package.json +1 -1
- package/tx/cs/cs-snomed.js +1 -1
- package/tx/cs/cs-uri.js +3 -3
- package/tx/library.js +7 -0
- package/tx/tx.fhir.org.yml +1 -0
- package/tx/tx.js +94 -18
- package/xig/xig.js +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to the Health Intersections Node Server will be documented i
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [v0.7.4] - 2026-03-19
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- XIG: show using resource package explicitly
|
|
13
|
+
- TX: Check conformance statement production at start up
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- TX: Load URI provider on tx.fhir.org
|
|
17
|
+
- TX: fix error getting SCT version for html format
|
|
18
|
+
|
|
19
|
+
### Tx Conformance Statement
|
|
20
|
+
|
|
21
|
+
FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.9.0)
|
|
22
|
+
|
|
8
23
|
## [v0.7.3] - 2026-03-19
|
|
9
24
|
|
|
10
25
|
### Changed
|
package/package.json
CHANGED
package/tx/cs/cs-snomed.js
CHANGED
|
@@ -1275,7 +1275,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1275
1275
|
|
|
1276
1276
|
id() {
|
|
1277
1277
|
const match = this.version().match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
|
|
1278
|
-
return "SCT-"+match[1]+"-"+match[2];
|
|
1278
|
+
return match && match[1] && match[2] ? "SCT-"+match[1]+"-"+match[2] : null;
|
|
1279
1279
|
}
|
|
1280
1280
|
|
|
1281
1281
|
describeVersion(version) {
|
package/tx/cs/cs-uri.js
CHANGED
|
@@ -21,7 +21,7 @@ class UriServices extends CodeSystemProvider {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
version() {
|
|
24
|
-
return
|
|
24
|
+
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
description() {
|
|
@@ -182,7 +182,7 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
defaultVersion() {
|
|
185
|
-
return
|
|
185
|
+
return null;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
system() {
|
|
@@ -190,7 +190,7 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
version() {
|
|
193
|
-
return
|
|
193
|
+
return null;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// eslint-disable-next-line no-unused-vars
|
package/tx/library.js
CHANGED
|
@@ -34,6 +34,7 @@ const {VSACValueSetProvider} = require("./vs/vs-vsac");
|
|
|
34
34
|
const { OCLCodeSystemProvider, OCLSourceCodeSystemFactory } = require('./ocl/cs-ocl');
|
|
35
35
|
const { OCLValueSetProvider } = require('./ocl/vs-ocl');
|
|
36
36
|
const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
|
|
37
|
+
const {UriServicesFactory} = require("./cs/cs-uri");
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* This class holds all the loaded content ready for processing
|
|
@@ -454,6 +455,12 @@ class Library {
|
|
|
454
455
|
this.registerProvider('internal', hgvs);
|
|
455
456
|
break;
|
|
456
457
|
}
|
|
458
|
+
case "urls" : {
|
|
459
|
+
const urls = new UriServicesFactory(this.i18n);
|
|
460
|
+
await urls.load();
|
|
461
|
+
this.registerProvider('internal', urls);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
457
464
|
case "vsac" : {
|
|
458
465
|
if (!this.vsacCfg || !this.vsacCfg.apiKey) {
|
|
459
466
|
throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
|
package/tx/tx.fhir.org.yml
CHANGED
package/tx/tx.js
CHANGED
|
@@ -196,6 +196,9 @@ class TXModule {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
this.log.info(`TX module initialized with ${config.endpoints.length} endpoint(s)`);
|
|
199
|
+
|
|
200
|
+
// Self-test: verify metadata generation works for each endpoint before accepting traffic
|
|
201
|
+
await this.selfTest();
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
/**
|
|
@@ -388,8 +391,8 @@ class TXModule {
|
|
|
388
391
|
}
|
|
389
392
|
|
|
390
393
|
if (contentType.includes('application/json') ||
|
|
391
|
-
|
|
392
|
-
|
|
394
|
+
contentType.includes('application/fhir+json') ||
|
|
395
|
+
contentType.includes('application/json+fhir')) {
|
|
393
396
|
|
|
394
397
|
// If body is a Buffer, parse it
|
|
395
398
|
if (Buffer.isBuffer(req.body)) {
|
|
@@ -731,11 +734,11 @@ class TXModule {
|
|
|
731
734
|
router.get('/CodeSystem/:id/\\$validate-code', async (req, res) => {
|
|
732
735
|
const start = Date.now();
|
|
733
736
|
try {
|
|
734
|
-
|
|
737
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
735
738
|
await worker.handleCodeSystemInstance(req, res, this.log);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
+
} finally {
|
|
740
|
+
this.countRequest('$validate', Date.now() - start);
|
|
741
|
+
}
|
|
739
742
|
});
|
|
740
743
|
router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
|
|
741
744
|
const start = Date.now();
|
|
@@ -745,7 +748,7 @@ class TXModule {
|
|
|
745
748
|
} finally {
|
|
746
749
|
this.countRequest('$validate', Date.now() - start);
|
|
747
750
|
}
|
|
748
|
-
|
|
751
|
+
|
|
749
752
|
});
|
|
750
753
|
|
|
751
754
|
// ValueSet/[id]/$validate-code
|
|
@@ -964,6 +967,79 @@ class TXModule {
|
|
|
964
967
|
});
|
|
965
968
|
}
|
|
966
969
|
|
|
970
|
+
/**
|
|
971
|
+
* Self-test: exercise CapabilityStatement and TerminologyCapabilities generation
|
|
972
|
+
* for each endpoint immediately after startup, throwing on any failure.
|
|
973
|
+
*/
|
|
974
|
+
async selfTest() {
|
|
975
|
+
this.log.info('Running startup self-test for metadata endpoints...');
|
|
976
|
+
|
|
977
|
+
for (const endpointInfo of this.endpoints) {
|
|
978
|
+
const label = `${endpointInfo.path} (FHIR v${endpointInfo.fhirVersion})`;
|
|
979
|
+
|
|
980
|
+
// Build a minimal mock req/res that captures what metadataHandler.handle() produces
|
|
981
|
+
const makeMockReqRes = (mode) => {
|
|
982
|
+
const captured = { data: null, status: 200 };
|
|
983
|
+
|
|
984
|
+
const req = {
|
|
985
|
+
method: 'GET',
|
|
986
|
+
query: { mode },
|
|
987
|
+
headers: {},
|
|
988
|
+
// eslint-disable-next-line no-unused-vars
|
|
989
|
+
get: (name) => null,
|
|
990
|
+
txEndpoint: endpointInfo,
|
|
991
|
+
txProvider: endpointInfo.provider,
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const res = {
|
|
995
|
+
statusCode: 200,
|
|
996
|
+
status(code) { captured.status = code; return this; },
|
|
997
|
+
setHeader() { return this; },
|
|
998
|
+
json(data) { captured.data = data; return this; },
|
|
999
|
+
send(data) { captured.data = data; return this; },
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
return { req, res, captured };
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// Test 1: CapabilityStatement (/metadata with no mode, or mode=full)
|
|
1006
|
+
try {
|
|
1007
|
+
const { req, res, captured } = makeMockReqRes(undefined);
|
|
1008
|
+
await this.metadataHandler.handle(req, res);
|
|
1009
|
+
if (!captured.data) {
|
|
1010
|
+
throw new Error('No response data returned');
|
|
1011
|
+
}
|
|
1012
|
+
const rt = captured.data.resourceType;
|
|
1013
|
+
if (rt !== 'CapabilityStatement') {
|
|
1014
|
+
throw new Error(`Expected CapabilityStatement, got ${rt}`);
|
|
1015
|
+
}
|
|
1016
|
+
this.log.info(` [OK] CapabilityStatement for ${label}`);
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
this.log.error(` [FAIL] CapabilityStatement for ${label}: ${err.message}`);
|
|
1019
|
+
throw new Error(`Startup self-test failed (CapabilityStatement, ${label}): ${err.message}`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Test 2: TerminologyCapabilities (/metadata?mode=terminology)
|
|
1023
|
+
try {
|
|
1024
|
+
const { req, res, captured } = makeMockReqRes('terminology');
|
|
1025
|
+
await this.metadataHandler.handle(req, res);
|
|
1026
|
+
if (!captured.data) {
|
|
1027
|
+
throw new Error('No response data returned');
|
|
1028
|
+
}
|
|
1029
|
+
const rt = captured.data.resourceType;
|
|
1030
|
+
if (rt !== 'TerminologyCapabilities') {
|
|
1031
|
+
throw new Error(`Expected TerminologyCapabilities, got ${rt}`);
|
|
1032
|
+
}
|
|
1033
|
+
this.log.info(` [OK] TerminologyCapabilities for ${label}`);
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
this.log.error(` [FAIL] TerminologyCapabilities for ${label}: ${err.message}`);
|
|
1036
|
+
throw new Error(`Startup self-test failed (TerminologyCapabilities, ${label}): ${err.message}`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
this.log.info('Startup self-test passed.');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
967
1043
|
/**
|
|
968
1044
|
* Build an OperationOutcome for errors
|
|
969
1045
|
*/
|
|
@@ -1077,21 +1153,21 @@ class TXModule {
|
|
|
1077
1153
|
ec = 0;
|
|
1078
1154
|
|
|
1079
1155
|
checkProperJson() { // jsonStr) {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1156
|
+
// const errors = [];
|
|
1157
|
+
// if (jsonStr.includes("[]")) errors.push("Found [] in json");
|
|
1158
|
+
// if (jsonStr.includes('""')) errors.push('Found "" in json');
|
|
1159
|
+
//
|
|
1160
|
+
// if (errors.length > 0) {
|
|
1161
|
+
// this.ec++;
|
|
1162
|
+
// const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
|
|
1163
|
+
// writeFileSync(filename, jsonStr);
|
|
1164
|
+
// throw new Error(errors.join('; '));
|
|
1165
|
+
// }
|
|
1090
1166
|
}
|
|
1091
1167
|
|
|
1092
1168
|
transformResourceForVersion(data, fhirVersion) {
|
|
1093
1169
|
if (fhirVersion == "5.0" || !data.resourceType) {
|
|
1094
|
-
|
|
1170
|
+
return data;
|
|
1095
1171
|
}
|
|
1096
1172
|
switch (data.resourceType) {
|
|
1097
1173
|
case "CodeSystem": return codeSystemFromR5(data, fhirVersion);
|
package/xig/xig.js
CHANGED
|
@@ -1241,13 +1241,14 @@ function buildAdditionalForm(queryParams) {
|
|
|
1241
1241
|
}
|
|
1242
1242
|
}
|
|
1243
1243
|
|
|
1244
|
+
|
|
1244
1245
|
// Add text search field and package filter field
|
|
1245
1246
|
html += `Text: <input type="text" name="text" value="${escape(text || '')}" class="" style="width: 200px;"/> `;
|
|
1246
1247
|
html += `Package: <input type="text" name="pkg" value="${escape(pkg || '')}" placeholder="e.g. hl7.fhir.us" class="" style="width: 200px;"/> `;
|
|
1247
1248
|
|
|
1248
1249
|
// Add submit button with 'only used' checkbox immediately before it
|
|
1249
1250
|
const onlyUsedChecked = onlyUsed === 'true' ? ' checked' : '';
|
|
1250
|
-
html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Used `;
|
|
1251
|
+
html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Show Used `;
|
|
1251
1252
|
html += '<input type="submit" value="Search" style="color:rgb(89, 137, 241)"/>';
|
|
1252
1253
|
|
|
1253
1254
|
html += '</form>';
|
|
@@ -2747,11 +2748,14 @@ function buildDependencyTable(dependencies) {
|
|
|
2747
2748
|
}
|
|
2748
2749
|
currentType = dep.ResourceType;
|
|
2749
2750
|
html += '<table class="table table-bordered">';
|
|
2750
|
-
html += `<tr style="background-color: #eeeeee"><td colspan="
|
|
2751
|
+
html += `<tr style="background-color: #eeeeee"><td colspan="3"><strong>${escape(currentType)}</strong></td></tr>`;
|
|
2751
2752
|
}
|
|
2752
2753
|
|
|
2753
2754
|
html += '<tr>';
|
|
2754
2755
|
|
|
2756
|
+
// Package column
|
|
2757
|
+
html += `<td>${escape(dep.PID || '')}</td>`;
|
|
2758
|
+
|
|
2755
2759
|
// Build the link to the resource detail page
|
|
2756
2760
|
const packagePid = dep.PID.replace(/#/g, '|'); // Convert # to | for URL
|
|
2757
2761
|
const resourceUrl = `/xig/resource/${encodeURIComponent(packagePid)}/${encodeURIComponent(dep.ResourceType)}/${encodeURIComponent(dep.Id)}`;
|