fhirsmith 0.7.3 → 0.7.5
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 +31 -0
- package/package.json +1 -1
- package/publisher/publisher.js +7 -6
- package/translations/Messages.properties +2 -0
- package/tx/cs/cs-country.js +804 -801
- package/tx/cs/cs-snomed.js +1 -1
- package/tx/cs/cs-uri.js +3 -3
- package/tx/library.js +30 -2
- package/tx/tx.fhir.org.yml +4 -0
- package/tx/tx.js +94 -18
- package/tx/workers/validate.js +11 -6
- package/tx/workers/worker.js +2 -5
- package/xig/xig.js +6 -2
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
|
|
@@ -100,6 +101,7 @@ class Library {
|
|
|
100
101
|
this.conceptMapProviders = [];
|
|
101
102
|
this.oclProviderSets = new Map();
|
|
102
103
|
this.oclConfig = {};
|
|
104
|
+
this.ignored = new Set();
|
|
103
105
|
|
|
104
106
|
// Create package manager for FHIR packages
|
|
105
107
|
const packageServers = ['https://packages2.fhir.org/packages'];
|
|
@@ -160,6 +162,7 @@ class Library {
|
|
|
160
162
|
const config = yaml.parse(yamlContent);
|
|
161
163
|
this.baseUrl = config.base.url;
|
|
162
164
|
this.oclConfig = config.ocl && typeof config.ocl === 'object' ? config.ocl : {};
|
|
165
|
+
this.ignored = new Set(Array.isArray(config.ignored) ? config.ignored : []);
|
|
163
166
|
|
|
164
167
|
this.log.info('Fetching Data from '+this.baseUrl);
|
|
165
168
|
|
|
@@ -287,7 +290,7 @@ class Library {
|
|
|
287
290
|
case 'ocl':
|
|
288
291
|
await this.loadOcl(details, isDefault, mode);
|
|
289
292
|
break;
|
|
290
|
-
|
|
293
|
+
|
|
291
294
|
default:
|
|
292
295
|
throw new Error(`Unknown source type: ${type}`);
|
|
293
296
|
}
|
|
@@ -454,6 +457,12 @@ class Library {
|
|
|
454
457
|
this.registerProvider('internal', hgvs);
|
|
455
458
|
break;
|
|
456
459
|
}
|
|
460
|
+
case "urls" : {
|
|
461
|
+
const urls = new UriServicesFactory(this.i18n);
|
|
462
|
+
await urls.load();
|
|
463
|
+
this.registerProvider('internal', urls);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
457
466
|
case "vsac" : {
|
|
458
467
|
if (!this.vsacCfg || !this.vsacCfg.apiKey) {
|
|
459
468
|
throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
|
|
@@ -560,6 +569,17 @@ class Library {
|
|
|
560
569
|
this.registerProvider(omopFN, omop, isDefault);
|
|
561
570
|
}
|
|
562
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Returns true if the given url/version should be excluded from npm/url package loading.
|
|
574
|
+
* Matches against the ignored list using either plain url or url#version.
|
|
575
|
+
*/
|
|
576
|
+
#isIgnored(url, version) {
|
|
577
|
+
if (this.ignored.size === 0) return false;
|
|
578
|
+
if (this.ignored.has(url)) return true;
|
|
579
|
+
if (version && this.ignored.has(`${url}#${version}`)) return true;
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
563
583
|
async loadNpm(packageManager, details, isDefault, mode, csOnly) {
|
|
564
584
|
// Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2")
|
|
565
585
|
let packageId = details;
|
|
@@ -584,6 +604,10 @@ class Library {
|
|
|
584
604
|
let csc = 0;
|
|
585
605
|
for (const resource of resources) {
|
|
586
606
|
const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
|
|
607
|
+
if (this.#isIgnored(cs.url, cs.version)) {
|
|
608
|
+
this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
587
611
|
cs.sourcePackage = contentLoader.pid();
|
|
588
612
|
cp.codeSystems.push(cs);
|
|
589
613
|
csc++;
|
|
@@ -618,6 +642,10 @@ class Library {
|
|
|
618
642
|
let csc = 0;
|
|
619
643
|
for (const resource of resources) {
|
|
620
644
|
const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
|
|
645
|
+
if (this.#isIgnored(cs.url, cs.version)) {
|
|
646
|
+
this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
621
649
|
cs.sourcePackage = contentLoader.pid();
|
|
622
650
|
cp.codeSystems.set(cs.url, cs);
|
|
623
651
|
cp.codeSystems.set(cs.vurl, cs);
|
|
@@ -898,4 +926,4 @@ class Library {
|
|
|
898
926
|
|
|
899
927
|
}
|
|
900
928
|
|
|
901
|
-
module.exports = { Library };
|
|
929
|
+
module.exports = { Library };
|
package/tx/tx.fhir.org.yml
CHANGED
|
@@ -8,6 +8,7 @@ sources:
|
|
|
8
8
|
- internal:areacode
|
|
9
9
|
- internal:mimetypes
|
|
10
10
|
- internal:usstates
|
|
11
|
+
- internal:urls
|
|
11
12
|
- internal:hgvs
|
|
12
13
|
- ucum:tx/data/ucum-essence.xml
|
|
13
14
|
- loinc:loinc-2.77-a.db
|
|
@@ -39,3 +40,6 @@ sources:
|
|
|
39
40
|
- npm:us.cdc.phinvads
|
|
40
41
|
- npm:hl7.fhir.uv.sdc
|
|
41
42
|
- internal:vsac
|
|
43
|
+
|
|
44
|
+
ignored:
|
|
45
|
+
- urn:iso:std:iso:3166#20210120
|
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/tx/workers/validate.js
CHANGED
|
@@ -1389,17 +1389,22 @@ class ValueSetChecker {
|
|
|
1389
1389
|
let hd = list.hasDisplay(this.params.workingLanguages(), null, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE)
|
|
1390
1390
|
if (!hd.found) {
|
|
1391
1391
|
let baseMsg;
|
|
1392
|
-
|
|
1392
|
+
let severity = this.dispWarning();
|
|
1393
|
+
if (list.userDefined) {
|
|
1394
|
+
baseMsg = 'Display_Name_Not_Fixed_use_Supplement';
|
|
1395
|
+
severity = 'information';
|
|
1396
|
+
} else if (hd.difference === DisplayDifference.Normalized) {
|
|
1393
1397
|
baseMsg = 'Display_Name_WS_for__should_be_one_of__instead_of';
|
|
1394
1398
|
} else {
|
|
1395
1399
|
baseMsg = 'Display_Name_for__should_be_one_of__instead_of';
|
|
1396
1400
|
}
|
|
1397
|
-
let mid = baseMsg;
|
|
1398
1401
|
let dc = list.displayCount(this.params.workingLanguages(), null, true);
|
|
1399
|
-
let
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
|
|
1402
|
+
let mid = baseMsg;
|
|
1403
|
+
if (severity !== 'information') {
|
|
1404
|
+
if (dc === 0) {
|
|
1405
|
+
severity = 'warning';
|
|
1406
|
+
dc = list.displayCount(this.params.workingLanguages(), null, false);
|
|
1407
|
+
}
|
|
1403
1408
|
}
|
|
1404
1409
|
|
|
1405
1410
|
let m, ds;
|
package/tx/workers/worker.js
CHANGED
|
@@ -276,12 +276,9 @@ class TerminologyWorker {
|
|
|
276
276
|
if (this.hasSupplement(cs, supplements)) {
|
|
277
277
|
continue;
|
|
278
278
|
}
|
|
279
|
-
// Handle exact URL match (no version specified in supplements)
|
|
279
|
+
// Handle exact URL match (no version specified in supplements field)
|
|
280
280
|
if (supplementsUrl === url) {
|
|
281
|
-
|
|
282
|
-
if (!version) {
|
|
283
|
-
supplements.push(cs);
|
|
284
|
-
}
|
|
281
|
+
supplements.push(cs);
|
|
285
282
|
continue;
|
|
286
283
|
}
|
|
287
284
|
|
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)}`;
|