fhirsmith 0.5.6 → 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 +38 -0
- package/README.md +2 -0
- package/configurations/projector.json +21 -0
- package/configurations/readme.md +5 -0
- package/library/html-server.js +2 -1
- package/library/package-manager.js +37 -34
- package/library/utilities.js +10 -1
- package/library/version-utilities.js +85 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +144 -52
- package/packages/packages.js +15 -7
- package/publisher/publisher.js +15 -3
- package/registry/api.js +173 -191
- package/registry/crawler.js +100 -65
- package/registry/model.js +14 -8
- package/registry/registry.js +5 -0
- package/root-template.html +1 -0
- package/server.js +113 -45
- package/tx/README.md +4 -4
- package/tx/cs/cs-api.js +18 -1
- package/tx/cs/cs-base.js +1 -0
- 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/cs/cs-rxnorm.js +9 -2
- package/tx/cs/cs-snomed.js +17 -2
- package/tx/html/codesystem-operations.liquid +17 -24
- package/tx/html/valueset-operations.liquid +46 -52
- package/tx/library/canonical-resource.js +6 -1
- package/tx/library/codesystem.js +6 -1
- package/tx/library/renderer.js +81 -7
- package/tx/library.js +145 -13
- 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 +51 -2
- package/tx/sct/expressions.js +20 -9
- package/tx/tx-html.js +144 -51
- package/tx/tx.js +10 -2
- 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 +125 -18
- 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 +6 -8
- 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/tx/xversion/xv-terminologyCapabilities.js +1 -1
package/registry/crawler.js
CHANGED
|
@@ -8,6 +8,8 @@ const {
|
|
|
8
8
|
ServerInformation,
|
|
9
9
|
ServerVersionInformation,
|
|
10
10
|
} = require('./model');
|
|
11
|
+
const {Extensions} = require("../tx/library/extensions");
|
|
12
|
+
const {debugLog} = require("../tx/operation-context");
|
|
11
13
|
|
|
12
14
|
const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
|
|
13
15
|
|
|
@@ -30,38 +32,13 @@ class RegistryCrawler {
|
|
|
30
32
|
this.errors = [];
|
|
31
33
|
this.totalBytes = 0;
|
|
32
34
|
this.log = console;
|
|
35
|
+
this.abortController = null;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
useLog(logv) {
|
|
36
39
|
this.log = logv;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
// /**
|
|
40
|
-
// * Start the crawler with periodic updates
|
|
41
|
-
// */
|
|
42
|
-
// start() {
|
|
43
|
-
// if (this.crawlTimer) {
|
|
44
|
-
// return; // Already running
|
|
45
|
-
// }
|
|
46
|
-
//
|
|
47
|
-
// // Initial crawl
|
|
48
|
-
// this.crawl();
|
|
49
|
-
//
|
|
50
|
-
// // Set up periodic crawling
|
|
51
|
-
// this.crawlTimer = setInterval(() => {
|
|
52
|
-
// this.crawl();
|
|
53
|
-
// }, this.config.crawlInterval);
|
|
54
|
-
// }
|
|
55
|
-
//
|
|
56
|
-
// /**
|
|
57
|
-
// * Stop the crawler
|
|
58
|
-
// */
|
|
59
|
-
// stop() {
|
|
60
|
-
// if (this.crawlTimer) {
|
|
61
|
-
// clearInterval(this.crawlTimer);
|
|
62
|
-
// this.crawlTimer = null;
|
|
63
|
-
// }
|
|
64
|
-
// }
|
|
65
42
|
|
|
66
43
|
/**
|
|
67
44
|
* Main entry point - crawl the registry starting from the master URL
|
|
@@ -73,6 +50,7 @@ class RegistryCrawler {
|
|
|
73
50
|
this.addLogEntry('warn', 'Crawl already in progress, skipping...');
|
|
74
51
|
return this.currentData;
|
|
75
52
|
}
|
|
53
|
+
this.abortController = new AbortController();
|
|
76
54
|
|
|
77
55
|
this.isCrawling = true;
|
|
78
56
|
const startTime = new Date();
|
|
@@ -100,6 +78,7 @@ class RegistryCrawler {
|
|
|
100
78
|
// Process each registry
|
|
101
79
|
const registries = masterJson.registries || [];
|
|
102
80
|
for (const registryConfig of registries) {
|
|
81
|
+
if (this.abortController?.signal.aborted) break;
|
|
103
82
|
const registry = await this.processRegistry(registryConfig);
|
|
104
83
|
if (registry) {
|
|
105
84
|
newData.registries.push(registry);
|
|
@@ -111,6 +90,7 @@ class RegistryCrawler {
|
|
|
111
90
|
// Update the current data
|
|
112
91
|
this.currentData = newData;
|
|
113
92
|
} catch (error) {
|
|
93
|
+
debugLog(error);
|
|
114
94
|
this.addLogEntry('error', 'Exception Scanning:', error);
|
|
115
95
|
this.currentData.outcome = `Error: ${error.message}`;
|
|
116
96
|
this.errors.push({
|
|
@@ -142,7 +122,7 @@ class RegistryCrawler {
|
|
|
142
122
|
}
|
|
143
123
|
|
|
144
124
|
if (!registry.address) {
|
|
145
|
-
this.addLogEntry('error', `No url provided for ${registry.name
|
|
125
|
+
this.addLogEntry('error', `No url provided for ${registry.name}`, '');
|
|
146
126
|
return registry;
|
|
147
127
|
}
|
|
148
128
|
|
|
@@ -158,6 +138,7 @@ class RegistryCrawler {
|
|
|
158
138
|
// Process each server in the registry
|
|
159
139
|
const servers = registryJson.servers || [];
|
|
160
140
|
for (const serverConfig of servers) {
|
|
141
|
+
if (this.abortController?.signal.aborted) break;
|
|
161
142
|
const server = await this.processServer(serverConfig, registry.address);
|
|
162
143
|
if (server) {
|
|
163
144
|
registry.servers.push(server);
|
|
@@ -165,6 +146,7 @@ class RegistryCrawler {
|
|
|
165
146
|
}
|
|
166
147
|
|
|
167
148
|
} catch (error) {
|
|
149
|
+
debugLog(error);
|
|
168
150
|
registry.error = error.message;
|
|
169
151
|
this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
|
|
170
152
|
}
|
|
@@ -181,7 +163,7 @@ class RegistryCrawler {
|
|
|
181
163
|
server.name = serverConfig.name;
|
|
182
164
|
server.address = serverConfig.url || '';
|
|
183
165
|
server.accessInfo = serverConfig.access_info || '';
|
|
184
|
-
|
|
166
|
+
|
|
185
167
|
if (!server.name) {
|
|
186
168
|
this.addLogEntry('error', 'No name provided for server', source);
|
|
187
169
|
return server;
|
|
@@ -200,7 +182,8 @@ class RegistryCrawler {
|
|
|
200
182
|
// Process each FHIR version
|
|
201
183
|
const fhirVersions = serverConfig.fhirVersions || [];
|
|
202
184
|
for (const versionConfig of fhirVersions) {
|
|
203
|
-
|
|
185
|
+
if (this.abortController?.signal.aborted) break;
|
|
186
|
+
const version = await this.processServerVersion(versionConfig, server, serverConfig.exclusions);
|
|
204
187
|
if (version) {
|
|
205
188
|
server.versions.push(version);
|
|
206
189
|
}
|
|
@@ -212,7 +195,7 @@ class RegistryCrawler {
|
|
|
212
195
|
/**
|
|
213
196
|
* Process a single server version
|
|
214
197
|
*/
|
|
215
|
-
async processServerVersion(versionConfig, server) {
|
|
198
|
+
async processServerVersion(versionConfig, server, exclusions) {
|
|
216
199
|
const version = new ServerVersionInformation();
|
|
217
200
|
version.version = versionConfig.version;
|
|
218
201
|
version.address = versionConfig.url;
|
|
@@ -233,20 +216,20 @@ class RegistryCrawler {
|
|
|
233
216
|
|
|
234
217
|
switch (majorVersion) {
|
|
235
218
|
case 3:
|
|
236
|
-
await this.processServerVersionR3(version, server);
|
|
219
|
+
await this.processServerVersionR3(version, server, exclusions);
|
|
237
220
|
break;
|
|
238
221
|
case 4:
|
|
239
|
-
await this.
|
|
222
|
+
await this.processServerVersionR4or5(version, server, '4.0.1', exclusions);
|
|
240
223
|
break;
|
|
241
224
|
case 5:
|
|
242
|
-
await this.
|
|
225
|
+
await this.processServerVersionR4or5(version, server, '5.0.0', exclusions);
|
|
243
226
|
break;
|
|
244
227
|
default:
|
|
245
228
|
throw new Error(`Version ${version.version} not supported`);
|
|
246
229
|
}
|
|
247
230
|
|
|
248
231
|
// Sort and deduplicate
|
|
249
|
-
version.codeSystems
|
|
232
|
+
version.codeSystems.sort((a, b) => this.compareCS(a, b));
|
|
250
233
|
version.valueSets = [...new Set(version.valueSets)].sort();
|
|
251
234
|
version.lastSuccess = new Date();
|
|
252
235
|
version.lastTat = `${Date.now() - startTime}ms`;
|
|
@@ -254,6 +237,7 @@ class RegistryCrawler {
|
|
|
254
237
|
this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
|
|
255
238
|
|
|
256
239
|
} catch (error) {
|
|
240
|
+
debugLog(error);
|
|
257
241
|
const elapsed = Date.now() - startTime;
|
|
258
242
|
this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
|
|
259
243
|
version.error = error.message;
|
|
@@ -266,7 +250,7 @@ class RegistryCrawler {
|
|
|
266
250
|
/**
|
|
267
251
|
* Process an R3 server
|
|
268
252
|
*/
|
|
269
|
-
async processServerVersionR3(version, server) {
|
|
253
|
+
async processServerVersionR3(version, server, exclusions) {
|
|
270
254
|
// Get capability statement
|
|
271
255
|
const capabilityUrl = `${version.address}/metadata`;
|
|
272
256
|
const capability = await this.fetchJson(capabilityUrl, server.name);
|
|
@@ -283,12 +267,12 @@ class RegistryCrawler {
|
|
|
283
267
|
termCap.parameter.forEach(param => {
|
|
284
268
|
if (param.name === 'system') {
|
|
285
269
|
const uri = param.valueUri || param.valueString;
|
|
286
|
-
if (uri) {
|
|
270
|
+
if (uri && !this.isExcluded(uri, exclusions)) {
|
|
287
271
|
version.codeSystems.push(uri);
|
|
288
272
|
// Look for version parts
|
|
289
273
|
if (param.part) {
|
|
290
274
|
param.part.forEach(part => {
|
|
291
|
-
if (part.name === 'version' && part.valueString) {
|
|
275
|
+
if (part.name === 'version' && part.valueString && !this.isExcluded(uri+'|'+part.valueString, exclusions)) {
|
|
292
276
|
version.codeSystems.push(`${uri}|${part.valueString}`);
|
|
293
277
|
}
|
|
294
278
|
});
|
|
@@ -298,24 +282,28 @@ class RegistryCrawler {
|
|
|
298
282
|
});
|
|
299
283
|
}
|
|
300
284
|
} catch (error) {
|
|
301
|
-
|
|
285
|
+
debugLog(error);
|
|
286
|
+
this.addLogEntry('error', `Could not fetch terminology capabilities from ${version.address}: ${error.message}`);
|
|
302
287
|
}
|
|
303
|
-
|
|
288
|
+
|
|
289
|
+
if (this.abortController?.signal.aborted) return;
|
|
304
290
|
// Search for value sets
|
|
305
|
-
await this.fetchValueSets(version, server);
|
|
291
|
+
await this.fetchValueSets(version, server, exclusions);
|
|
306
292
|
}
|
|
307
293
|
|
|
308
294
|
/**
|
|
309
295
|
* Process an R4 server
|
|
310
296
|
*/
|
|
311
|
-
async
|
|
297
|
+
async processServerVersionR4or5(version, server, defVersion, exclusions) {
|
|
312
298
|
// Get capability statement
|
|
313
299
|
const capabilityUrl = `${version.address}/metadata`;
|
|
314
300
|
const capability = await this.fetchJson(capabilityUrl, server.code);
|
|
315
301
|
|
|
316
|
-
version.version = capability.fhirVersion ||
|
|
302
|
+
version.version = capability.fhirVersion || defVersion;
|
|
317
303
|
version.software = capability.software ? capability.software.name : "unknown";
|
|
318
|
-
|
|
304
|
+
|
|
305
|
+
let set = new Set();
|
|
306
|
+
|
|
319
307
|
// Get terminology capabilities
|
|
320
308
|
try {
|
|
321
309
|
const termCapUrl = `${version.address}/metadata?mode=terminology`;
|
|
@@ -323,12 +311,19 @@ class RegistryCrawler {
|
|
|
323
311
|
|
|
324
312
|
if (termCap.codeSystem) {
|
|
325
313
|
termCap.codeSystem.forEach(cs => {
|
|
326
|
-
|
|
327
|
-
|
|
314
|
+
let content = cs.content || Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
|
|
315
|
+
if (cs.uri && !this.isExcluded(cs.uri, exclusions)) {
|
|
316
|
+
if (!set.has(cs.uri)) {
|
|
317
|
+
set.add(cs.uri);
|
|
318
|
+
version.codeSystems.push(this.addContent({uri: cs.uri}, content));
|
|
319
|
+
}
|
|
328
320
|
if (cs.version) {
|
|
329
321
|
cs.version.forEach(v => {
|
|
330
|
-
if (v.code) {
|
|
331
|
-
|
|
322
|
+
if (v.code && !this.isExcluded(cs.uri+"|"+v.code, exclusions)) {
|
|
323
|
+
if (!set.has(cs.uri+"|"+v.code)) {
|
|
324
|
+
version.codeSystems.push(this.addContent({uri: cs.uri, version: v.code}, content));
|
|
325
|
+
set.add(cs.uri+"|"+v.code);
|
|
326
|
+
}
|
|
332
327
|
}
|
|
333
328
|
});
|
|
334
329
|
}
|
|
@@ -336,20 +331,12 @@ class RegistryCrawler {
|
|
|
336
331
|
});
|
|
337
332
|
}
|
|
338
333
|
} catch (error) {
|
|
339
|
-
|
|
334
|
+
debugLog(error);
|
|
335
|
+
this.addLogEntry('error', `Could not fetch terminology capabilities from ${version.address}: ${error.message}`);
|
|
340
336
|
}
|
|
341
337
|
|
|
342
338
|
// Search for value sets
|
|
343
|
-
await this.fetchValueSets(version,
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Process an R5 server
|
|
348
|
-
*/
|
|
349
|
-
async processServerVersionR5(version, server) {
|
|
350
|
-
// R5 is essentially the same as R4 for our purposes
|
|
351
|
-
await this.processServerVersionR4(version, server);
|
|
352
|
-
version.version = version.version || '5.0.0';
|
|
339
|
+
await this.fetchValueSets(version, server, exclusions);
|
|
353
340
|
}
|
|
354
341
|
|
|
355
342
|
/**
|
|
@@ -360,16 +347,22 @@ class RegistryCrawler {
|
|
|
360
347
|
* @param {Object} version - The server version information
|
|
361
348
|
* @param {Object} server - The server information
|
|
362
349
|
*/
|
|
363
|
-
async fetchValueSets(version, server) {
|
|
350
|
+
async fetchValueSets(version, server, exclusions) {
|
|
364
351
|
// Initial search URL
|
|
365
|
-
let
|
|
352
|
+
let count = 0;
|
|
353
|
+
let searchUrl = `${version.address}/ValueSet?_elements=url,version`+(version.address.includes("fhir.org") ? "&_count=200" : "");
|
|
366
354
|
try {
|
|
367
355
|
// Set of URLs to avoid duplicates
|
|
368
356
|
const valueSetUrls = new Set();
|
|
369
357
|
|
|
370
|
-
|
|
371
358
|
// Continue fetching while we have a URL
|
|
372
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;
|
|
373
366
|
this.log.debug(`Fetching value sets from ${searchUrl}`);
|
|
374
367
|
const bundle = await this.fetchJson(searchUrl, server.code);
|
|
375
368
|
|
|
@@ -378,9 +371,9 @@ class RegistryCrawler {
|
|
|
378
371
|
bundle.entry.forEach(entry => {
|
|
379
372
|
if (entry.resource) {
|
|
380
373
|
const vs = entry.resource;
|
|
381
|
-
if (vs.url) {
|
|
374
|
+
if (vs.url && !this.isExcluded(vs.url, exclusions)) {
|
|
382
375
|
valueSetUrls.add(vs.url);
|
|
383
|
-
if (vs.version) {
|
|
376
|
+
if (vs.version && !this.isExcluded(vs.url+'|'+vs.version, exclusions)) {
|
|
384
377
|
valueSetUrls.add(`${vs.url}|${vs.version}`);
|
|
385
378
|
}
|
|
386
379
|
}
|
|
@@ -402,6 +395,7 @@ class RegistryCrawler {
|
|
|
402
395
|
version.valueSets = Array.from(valueSetUrls).sort();
|
|
403
396
|
|
|
404
397
|
} catch (error) {
|
|
398
|
+
debugLog(error);
|
|
405
399
|
this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
|
|
406
400
|
}
|
|
407
401
|
}
|
|
@@ -460,6 +454,7 @@ class RegistryCrawler {
|
|
|
460
454
|
const response = await axios.get(fetchUrl, {
|
|
461
455
|
timeout: this.config.timeout,
|
|
462
456
|
headers: headers,
|
|
457
|
+
signal: this.abortController?.signal,
|
|
463
458
|
validateStatus: (status) => status < 500 // Don't throw on 4xx
|
|
464
459
|
});
|
|
465
460
|
|
|
@@ -478,6 +473,7 @@ class RegistryCrawler {
|
|
|
478
473
|
return response.data;
|
|
479
474
|
|
|
480
475
|
} catch (error) {
|
|
476
|
+
debugLog(error);
|
|
481
477
|
if (error.response) {
|
|
482
478
|
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
483
479
|
} else if (error.request) {
|
|
@@ -621,19 +617,58 @@ class RegistryCrawler {
|
|
|
621
617
|
* @param {string} level - Filter by log level
|
|
622
618
|
* @returns {Array} Array of log entries
|
|
623
619
|
*/
|
|
624
|
-
getLogs(limit = 100)
|
|
620
|
+
getLogs(limit = 100, level = null)
|
|
625
621
|
{
|
|
626
622
|
if (!this.logs) {
|
|
627
623
|
return [];
|
|
628
624
|
}
|
|
629
625
|
|
|
630
626
|
// Filter by level if specified
|
|
631
|
-
let filteredLogs = this.logs;
|
|
627
|
+
let filteredLogs = level ? this.logs.filter(entry => entry.level === level) : this.logs;
|
|
632
628
|
|
|
633
629
|
// Get the latest entries up to the limit
|
|
634
630
|
return filteredLogs.slice(-limit);
|
|
635
631
|
}
|
|
636
632
|
|
|
633
|
+
addContent(param, content) {
|
|
634
|
+
if (content) {
|
|
635
|
+
param.content = content;
|
|
636
|
+
}
|
|
637
|
+
return param;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
compareCS(a, b) {
|
|
641
|
+
if (a.version || b.version) {
|
|
642
|
+
let s = (a.uri+'|'+a.version) || '';
|
|
643
|
+
return s.localeCompare(b.uri+'|'+b.version);
|
|
644
|
+
} else {
|
|
645
|
+
return (a.uri || '').localeCompare(b.uri);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
isExcluded(url, exclusions) {
|
|
650
|
+
for (let exclusion of exclusions || []) {
|
|
651
|
+
let match = false;
|
|
652
|
+
if (exclusion.endsWith('*')) {
|
|
653
|
+
const prefix = exclusion.slice(0, -1);
|
|
654
|
+
match = url.startsWith(prefix);
|
|
655
|
+
} else {
|
|
656
|
+
// Otherwise do exact matching on both full and base URL
|
|
657
|
+
match = url === exclusion;
|
|
658
|
+
}
|
|
659
|
+
if (match) {
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
shutdown() {
|
|
667
|
+
if (this.abortController) {
|
|
668
|
+
this.abortController.abort();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
637
672
|
}
|
|
638
673
|
|
|
639
674
|
module.exports = RegistryCrawler;
|
package/registry/model.js
CHANGED
|
@@ -37,7 +37,7 @@ class ServerVersionInformation {
|
|
|
37
37
|
getCsListHtml() {
|
|
38
38
|
if (this.codeSystems.length === 0) return '<ul></ul>';
|
|
39
39
|
return '<ul>' + this.codeSystems.map(cs =>
|
|
40
|
-
`<li>${escape(cs)}</li>`
|
|
40
|
+
`<li>${escape(cs.uri+(cs.version ? '|'+cs.version : ''))}</li>`
|
|
41
41
|
).join('') + '</ul>';
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -392,7 +392,7 @@ class ServerRegistryUtilities {
|
|
|
392
392
|
return value === mask;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
static hasMatchingCodeSystem(cs, list, supportMask) {
|
|
395
|
+
static hasMatchingCodeSystem(cs, list, supportMask, content) {
|
|
396
396
|
if (!cs || list.length === 0) return false;
|
|
397
397
|
|
|
398
398
|
// Handle URLs with pipes - extract base URL
|
|
@@ -403,13 +403,19 @@ class ServerRegistryUtilities {
|
|
|
403
403
|
|
|
404
404
|
return list.some(item => {
|
|
405
405
|
// If we support wildcards (masks) and the item ends with "*", do prefix matching
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
406
|
+
let vurl = item.uri ? item.version ? item.uri+"|"+item.version : item.uri : item;
|
|
407
|
+
let ok = false;
|
|
408
|
+
if (supportMask && vurl.endsWith('*')) {
|
|
409
|
+
const prefix = vurl.slice(0, -1);
|
|
410
|
+
ok = cs.startsWith(prefix) || baseCs.startsWith(prefix);
|
|
411
|
+
} else {
|
|
412
|
+
// Otherwise do exact matching on both full and base URL
|
|
413
|
+
ok = vurl === cs || vurl === baseCs;
|
|
409
414
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
415
|
+
if (ok && content) {
|
|
416
|
+
content.content = item.content;
|
|
417
|
+
}
|
|
418
|
+
return ok;
|
|
413
419
|
});
|
|
414
420
|
}
|
|
415
421
|
|
package/registry/registry.js
CHANGED
|
@@ -926,6 +926,9 @@ class RegistryModule {
|
|
|
926
926
|
this.crawlInterval = null;
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
+
if (this.crawler) {
|
|
930
|
+
this.crawler.shutdown();
|
|
931
|
+
}
|
|
929
932
|
// Save current data
|
|
930
933
|
if (this.crawler && this.currentData) {
|
|
931
934
|
await this.saveData();
|
|
@@ -1155,6 +1158,7 @@ class RegistryModule {
|
|
|
1155
1158
|
html += '<th>URL</th>';
|
|
1156
1159
|
html += '<th>Security</th>';
|
|
1157
1160
|
html += '<th>Access Info</th>';
|
|
1161
|
+
html += '<th>Content</th>';
|
|
1158
1162
|
html += '</tr>';
|
|
1159
1163
|
html += '</thead>';
|
|
1160
1164
|
html += '<tbody>';
|
|
@@ -1165,6 +1169,7 @@ class RegistryModule {
|
|
|
1165
1169
|
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1166
1170
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1167
1171
|
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
1172
|
+
html += `<td>${server.content ? escape(server.content) : ''}</td>`;
|
|
1168
1173
|
html += '</tr>';
|
|
1169
1174
|
});
|
|
1170
1175
|
|
package/root-template.html
CHANGED
package/server.js
CHANGED
|
@@ -395,6 +395,14 @@ process.on('uncaughtException', (error) => {
|
|
|
395
395
|
});
|
|
396
396
|
|
|
397
397
|
app.get('/', async (req, res) => {
|
|
398
|
+
// If an override index.html exists, serve it instead of the FHIRsmith home page
|
|
399
|
+
if (config.server?.webBase) {
|
|
400
|
+
const overrideIndex = path.join(path.resolve(config.server.webBase), 'index.html');
|
|
401
|
+
if (fs.existsSync(overrideIndex)) {
|
|
402
|
+
return res.sendFile(overrideIndex);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
398
406
|
// Check if client wants HTML response
|
|
399
407
|
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
400
408
|
|
|
@@ -410,69 +418,50 @@ app.get('/', async (req, res) => {
|
|
|
410
418
|
|
|
411
419
|
const content = await buildRootPageContent();
|
|
412
420
|
|
|
421
|
+
// Load optional about box fragment from data directory
|
|
422
|
+
let about = '';
|
|
423
|
+
const aboutPath = path.join(folders.dataDir(), 'about.html');
|
|
424
|
+
if (fs.existsSync(aboutPath)) {
|
|
425
|
+
about = fs.readFileSync(aboutPath, 'utf8');
|
|
426
|
+
}
|
|
427
|
+
|
|
413
428
|
// Build basic stats for root page
|
|
414
429
|
const stats = {
|
|
415
430
|
version: packageJson.version,
|
|
416
431
|
enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
|
|
417
|
-
processingTime: Date.now() - startTime
|
|
432
|
+
processingTime: Date.now() - startTime,
|
|
433
|
+
about
|
|
418
434
|
};
|
|
419
435
|
|
|
420
436
|
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
421
437
|
res.setHeader('Content-Type', 'text/html');
|
|
422
438
|
res.send(html);
|
|
439
|
+
return;
|
|
423
440
|
} catch (error) {
|
|
424
441
|
serverLog.error('Error rendering root page:', error);
|
|
425
442
|
htmlServer.sendErrorResponse(res, 'root', error);
|
|
443
|
+
return;
|
|
426
444
|
}
|
|
427
|
-
} else {
|
|
428
|
-
// Return JSON response for API clients
|
|
429
|
-
const enabledModules = {};
|
|
430
|
-
Object.keys(config.modules).forEach(moduleName => {
|
|
431
|
-
if (config.modules[moduleName].enabled) {
|
|
432
|
-
if (moduleName === 'tx') {
|
|
433
|
-
// TX module has multiple endpoints
|
|
434
|
-
enabledModules[moduleName] = {
|
|
435
|
-
enabled: true,
|
|
436
|
-
endpoints: config.modules.tx.endpoints.map(e => ({
|
|
437
|
-
path: e.path,
|
|
438
|
-
fhirVersion: e.fhirVersion,
|
|
439
|
-
context: e.context || null
|
|
440
|
-
}))
|
|
441
|
-
};
|
|
442
|
-
} else {
|
|
443
|
-
enabledModules[moduleName] = {
|
|
444
|
-
enabled: true,
|
|
445
|
-
endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
res.json({
|
|
452
|
-
message: 'FHIR Development Server',
|
|
453
|
-
version: '1.0.0',
|
|
454
|
-
modules: enabledModules,
|
|
455
|
-
endpoints: {
|
|
456
|
-
health: '/health',
|
|
457
|
-
...Object.fromEntries(
|
|
458
|
-
Object.keys(enabledModules)
|
|
459
|
-
.filter(m => m !== 'tx')
|
|
460
|
-
.map(m => [
|
|
461
|
-
m,
|
|
462
|
-
m === 'vcl' ? '/VCL' : `/${m}`
|
|
463
|
-
])
|
|
464
|
-
),
|
|
465
|
-
// Add TX endpoints separately
|
|
466
|
-
...(enabledModules.tx ? {
|
|
467
|
-
tx: config.modules.tx.endpoints.map(e => e.path)
|
|
468
|
-
} : {})
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
445
|
}
|
|
446
|
+
return serveFhirsmithHome(req, res);
|
|
472
447
|
});
|
|
473
448
|
|
|
449
|
+
app.get('/fhirsmith', (req, res) => serveFhirsmithHome(req, res));
|
|
474
450
|
|
|
475
451
|
// Serve static files
|
|
452
|
+
if (config.server?.webBase) {
|
|
453
|
+
const overrideDir = path.resolve(config.server.webBase);
|
|
454
|
+
app.use((req, res, next) => {
|
|
455
|
+
const filePath = path.join(overrideDir, req.path);
|
|
456
|
+
fs.access(filePath, fs.constants.F_OK, (err) => {
|
|
457
|
+
if (!err) {
|
|
458
|
+
res.sendFile(filePath);
|
|
459
|
+
} else {
|
|
460
|
+
next();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
476
465
|
app.use(express.static(path.join(__dirname, 'static')));
|
|
477
466
|
|
|
478
467
|
// Health check endpoint
|
|
@@ -610,5 +599,84 @@ process.on('SIGINT', async () => {
|
|
|
610
599
|
process.exit(0);
|
|
611
600
|
});
|
|
612
601
|
|
|
602
|
+
async function serveFhirsmithHome(req, res) {
|
|
603
|
+
// Check if client wants HTML response
|
|
604
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
605
|
+
|
|
606
|
+
if (acceptsHtml) {
|
|
607
|
+
try {
|
|
608
|
+
const startTime = Date.now();
|
|
609
|
+
|
|
610
|
+
// Load template if not already loaded
|
|
611
|
+
if (!htmlServer.hasTemplate('root')) {
|
|
612
|
+
const templatePath = path.join(__dirname, 'root-template.html');
|
|
613
|
+
htmlServer.loadTemplate('root', templatePath);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const content = await buildRootPageContent();
|
|
617
|
+
|
|
618
|
+
// Build basic stats for root page
|
|
619
|
+
const stats = {
|
|
620
|
+
version: packageJson.version,
|
|
621
|
+
enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
|
|
622
|
+
processingTime: Date.now() - startTime
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
626
|
+
res.setHeader('Content-Type', 'text/html');
|
|
627
|
+
res.send(html);
|
|
628
|
+
return;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
serverLog.error('Error rendering root page:', error);
|
|
631
|
+
htmlServer.sendErrorResponse(res, 'root', error);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
// Return JSON response for API clients
|
|
636
|
+
const enabledModules = {};
|
|
637
|
+
Object.keys(config.modules).forEach(moduleName => {
|
|
638
|
+
if (config.modules[moduleName].enabled) {
|
|
639
|
+
if (moduleName === 'tx') {
|
|
640
|
+
// TX module has multiple endpoints
|
|
641
|
+
enabledModules[moduleName] = {
|
|
642
|
+
enabled: true,
|
|
643
|
+
endpoints: config.modules.tx.endpoints.map(e => ({
|
|
644
|
+
path: e.path,
|
|
645
|
+
fhirVersion: e.fhirVersion,
|
|
646
|
+
context: e.context || null
|
|
647
|
+
}))
|
|
648
|
+
};
|
|
649
|
+
} else {
|
|
650
|
+
enabledModules[moduleName] = {
|
|
651
|
+
enabled: true,
|
|
652
|
+
endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
res.json({
|
|
659
|
+
message: 'FHIR Development Server',
|
|
660
|
+
version: '1.0.0',
|
|
661
|
+
modules: enabledModules,
|
|
662
|
+
endpoints: {
|
|
663
|
+
health: '/health',
|
|
664
|
+
...Object.fromEntries(
|
|
665
|
+
Object.keys(enabledModules)
|
|
666
|
+
.filter(m => m !== 'tx')
|
|
667
|
+
.map(m => [
|
|
668
|
+
m,
|
|
669
|
+
m === 'vcl' ? '/VCL' : `/${m}`
|
|
670
|
+
])
|
|
671
|
+
),
|
|
672
|
+
// Add TX endpoints separately
|
|
673
|
+
...(enabledModules.tx ? {
|
|
674
|
+
tx: config.modules.tx.endpoints.map(e => e.path)
|
|
675
|
+
} : {})
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
613
681
|
// Start the server
|
|
614
682
|
startServer();
|
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
|
]
|