fhirsmith 0.6.0 → 0.7.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 +20 -0
- package/README.md +2 -0
- package/configurations/projector.json +21 -0
- package/configurations/readme.md +5 -0
- package/library/package-manager.js +0 -2
- package/library/version-utilities.js +85 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +44 -9
- package/packages/packages.js +1 -0
- package/registry/crawler.js +35 -14
- package/registry/registry.js +3 -0
- package/server.js +4 -0
- package/tx/README.md +4 -4
- package/tx/cs/cs-loinc.js +5 -2
- package/tx/cs/cs-provider-api.js +25 -1
- package/tx/cs/cs-provider-list.js +2 -2
- package/tx/library/canonical-resource.js +6 -1
- package/tx/library.js +127 -10
- package/tx/ocl/README.md +236 -0
- package/tx/ocl/cache/cache-paths.cjs +32 -0
- package/tx/ocl/cache/cache-paths.js +2 -0
- package/tx/ocl/cache/cache-utils.cjs +43 -0
- package/tx/ocl/cache/cache-utils.js +2 -0
- package/tx/ocl/cm-ocl.cjs +531 -0
- package/tx/ocl/cm-ocl.js +1 -105
- package/tx/ocl/cs-ocl.cjs +1779 -0
- package/tx/ocl/cs-ocl.js +1 -38
- package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
- package/tx/ocl/fingerprint/fingerprint.js +2 -0
- package/tx/ocl/http/client.cjs +31 -0
- package/tx/ocl/http/client.js +2 -0
- package/tx/ocl/http/pagination.cjs +98 -0
- package/tx/ocl/http/pagination.js +2 -0
- package/tx/ocl/jobs/background-queue.cjs +200 -0
- package/tx/ocl/jobs/background-queue.js +2 -0
- package/tx/ocl/mappers/concept-mapper.cjs +66 -0
- package/tx/ocl/mappers/concept-mapper.js +2 -0
- package/tx/ocl/model/concept-filter-context.cjs +51 -0
- package/tx/ocl/model/concept-filter-context.js +2 -0
- package/tx/ocl/shared/constants.cjs +15 -0
- package/tx/ocl/shared/constants.js +2 -0
- package/tx/ocl/shared/patches.cjs +224 -0
- package/tx/ocl/shared/patches.js +2 -0
- package/tx/ocl/vs-ocl.cjs +1848 -0
- package/tx/ocl/vs-ocl.js +1 -104
- package/tx/operation-context.js +8 -1
- package/tx/params.js +24 -3
- package/tx/provider.js +47 -0
- package/tx/tx-html.js +1 -1
- package/tx/tx.js +8 -0
- package/tx/vs/vs-vsac.js +4 -3
- package/tx/workers/batch-validate.js +3 -2
- package/tx/workers/batch.js +3 -2
- package/tx/workers/expand.js +64 -9
- package/tx/workers/lookup.js +5 -4
- package/tx/workers/read.js +2 -1
- package/tx/workers/related.js +3 -2
- package/tx/workers/search.js +4 -9
- package/tx/workers/subsumes.js +3 -2
- package/tx/workers/translate.js +4 -3
- package/tx/workers/validate.js +132 -40
- package/tx/workers/worker.js +1 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.0] - 2026-03-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Add support for serving for OCL TX content (h/t Italo Macêdo from the OCL team)
|
|
12
|
+
- Add default configurations (wip)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Make web-crawlers more robust after tx.fhir.org crash
|
|
16
|
+
- Don't accept NPM packages that have .js code or install scripts
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Fix many bugs in expansion and validation for value sets that include two different versions of the same code system
|
|
20
|
+
- Fix CodeSystem search on system parameter to reduce user confusion
|
|
21
|
+
- Fix CodeSystem search such that default search is without any specified source
|
|
22
|
+
- Fix headers sent multiple times error
|
|
23
|
+
|
|
24
|
+
### Tx Conformance Statement
|
|
25
|
+
|
|
26
|
+
FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.8.2)
|
|
27
|
+
|
|
8
28
|
## [v0.6.0] - 2026-03-06
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -229,6 +229,8 @@ GitHub Actions will automatically:
|
|
|
229
229
|
- Change description
|
|
230
230
|
### Fixed
|
|
231
231
|
- Bug fix description
|
|
232
|
+
### Tx Conformance Statement
|
|
233
|
+
{copy content from text-cases-summary.txt}
|
|
232
234
|
```
|
|
233
235
|
2. Update `package.json` to have the same release version
|
|
234
236
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hostName" : "NPM Publisher",
|
|
3
|
+
"server": {
|
|
4
|
+
"port": 3001,
|
|
5
|
+
"cors": {
|
|
6
|
+
"origin": true,
|
|
7
|
+
"credentials": true
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"modules": {
|
|
11
|
+
"npmprojector": {
|
|
12
|
+
"enabled": false,
|
|
13
|
+
"fhirVersion": "r4",
|
|
14
|
+
"basePath": "/us-core",
|
|
15
|
+
"npm": "hl7.fhir.us.core#7.0.1",
|
|
16
|
+
"resourceFolders": ["data"],
|
|
17
|
+
"searchParametersFolder": "data",
|
|
18
|
+
"debounceMs": 500
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -380,7 +380,6 @@ class PackageManager {
|
|
|
380
380
|
return cachedPath;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
console.log("Fetch Package "+packageId+"#"+version);
|
|
384
383
|
// Not in cache, fetch from servers
|
|
385
384
|
const packageData = await this.fetchFromServers(packageId, resolvedVersion);
|
|
386
385
|
|
|
@@ -593,7 +592,6 @@ class PackageManager {
|
|
|
593
592
|
* @returns {Promise<string>} Path to extracted package folder
|
|
594
593
|
*/
|
|
595
594
|
async fetchUrl(url) {
|
|
596
|
-
console.log("Fetch Package from URL: " + url);
|
|
597
595
|
try {
|
|
598
596
|
const client = new CIBuildClient();
|
|
599
597
|
const packageData = await client.fetchFromUrlSpecific(url);
|
|
@@ -1051,6 +1051,91 @@ class VersionUtilities {
|
|
|
1051
1051
|
return url;
|
|
1052
1052
|
}
|
|
1053
1053
|
}
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
static isAnInteger(version) {
|
|
1057
|
+
return /^\d+$/.test(version);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
static appearsToBeDate(version) {
|
|
1061
|
+
if (!version || typeof version !== 'string') return false;
|
|
1062
|
+
// Strip optional time portion (T...) before checking
|
|
1063
|
+
const datePart = version.split('T')[0];
|
|
1064
|
+
return /^\d{4}-?\d{2}(-?\d{2})?$/.test(datePart);
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
static guessVersionAlgorithmFromVersion(version) {
|
|
1069
|
+
if (VersionUtilities.isSemVerWithWildcards(version)) {
|
|
1070
|
+
return 'semver';
|
|
1071
|
+
}
|
|
1072
|
+
if (this.appearsToBeDate(version)) {
|
|
1073
|
+
return 'date';
|
|
1074
|
+
}
|
|
1075
|
+
if (this.isAnInteger(version)) {
|
|
1076
|
+
return 'integer';
|
|
1077
|
+
}
|
|
1078
|
+
return 'alpha';
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
static dateIsMoreRecent(date, date2) {
|
|
1082
|
+
return VersionUtilities.normaliseDateString(date) > VersionUtilities.normaliseDateString(date2);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
static normaliseDateString(date) {
|
|
1086
|
+
// Strip time portion, then remove dashes so all formats compare uniformly as YYYYMMDD or YYYYMM
|
|
1087
|
+
return date.split('T')[0].replace(/-/g, '');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* guesses the correct format, then compares accordingly
|
|
1093
|
+
*/
|
|
1094
|
+
static compareVersionsGeneral(version1, version2) {
|
|
1095
|
+
if (version1 && version2) {
|
|
1096
|
+
if (version1 == version2) {
|
|
1097
|
+
return 0;
|
|
1098
|
+
}
|
|
1099
|
+
const fmt1 = VersionUtilities.guessVersionAlgorithmFromVersion(version1);
|
|
1100
|
+
const fmt2 = VersionUtilities.guessVersionAlgorithmFromVersion(version2);
|
|
1101
|
+
if (fmt1 != fmt2) {
|
|
1102
|
+
return version1.localeCompare(version2);
|
|
1103
|
+
}
|
|
1104
|
+
switch (fmt1) {
|
|
1105
|
+
case 'semver': {
|
|
1106
|
+
let b1 = VersionUtilities.isThisOrLater(version1, version2, VersionPrecision.PATCH);
|
|
1107
|
+
let b2 = VersionUtilities.isThisOrLater(version2, version1, VersionPrecision.PATCH);
|
|
1108
|
+
if (b1 && b2) {
|
|
1109
|
+
return 0;
|
|
1110
|
+
} else if (b2) {
|
|
1111
|
+
return 1;
|
|
1112
|
+
} else {
|
|
1113
|
+
return -1;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
case 'date':
|
|
1117
|
+
if (VersionUtilities.dateIsMoreRecent(version1, version2)) {
|
|
1118
|
+
return 1;
|
|
1119
|
+
} else if (VersionUtilities.dateIsMoreRecent(version2, version1)) {
|
|
1120
|
+
return -1;
|
|
1121
|
+
} else {
|
|
1122
|
+
return 0;
|
|
1123
|
+
}
|
|
1124
|
+
case 'integer':
|
|
1125
|
+
return parseInt(version1, 10) - parseInt(version2, 10);
|
|
1126
|
+
case 'alpha':
|
|
1127
|
+
return version1.localeCompare(version2);
|
|
1128
|
+
default:
|
|
1129
|
+
return version1.localeCompare(version2);
|
|
1130
|
+
}
|
|
1131
|
+
} else if (version1) {
|
|
1132
|
+
return 1;
|
|
1133
|
+
} else if (version2) {
|
|
1134
|
+
return -1;
|
|
1135
|
+
} else {
|
|
1136
|
+
return 0;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1054
1139
|
}
|
|
1055
1140
|
|
|
1056
1141
|
module.exports = { VersionUtilities, VersionPrecision, SemverParser };
|
package/package.json
CHANGED
|
@@ -9,11 +9,12 @@ const {XMLParser} = require('fast-xml-parser');
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const {debugLog} = require("../tx/operation-context");
|
|
12
13
|
|
|
13
14
|
class PackageCrawler {
|
|
14
15
|
log;
|
|
15
16
|
packages = new Set();
|
|
16
|
-
|
|
17
|
+
|
|
17
18
|
constructor(config, db, stats) {
|
|
18
19
|
this.config = config;
|
|
19
20
|
this.db = db;
|
|
@@ -21,6 +22,7 @@ class PackageCrawler {
|
|
|
21
22
|
this.totalBytes = 0;
|
|
22
23
|
this.crawlerLog = {};
|
|
23
24
|
this.errors = '';
|
|
25
|
+
this.abortController = null;
|
|
24
26
|
this.db.run('PRAGMA journal_mode = WAL');
|
|
25
27
|
this.db.run('PRAGMA busy_timeout = 5000');
|
|
26
28
|
}
|
|
@@ -28,7 +30,8 @@ class PackageCrawler {
|
|
|
28
30
|
async crawl(log) {
|
|
29
31
|
this.log = log;
|
|
30
32
|
this.packages.clear();
|
|
31
|
-
|
|
33
|
+
this.abortController = new AbortController();
|
|
34
|
+
|
|
32
35
|
const startTime = Date.now();
|
|
33
36
|
this.crawlerLog = {
|
|
34
37
|
startTime: new Date().toISOString(),
|
|
@@ -54,6 +57,7 @@ class PackageCrawler {
|
|
|
54
57
|
|
|
55
58
|
// Process each feed
|
|
56
59
|
for (const feedConfig of masterResponse.feeds) {
|
|
60
|
+
if (this.abortController?.signal.aborted) break;
|
|
57
61
|
if (!feedConfig.url) {
|
|
58
62
|
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
59
63
|
continue;
|
|
@@ -71,6 +75,7 @@ class PackageCrawler {
|
|
|
71
75
|
}
|
|
72
76
|
// process simplifier last
|
|
73
77
|
for (const feedConfig of masterResponse.feeds) {
|
|
78
|
+
if (this.abortController?.signal.aborted) break;
|
|
74
79
|
if (!feedConfig.url) {
|
|
75
80
|
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
76
81
|
continue;
|
|
@@ -123,6 +128,7 @@ class PackageCrawler {
|
|
|
123
128
|
} else {
|
|
124
129
|
const response = await axios.get(url, {
|
|
125
130
|
timeout: 30000,
|
|
131
|
+
signal: this.abortController?.signal,
|
|
126
132
|
headers: {
|
|
127
133
|
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
128
134
|
}
|
|
@@ -130,7 +136,7 @@ class PackageCrawler {
|
|
|
130
136
|
return response.data;
|
|
131
137
|
}
|
|
132
138
|
} catch (error) {
|
|
133
|
-
|
|
139
|
+
debugLog(error);
|
|
134
140
|
if (error.response && error.response.status === 429) {
|
|
135
141
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
136
142
|
}
|
|
@@ -151,6 +157,7 @@ class PackageCrawler {
|
|
|
151
157
|
} else {
|
|
152
158
|
const response = await axios.get(url, {
|
|
153
159
|
timeout: 30000,
|
|
160
|
+
signal: this.abortController?.signal,
|
|
154
161
|
headers: {
|
|
155
162
|
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
156
163
|
}
|
|
@@ -165,6 +172,7 @@ class PackageCrawler {
|
|
|
165
172
|
return parser.parse(response.data);
|
|
166
173
|
}
|
|
167
174
|
} catch (error) {
|
|
175
|
+
debugLog(error);
|
|
168
176
|
if (error.response && error.response.status === 429) {
|
|
169
177
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
170
178
|
}
|
|
@@ -182,6 +190,7 @@ class PackageCrawler {
|
|
|
182
190
|
const response = await axios.get(url, {
|
|
183
191
|
timeout: 60000,
|
|
184
192
|
responseType: 'arraybuffer',
|
|
193
|
+
signal: this.abortController?.signal,
|
|
185
194
|
headers: {
|
|
186
195
|
'User-Agent': 'FHIR Package Crawler/1.0'
|
|
187
196
|
}
|
|
@@ -191,6 +200,7 @@ class PackageCrawler {
|
|
|
191
200
|
return Buffer.from(response.data);
|
|
192
201
|
}
|
|
193
202
|
} catch (error) {
|
|
203
|
+
debugLog(error);
|
|
194
204
|
if (error.response && error.response.status === 429) {
|
|
195
205
|
throw new Error(`RATE_LIMITED: Server returned 429 Too Many Requests for ${url}`);
|
|
196
206
|
}
|
|
@@ -222,6 +232,7 @@ class PackageCrawler {
|
|
|
222
232
|
this.log.info(`Found ${items.length} items in feed`);
|
|
223
233
|
|
|
224
234
|
for (let i = 0; i < items.length; i++) {
|
|
235
|
+
if (this.abortController?.signal.aborted) break;
|
|
225
236
|
try {
|
|
226
237
|
await this.updateItem(url, items[i], i, packageRestrictions, feedLog);
|
|
227
238
|
} catch (itemError) {
|
|
@@ -244,7 +255,7 @@ class PackageCrawler {
|
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
} catch (error) {
|
|
247
|
-
|
|
258
|
+
debugLog(error);
|
|
248
259
|
// Check if this is a 429 error on feed fetch
|
|
249
260
|
if (error.message.includes('RATE_LIMITED')) {
|
|
250
261
|
this.log.info(`Rate limited while fetching feed ${url}, skipping this feed`);
|
|
@@ -302,7 +313,7 @@ class PackageCrawler {
|
|
|
302
313
|
}
|
|
303
314
|
|
|
304
315
|
// Check package restrictions
|
|
305
|
-
if (!this.isPackageAllowed(id, source, packageRestrictions)) {
|
|
316
|
+
if (!this.isPackageAllowed(id, source, packageRestrictions).allowed) {
|
|
306
317
|
if (!source.includes('simplifier.net')) {
|
|
307
318
|
const error = `The package ${id} is not allowed to come from ${source}`;
|
|
308
319
|
this.log.info(error);
|
|
@@ -329,11 +340,12 @@ class PackageCrawler {
|
|
|
329
340
|
|
|
330
341
|
// Parse publication date
|
|
331
342
|
let pubDate;
|
|
343
|
+
let pd;
|
|
332
344
|
try {
|
|
333
345
|
let pd = item.pubDate;
|
|
334
346
|
pubDate = this.parsePubDate(pd);
|
|
335
347
|
} catch (error) {
|
|
336
|
-
itemLog.error = `Invalid date format '{pd}': ${error.message}`;
|
|
348
|
+
itemLog.error = `Invalid date format '${pd}': ${error.message}`;
|
|
337
349
|
itemLog.status = 'error';
|
|
338
350
|
return;
|
|
339
351
|
}
|
|
@@ -355,7 +367,7 @@ class PackageCrawler {
|
|
|
355
367
|
itemLog.status = 'Fetched';
|
|
356
368
|
|
|
357
369
|
} catch (error) {
|
|
358
|
-
this.log.error(`Exception processing item ${itemLog.guid || index}
|
|
370
|
+
this.log.error(`Exception processing item ${itemLog.guid || index} from ${source}: `+ error.message);
|
|
359
371
|
itemLog.status = 'Exception';
|
|
360
372
|
itemLog.error = error.message;
|
|
361
373
|
if (error.message.includes('RATE_LIMITED')) {
|
|
@@ -383,7 +395,7 @@ class PackageCrawler {
|
|
|
383
395
|
|
|
384
396
|
if (this.matchesPattern(fixedPackageId, fixedMask)) {
|
|
385
397
|
// This package matches a restriction - check if source is allowed
|
|
386
|
-
const allowedFeeds = restriction.feeds.map(feed => feed);
|
|
398
|
+
const allowedFeeds = restriction.feeds.map(feed => fixUrl(feed));
|
|
387
399
|
const feedList = allowedFeeds.join(', ');
|
|
388
400
|
|
|
389
401
|
for (const allowedFeed of restriction.feeds) {
|
|
@@ -485,6 +497,14 @@ class PackageCrawler {
|
|
|
485
497
|
throw new Error(`NPM Canonical "${canonical}" is not valid from ${source}`);
|
|
486
498
|
}
|
|
487
499
|
|
|
500
|
+
const isTemplate = npmPackage.kind === 2; // fhir.template
|
|
501
|
+
if (npmPackage.hasInstallScripts) {
|
|
502
|
+
throw new Error(`Package ${idver} rejected: contains install scripts (preinstall/install/postinstall)`);
|
|
503
|
+
}
|
|
504
|
+
if (npmPackage.hasJavaScript && !isTemplate) {
|
|
505
|
+
throw new Error(`Package ${idver} rejected: contains JavaScript files but is not a template package`);
|
|
506
|
+
}
|
|
507
|
+
|
|
488
508
|
// Extract URLs from package
|
|
489
509
|
const urls = this.processPackageUrls(npmPackage);
|
|
490
510
|
|
|
@@ -492,7 +512,7 @@ class PackageCrawler {
|
|
|
492
512
|
await this.commit(packageBuffer, npmPackage, date, guid, id, version, canonical, urls);
|
|
493
513
|
|
|
494
514
|
} catch (error) {
|
|
495
|
-
|
|
515
|
+
debugLog(error);
|
|
496
516
|
this.log.error(`Error storing package ${guid}:`+ error.message);
|
|
497
517
|
throw error;
|
|
498
518
|
}
|
|
@@ -555,6 +575,14 @@ class PackageCrawler {
|
|
|
555
575
|
}
|
|
556
576
|
|
|
557
577
|
const packageJson = JSON.parse(files['package.json']);
|
|
578
|
+
const hasInstallScripts = !!(
|
|
579
|
+
packageJson.scripts && (
|
|
580
|
+
packageJson.scripts.preinstall ||
|
|
581
|
+
packageJson.scripts.install ||
|
|
582
|
+
packageJson.scripts.postinstall
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
const hasJavaScript = Object.keys(files).some(f => f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.cjs'));
|
|
558
586
|
|
|
559
587
|
// Extract basic NPM fields
|
|
560
588
|
const id = packageJson.name || '';
|
|
@@ -664,6 +692,8 @@ class PackageCrawler {
|
|
|
664
692
|
url: homepage,
|
|
665
693
|
dependencies,
|
|
666
694
|
kind,
|
|
695
|
+
hasInstallScripts,
|
|
696
|
+
hasJavaScript,
|
|
667
697
|
notForPublication,
|
|
668
698
|
files
|
|
669
699
|
};
|
|
@@ -903,6 +933,11 @@ class PackageCrawler {
|
|
|
903
933
|
return id;
|
|
904
934
|
}
|
|
905
935
|
}
|
|
936
|
+
shutdown() {
|
|
937
|
+
if (this.abortController) {
|
|
938
|
+
this.abortController.abort();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
906
941
|
}
|
|
907
942
|
|
|
908
943
|
module.exports = PackageCrawler;
|
package/packages/packages.js
CHANGED
package/registry/crawler.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
ServerVersionInformation,
|
|
10
10
|
} = require('./model');
|
|
11
11
|
const {Extensions} = require("../tx/library/extensions");
|
|
12
|
+
const {debugLog} = require("../tx/operation-context");
|
|
12
13
|
|
|
13
14
|
const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
|
|
14
15
|
|
|
@@ -31,6 +32,7 @@ class RegistryCrawler {
|
|
|
31
32
|
this.errors = [];
|
|
32
33
|
this.totalBytes = 0;
|
|
33
34
|
this.log = console;
|
|
35
|
+
this.abortController = null;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
useLog(logv) {
|
|
@@ -48,6 +50,7 @@ class RegistryCrawler {
|
|
|
48
50
|
this.addLogEntry('warn', 'Crawl already in progress, skipping...');
|
|
49
51
|
return this.currentData;
|
|
50
52
|
}
|
|
53
|
+
this.abortController = new AbortController();
|
|
51
54
|
|
|
52
55
|
this.isCrawling = true;
|
|
53
56
|
const startTime = new Date();
|
|
@@ -75,6 +78,7 @@ class RegistryCrawler {
|
|
|
75
78
|
// Process each registry
|
|
76
79
|
const registries = masterJson.registries || [];
|
|
77
80
|
for (const registryConfig of registries) {
|
|
81
|
+
if (this.abortController?.signal.aborted) break;
|
|
78
82
|
const registry = await this.processRegistry(registryConfig);
|
|
79
83
|
if (registry) {
|
|
80
84
|
newData.registries.push(registry);
|
|
@@ -86,7 +90,7 @@ class RegistryCrawler {
|
|
|
86
90
|
// Update the current data
|
|
87
91
|
this.currentData = newData;
|
|
88
92
|
} catch (error) {
|
|
89
|
-
|
|
93
|
+
debugLog(error);
|
|
90
94
|
this.addLogEntry('error', 'Exception Scanning:', error);
|
|
91
95
|
this.currentData.outcome = `Error: ${error.message}`;
|
|
92
96
|
this.errors.push({
|
|
@@ -118,7 +122,7 @@ class RegistryCrawler {
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
if (!registry.address) {
|
|
121
|
-
this.addLogEntry('error', `No url provided for ${registry.name
|
|
125
|
+
this.addLogEntry('error', `No url provided for ${registry.name}`, '');
|
|
122
126
|
return registry;
|
|
123
127
|
}
|
|
124
128
|
|
|
@@ -134,6 +138,7 @@ class RegistryCrawler {
|
|
|
134
138
|
// Process each server in the registry
|
|
135
139
|
const servers = registryJson.servers || [];
|
|
136
140
|
for (const serverConfig of servers) {
|
|
141
|
+
if (this.abortController?.signal.aborted) break;
|
|
137
142
|
const server = await this.processServer(serverConfig, registry.address);
|
|
138
143
|
if (server) {
|
|
139
144
|
registry.servers.push(server);
|
|
@@ -141,7 +146,7 @@ class RegistryCrawler {
|
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
} catch (error) {
|
|
144
|
-
|
|
149
|
+
debugLog(error);
|
|
145
150
|
registry.error = error.message;
|
|
146
151
|
this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
|
|
147
152
|
}
|
|
@@ -177,6 +182,7 @@ class RegistryCrawler {
|
|
|
177
182
|
// Process each FHIR version
|
|
178
183
|
const fhirVersions = serverConfig.fhirVersions || [];
|
|
179
184
|
for (const versionConfig of fhirVersions) {
|
|
185
|
+
if (this.abortController?.signal.aborted) break;
|
|
180
186
|
const version = await this.processServerVersion(versionConfig, server, serverConfig.exclusions);
|
|
181
187
|
if (version) {
|
|
182
188
|
server.versions.push(version);
|
|
@@ -231,7 +237,7 @@ class RegistryCrawler {
|
|
|
231
237
|
this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
|
|
232
238
|
|
|
233
239
|
} catch (error) {
|
|
234
|
-
|
|
240
|
+
debugLog(error);
|
|
235
241
|
const elapsed = Date.now() - startTime;
|
|
236
242
|
this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
|
|
237
243
|
version.error = error.message;
|
|
@@ -276,10 +282,11 @@ class RegistryCrawler {
|
|
|
276
282
|
});
|
|
277
283
|
}
|
|
278
284
|
} catch (error) {
|
|
279
|
-
|
|
280
|
-
this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
|
|
285
|
+
debugLog(error);
|
|
286
|
+
this.addLogEntry('error', `Could not fetch terminology capabilities from ${version.address}: ${error.message}`);
|
|
281
287
|
}
|
|
282
|
-
|
|
288
|
+
|
|
289
|
+
if (this.abortController?.signal.aborted) return;
|
|
283
290
|
// Search for value sets
|
|
284
291
|
await this.fetchValueSets(version, server, exclusions);
|
|
285
292
|
}
|
|
@@ -324,8 +331,8 @@ class RegistryCrawler {
|
|
|
324
331
|
});
|
|
325
332
|
}
|
|
326
333
|
} catch (error) {
|
|
327
|
-
|
|
328
|
-
this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
|
|
334
|
+
debugLog(error);
|
|
335
|
+
this.addLogEntry('error', `Could not fetch terminology capabilities from ${version.address}: ${error.message}`);
|
|
329
336
|
}
|
|
330
337
|
|
|
331
338
|
// Search for value sets
|
|
@@ -342,14 +349,20 @@ class RegistryCrawler {
|
|
|
342
349
|
*/
|
|
343
350
|
async fetchValueSets(version, server, exclusions) {
|
|
344
351
|
// Initial search URL
|
|
352
|
+
let count = 0;
|
|
345
353
|
let searchUrl = `${version.address}/ValueSet?_elements=url,version`+(version.address.includes("fhir.org") ? "&_count=200" : "");
|
|
346
354
|
try {
|
|
347
355
|
// Set of URLs to avoid duplicates
|
|
348
356
|
const valueSetUrls = new Set();
|
|
349
357
|
|
|
350
|
-
|
|
351
358
|
// Continue fetching while we have a URL
|
|
352
359
|
while (searchUrl) {
|
|
360
|
+
count++;
|
|
361
|
+
if (count == 1000) {
|
|
362
|
+
throw new Error(`Fetch ValueSet loop exceeded 1000 iterations - a logic problem on the server? (${version.address})`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this.abortController?.signal.aborted) break;
|
|
353
366
|
this.log.debug(`Fetching value sets from ${searchUrl}`);
|
|
354
367
|
const bundle = await this.fetchJson(searchUrl, server.code);
|
|
355
368
|
|
|
@@ -382,7 +395,7 @@ class RegistryCrawler {
|
|
|
382
395
|
version.valueSets = Array.from(valueSetUrls).sort();
|
|
383
396
|
|
|
384
397
|
} catch (error) {
|
|
385
|
-
|
|
398
|
+
debugLog(error);
|
|
386
399
|
this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
|
|
387
400
|
}
|
|
388
401
|
}
|
|
@@ -441,6 +454,7 @@ class RegistryCrawler {
|
|
|
441
454
|
const response = await axios.get(fetchUrl, {
|
|
442
455
|
timeout: this.config.timeout,
|
|
443
456
|
headers: headers,
|
|
457
|
+
signal: this.abortController?.signal,
|
|
444
458
|
validateStatus: (status) => status < 500 // Don't throw on 4xx
|
|
445
459
|
});
|
|
446
460
|
|
|
@@ -459,7 +473,7 @@ class RegistryCrawler {
|
|
|
459
473
|
return response.data;
|
|
460
474
|
|
|
461
475
|
} catch (error) {
|
|
462
|
-
|
|
476
|
+
debugLog(error);
|
|
463
477
|
if (error.response) {
|
|
464
478
|
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
465
479
|
} else if (error.request) {
|
|
@@ -603,14 +617,14 @@ class RegistryCrawler {
|
|
|
603
617
|
* @param {string} level - Filter by log level
|
|
604
618
|
* @returns {Array} Array of log entries
|
|
605
619
|
*/
|
|
606
|
-
getLogs(limit = 100)
|
|
620
|
+
getLogs(limit = 100, level = null)
|
|
607
621
|
{
|
|
608
622
|
if (!this.logs) {
|
|
609
623
|
return [];
|
|
610
624
|
}
|
|
611
625
|
|
|
612
626
|
// Filter by level if specified
|
|
613
|
-
let filteredLogs = this.logs;
|
|
627
|
+
let filteredLogs = level ? this.logs.filter(entry => entry.level === level) : this.logs;
|
|
614
628
|
|
|
615
629
|
// Get the latest entries up to the limit
|
|
616
630
|
return filteredLogs.slice(-limit);
|
|
@@ -648,6 +662,13 @@ class RegistryCrawler {
|
|
|
648
662
|
}
|
|
649
663
|
return false;
|
|
650
664
|
}
|
|
665
|
+
|
|
666
|
+
shutdown() {
|
|
667
|
+
if (this.abortController) {
|
|
668
|
+
this.abortController.abort();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
651
672
|
}
|
|
652
673
|
|
|
653
674
|
module.exports = RegistryCrawler;
|
package/registry/registry.js
CHANGED
package/server.js
CHANGED
|
@@ -436,9 +436,11 @@ app.get('/', async (req, res) => {
|
|
|
436
436
|
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
437
437
|
res.setHeader('Content-Type', 'text/html');
|
|
438
438
|
res.send(html);
|
|
439
|
+
return;
|
|
439
440
|
} catch (error) {
|
|
440
441
|
serverLog.error('Error rendering root page:', error);
|
|
441
442
|
htmlServer.sendErrorResponse(res, 'root', error);
|
|
443
|
+
return;
|
|
442
444
|
}
|
|
443
445
|
}
|
|
444
446
|
return serveFhirsmithHome(req, res);
|
|
@@ -623,9 +625,11 @@ async function serveFhirsmithHome(req, res) {
|
|
|
623
625
|
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
624
626
|
res.setHeader('Content-Type', 'text/html');
|
|
625
627
|
res.send(html);
|
|
628
|
+
return;
|
|
626
629
|
} catch (error) {
|
|
627
630
|
serverLog.error('Error rendering root page:', error);
|
|
628
631
|
htmlServer.sendErrorResponse(res, 'root', error);
|
|
632
|
+
return;
|
|
629
633
|
}
|
|
630
634
|
} else {
|
|
631
635
|
// Return JSON response for API clients
|
package/tx/README.md
CHANGED
|
@@ -33,22 +33,22 @@ Add the `tx` section to your `config.json`:
|
|
|
33
33
|
"endpoints": [
|
|
34
34
|
{
|
|
35
35
|
"path": "/tx/r5",
|
|
36
|
-
"fhirVersion": 5,
|
|
36
|
+
"fhirVersion": "5.0",
|
|
37
37
|
"context": null
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "/tx/r4",
|
|
41
|
-
"fhirVersion": 4,
|
|
41
|
+
"fhirVersion": "4.0",
|
|
42
42
|
"context": null
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "/tx/r3",
|
|
46
|
-
"fhirVersion": 3,
|
|
46
|
+
"fhirVersion": "3.0",
|
|
47
47
|
"context": null
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"path": "/tx/r4/demo",
|
|
51
|
-
"fhirVersion": 4,
|
|
51
|
+
"fhirVersion": "4.0",
|
|
52
52
|
"context": "demo"
|
|
53
53
|
}
|
|
54
54
|
]
|
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -203,9 +203,12 @@ class LoincServices extends BaseCSServices {
|
|
|
203
203
|
// Use language-aware display logic
|
|
204
204
|
if (this.opContext.langs && !this.opContext.langs.isEnglishOrNothing()) {
|
|
205
205
|
const displays = await this.#getDisplaysForContext(ctxt, this.opContext.langs);
|
|
206
|
+
const requestedLanguages = Array.isArray(this.opContext.langs.languages)
|
|
207
|
+
? this.opContext.langs.languages
|
|
208
|
+
: (Array.isArray(this.opContext.langs.langs) ? this.opContext.langs.langs : []);
|
|
206
209
|
|
|
207
210
|
// Try to find exact language match
|
|
208
|
-
for (const lang of
|
|
211
|
+
for (const lang of requestedLanguages) {
|
|
209
212
|
for (const display of displays) {
|
|
210
213
|
if (lang.matches(display.language, true)) {
|
|
211
214
|
return display.value;
|
|
@@ -214,7 +217,7 @@ class LoincServices extends BaseCSServices {
|
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
// Try partial language match
|
|
217
|
-
for (const lang of
|
|
220
|
+
for (const lang of requestedLanguages) {
|
|
218
221
|
for (const display of displays) {
|
|
219
222
|
if (lang.matches(display.language, false)) {
|
|
220
223
|
return display.value;
|