fhirsmith 0.5.6 → 0.6.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 +18 -0
- package/library/html-server.js +2 -1
- package/library/package-manager.js +37 -32
- package/library/utilities.js +10 -1
- package/package.json +1 -1
- package/packages/package-crawler.js +103 -46
- package/packages/packages.js +14 -7
- package/publisher/publisher.js +15 -3
- package/registry/api.js +173 -191
- package/registry/crawler.js +72 -58
- package/registry/model.js +14 -8
- package/registry/registry.js +2 -0
- package/root-template.html +1 -0
- package/server.js +109 -45
- package/tx/cs/cs-api.js +18 -1
- package/tx/cs/cs-base.js +1 -0
- 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/codesystem.js +6 -1
- package/tx/library/renderer.js +81 -7
- package/tx/library.js +18 -3
- package/tx/provider.js +4 -2
- package/tx/sct/expressions.js +20 -9
- package/tx/tx-html.js +143 -50
- package/tx/tx.js +2 -2
- package/tx/workers/expand.js +61 -9
- package/tx/workers/search.js +5 -2
- package/tx/xversion/xv-terminologyCapabilities.js +1 -1
package/registry/crawler.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
ServerInformation,
|
|
9
9
|
ServerVersionInformation,
|
|
10
10
|
} = require('./model');
|
|
11
|
+
const {Extensions} = require("../tx/library/extensions");
|
|
11
12
|
|
|
12
13
|
const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
|
|
13
14
|
|
|
@@ -36,32 +37,6 @@ class RegistryCrawler {
|
|
|
36
37
|
this.log = logv;
|
|
37
38
|
}
|
|
38
39
|
|
|
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
40
|
|
|
66
41
|
/**
|
|
67
42
|
* Main entry point - crawl the registry starting from the master URL
|
|
@@ -111,6 +86,7 @@ class RegistryCrawler {
|
|
|
111
86
|
// Update the current data
|
|
112
87
|
this.currentData = newData;
|
|
113
88
|
} catch (error) {
|
|
89
|
+
console.log(error);
|
|
114
90
|
this.addLogEntry('error', 'Exception Scanning:', error);
|
|
115
91
|
this.currentData.outcome = `Error: ${error.message}`;
|
|
116
92
|
this.errors.push({
|
|
@@ -165,6 +141,7 @@ class RegistryCrawler {
|
|
|
165
141
|
}
|
|
166
142
|
|
|
167
143
|
} catch (error) {
|
|
144
|
+
console.log(error);
|
|
168
145
|
registry.error = error.message;
|
|
169
146
|
this.addLogEntry('error', `Exception processing registry ${registry.name}: ${error.message}`, registry.address);
|
|
170
147
|
}
|
|
@@ -181,7 +158,7 @@ class RegistryCrawler {
|
|
|
181
158
|
server.name = serverConfig.name;
|
|
182
159
|
server.address = serverConfig.url || '';
|
|
183
160
|
server.accessInfo = serverConfig.access_info || '';
|
|
184
|
-
|
|
161
|
+
|
|
185
162
|
if (!server.name) {
|
|
186
163
|
this.addLogEntry('error', 'No name provided for server', source);
|
|
187
164
|
return server;
|
|
@@ -200,7 +177,7 @@ class RegistryCrawler {
|
|
|
200
177
|
// Process each FHIR version
|
|
201
178
|
const fhirVersions = serverConfig.fhirVersions || [];
|
|
202
179
|
for (const versionConfig of fhirVersions) {
|
|
203
|
-
const version = await this.processServerVersion(versionConfig, server);
|
|
180
|
+
const version = await this.processServerVersion(versionConfig, server, serverConfig.exclusions);
|
|
204
181
|
if (version) {
|
|
205
182
|
server.versions.push(version);
|
|
206
183
|
}
|
|
@@ -212,7 +189,7 @@ class RegistryCrawler {
|
|
|
212
189
|
/**
|
|
213
190
|
* Process a single server version
|
|
214
191
|
*/
|
|
215
|
-
async processServerVersion(versionConfig, server) {
|
|
192
|
+
async processServerVersion(versionConfig, server, exclusions) {
|
|
216
193
|
const version = new ServerVersionInformation();
|
|
217
194
|
version.version = versionConfig.version;
|
|
218
195
|
version.address = versionConfig.url;
|
|
@@ -233,20 +210,20 @@ class RegistryCrawler {
|
|
|
233
210
|
|
|
234
211
|
switch (majorVersion) {
|
|
235
212
|
case 3:
|
|
236
|
-
await this.processServerVersionR3(version, server);
|
|
213
|
+
await this.processServerVersionR3(version, server, exclusions);
|
|
237
214
|
break;
|
|
238
215
|
case 4:
|
|
239
|
-
await this.
|
|
216
|
+
await this.processServerVersionR4or5(version, server, '4.0.1', exclusions);
|
|
240
217
|
break;
|
|
241
218
|
case 5:
|
|
242
|
-
await this.
|
|
219
|
+
await this.processServerVersionR4or5(version, server, '5.0.0', exclusions);
|
|
243
220
|
break;
|
|
244
221
|
default:
|
|
245
222
|
throw new Error(`Version ${version.version} not supported`);
|
|
246
223
|
}
|
|
247
224
|
|
|
248
225
|
// Sort and deduplicate
|
|
249
|
-
version.codeSystems
|
|
226
|
+
version.codeSystems.sort((a, b) => this.compareCS(a, b));
|
|
250
227
|
version.valueSets = [...new Set(version.valueSets)].sort();
|
|
251
228
|
version.lastSuccess = new Date();
|
|
252
229
|
version.lastTat = `${Date.now() - startTime}ms`;
|
|
@@ -254,6 +231,7 @@ class RegistryCrawler {
|
|
|
254
231
|
this.addLogEntry('info', ` Server ${version.address}: ${version.lastTat} for ${version.codeSystems.length} CodeSystems and ${version.valueSets.length} ValueSets`);
|
|
255
232
|
|
|
256
233
|
} catch (error) {
|
|
234
|
+
console.log(error);
|
|
257
235
|
const elapsed = Date.now() - startTime;
|
|
258
236
|
this.addLogEntry('error', `Server ${version.address}: Error after ${elapsed}ms: ${error.message}`);
|
|
259
237
|
version.error = error.message;
|
|
@@ -266,7 +244,7 @@ class RegistryCrawler {
|
|
|
266
244
|
/**
|
|
267
245
|
* Process an R3 server
|
|
268
246
|
*/
|
|
269
|
-
async processServerVersionR3(version, server) {
|
|
247
|
+
async processServerVersionR3(version, server, exclusions) {
|
|
270
248
|
// Get capability statement
|
|
271
249
|
const capabilityUrl = `${version.address}/metadata`;
|
|
272
250
|
const capability = await this.fetchJson(capabilityUrl, server.name);
|
|
@@ -283,12 +261,12 @@ class RegistryCrawler {
|
|
|
283
261
|
termCap.parameter.forEach(param => {
|
|
284
262
|
if (param.name === 'system') {
|
|
285
263
|
const uri = param.valueUri || param.valueString;
|
|
286
|
-
if (uri) {
|
|
264
|
+
if (uri && !this.isExcluded(uri, exclusions)) {
|
|
287
265
|
version.codeSystems.push(uri);
|
|
288
266
|
// Look for version parts
|
|
289
267
|
if (param.part) {
|
|
290
268
|
param.part.forEach(part => {
|
|
291
|
-
if (part.name === 'version' && part.valueString) {
|
|
269
|
+
if (part.name === 'version' && part.valueString && !this.isExcluded(uri+'|'+part.valueString, exclusions)) {
|
|
292
270
|
version.codeSystems.push(`${uri}|${part.valueString}`);
|
|
293
271
|
}
|
|
294
272
|
});
|
|
@@ -298,24 +276,27 @@ class RegistryCrawler {
|
|
|
298
276
|
});
|
|
299
277
|
}
|
|
300
278
|
} catch (error) {
|
|
279
|
+
console.log(error);
|
|
301
280
|
this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
|
|
302
281
|
}
|
|
303
282
|
|
|
304
283
|
// Search for value sets
|
|
305
|
-
await this.fetchValueSets(version, server);
|
|
284
|
+
await this.fetchValueSets(version, server, exclusions);
|
|
306
285
|
}
|
|
307
286
|
|
|
308
287
|
/**
|
|
309
288
|
* Process an R4 server
|
|
310
289
|
*/
|
|
311
|
-
async
|
|
290
|
+
async processServerVersionR4or5(version, server, defVersion, exclusions) {
|
|
312
291
|
// Get capability statement
|
|
313
292
|
const capabilityUrl = `${version.address}/metadata`;
|
|
314
293
|
const capability = await this.fetchJson(capabilityUrl, server.code);
|
|
315
294
|
|
|
316
|
-
version.version = capability.fhirVersion ||
|
|
295
|
+
version.version = capability.fhirVersion || defVersion;
|
|
317
296
|
version.software = capability.software ? capability.software.name : "unknown";
|
|
318
|
-
|
|
297
|
+
|
|
298
|
+
let set = new Set();
|
|
299
|
+
|
|
319
300
|
// Get terminology capabilities
|
|
320
301
|
try {
|
|
321
302
|
const termCapUrl = `${version.address}/metadata?mode=terminology`;
|
|
@@ -323,12 +304,19 @@ class RegistryCrawler {
|
|
|
323
304
|
|
|
324
305
|
if (termCap.codeSystem) {
|
|
325
306
|
termCap.codeSystem.forEach(cs => {
|
|
326
|
-
|
|
327
|
-
|
|
307
|
+
let content = cs.content || Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
|
|
308
|
+
if (cs.uri && !this.isExcluded(cs.uri, exclusions)) {
|
|
309
|
+
if (!set.has(cs.uri)) {
|
|
310
|
+
set.add(cs.uri);
|
|
311
|
+
version.codeSystems.push(this.addContent({uri: cs.uri}, content));
|
|
312
|
+
}
|
|
328
313
|
if (cs.version) {
|
|
329
314
|
cs.version.forEach(v => {
|
|
330
|
-
if (v.code) {
|
|
331
|
-
|
|
315
|
+
if (v.code && !this.isExcluded(cs.uri+"|"+v.code, exclusions)) {
|
|
316
|
+
if (!set.has(cs.uri+"|"+v.code)) {
|
|
317
|
+
version.codeSystems.push(this.addContent({uri: cs.uri, version: v.code}, content));
|
|
318
|
+
set.add(cs.uri+"|"+v.code);
|
|
319
|
+
}
|
|
332
320
|
}
|
|
333
321
|
});
|
|
334
322
|
}
|
|
@@ -336,20 +324,12 @@ class RegistryCrawler {
|
|
|
336
324
|
});
|
|
337
325
|
}
|
|
338
326
|
} catch (error) {
|
|
327
|
+
console.log(error);
|
|
339
328
|
this.addLogEntry('error', `Could not fetch terminology capabilities: ${error.message}`);
|
|
340
329
|
}
|
|
341
330
|
|
|
342
331
|
// 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';
|
|
332
|
+
await this.fetchValueSets(version, server, exclusions);
|
|
353
333
|
}
|
|
354
334
|
|
|
355
335
|
/**
|
|
@@ -360,9 +340,9 @@ class RegistryCrawler {
|
|
|
360
340
|
* @param {Object} version - The server version information
|
|
361
341
|
* @param {Object} server - The server information
|
|
362
342
|
*/
|
|
363
|
-
async fetchValueSets(version, server) {
|
|
343
|
+
async fetchValueSets(version, server, exclusions) {
|
|
364
344
|
// Initial search URL
|
|
365
|
-
let searchUrl = `${version.address}/ValueSet?_elements=url,version
|
|
345
|
+
let searchUrl = `${version.address}/ValueSet?_elements=url,version`+(version.address.includes("fhir.org") ? "&_count=200" : "");
|
|
366
346
|
try {
|
|
367
347
|
// Set of URLs to avoid duplicates
|
|
368
348
|
const valueSetUrls = new Set();
|
|
@@ -378,9 +358,9 @@ class RegistryCrawler {
|
|
|
378
358
|
bundle.entry.forEach(entry => {
|
|
379
359
|
if (entry.resource) {
|
|
380
360
|
const vs = entry.resource;
|
|
381
|
-
if (vs.url) {
|
|
361
|
+
if (vs.url && !this.isExcluded(vs.url, exclusions)) {
|
|
382
362
|
valueSetUrls.add(vs.url);
|
|
383
|
-
if (vs.version) {
|
|
363
|
+
if (vs.version && !this.isExcluded(vs.url+'|'+vs.version, exclusions)) {
|
|
384
364
|
valueSetUrls.add(`${vs.url}|${vs.version}`);
|
|
385
365
|
}
|
|
386
366
|
}
|
|
@@ -402,6 +382,7 @@ class RegistryCrawler {
|
|
|
402
382
|
version.valueSets = Array.from(valueSetUrls).sort();
|
|
403
383
|
|
|
404
384
|
} catch (error) {
|
|
385
|
+
console.log(error);
|
|
405
386
|
this.addLogEntry('error', `Could not fetch value sets: ${error.message} from ${searchUrl}`);
|
|
406
387
|
}
|
|
407
388
|
}
|
|
@@ -478,6 +459,7 @@ class RegistryCrawler {
|
|
|
478
459
|
return response.data;
|
|
479
460
|
|
|
480
461
|
} catch (error) {
|
|
462
|
+
console.log(error);
|
|
481
463
|
if (error.response) {
|
|
482
464
|
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
483
465
|
} else if (error.request) {
|
|
@@ -634,6 +616,38 @@ class RegistryCrawler {
|
|
|
634
616
|
return filteredLogs.slice(-limit);
|
|
635
617
|
}
|
|
636
618
|
|
|
619
|
+
addContent(param, content) {
|
|
620
|
+
if (content) {
|
|
621
|
+
param.content = content;
|
|
622
|
+
}
|
|
623
|
+
return param;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
compareCS(a, b) {
|
|
627
|
+
if (a.version || b.version) {
|
|
628
|
+
let s = (a.uri+'|'+a.version) || '';
|
|
629
|
+
return s.localeCompare(b.uri+'|'+b.version);
|
|
630
|
+
} else {
|
|
631
|
+
return (a.uri || '').localeCompare(b.uri);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
isExcluded(url, exclusions) {
|
|
636
|
+
for (let exclusion of exclusions || []) {
|
|
637
|
+
let match = false;
|
|
638
|
+
if (exclusion.endsWith('*')) {
|
|
639
|
+
const prefix = exclusion.slice(0, -1);
|
|
640
|
+
match = url.startsWith(prefix);
|
|
641
|
+
} else {
|
|
642
|
+
// Otherwise do exact matching on both full and base URL
|
|
643
|
+
match = url === exclusion;
|
|
644
|
+
}
|
|
645
|
+
if (match) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
637
651
|
}
|
|
638
652
|
|
|
639
653
|
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
|
@@ -1155,6 +1155,7 @@ class RegistryModule {
|
|
|
1155
1155
|
html += '<th>URL</th>';
|
|
1156
1156
|
html += '<th>Security</th>';
|
|
1157
1157
|
html += '<th>Access Info</th>';
|
|
1158
|
+
html += '<th>Content</th>';
|
|
1158
1159
|
html += '</tr>';
|
|
1159
1160
|
html += '</thead>';
|
|
1160
1161
|
html += '<tbody>';
|
|
@@ -1165,6 +1166,7 @@ class RegistryModule {
|
|
|
1165
1166
|
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1166
1167
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1167
1168
|
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
1169
|
+
html += `<td>${server.content ? escape(server.content) : ''}</td>`;
|
|
1168
1170
|
html += '</tr>';
|
|
1169
1171
|
});
|
|
1170
1172
|
|
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,11 +418,19 @@ 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);
|
|
@@ -424,55 +440,26 @@ app.get('/', async (req, res) => {
|
|
|
424
440
|
serverLog.error('Error rendering root page:', error);
|
|
425
441
|
htmlServer.sendErrorResponse(res, 'root', error);
|
|
426
442
|
}
|
|
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
443
|
}
|
|
444
|
+
return serveFhirsmithHome(req, res);
|
|
472
445
|
});
|
|
473
446
|
|
|
447
|
+
app.get('/fhirsmith', (req, res) => serveFhirsmithHome(req, res));
|
|
474
448
|
|
|
475
449
|
// Serve static files
|
|
450
|
+
if (config.server?.webBase) {
|
|
451
|
+
const overrideDir = path.resolve(config.server.webBase);
|
|
452
|
+
app.use((req, res, next) => {
|
|
453
|
+
const filePath = path.join(overrideDir, req.path);
|
|
454
|
+
fs.access(filePath, fs.constants.F_OK, (err) => {
|
|
455
|
+
if (!err) {
|
|
456
|
+
res.sendFile(filePath);
|
|
457
|
+
} else {
|
|
458
|
+
next();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
476
463
|
app.use(express.static(path.join(__dirname, 'static')));
|
|
477
464
|
|
|
478
465
|
// Health check endpoint
|
|
@@ -610,5 +597,82 @@ process.on('SIGINT', async () => {
|
|
|
610
597
|
process.exit(0);
|
|
611
598
|
});
|
|
612
599
|
|
|
600
|
+
async function serveFhirsmithHome(req, res) {
|
|
601
|
+
// Check if client wants HTML response
|
|
602
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
603
|
+
|
|
604
|
+
if (acceptsHtml) {
|
|
605
|
+
try {
|
|
606
|
+
const startTime = Date.now();
|
|
607
|
+
|
|
608
|
+
// Load template if not already loaded
|
|
609
|
+
if (!htmlServer.hasTemplate('root')) {
|
|
610
|
+
const templatePath = path.join(__dirname, 'root-template.html');
|
|
611
|
+
htmlServer.loadTemplate('root', templatePath);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const content = await buildRootPageContent();
|
|
615
|
+
|
|
616
|
+
// Build basic stats for root page
|
|
617
|
+
const stats = {
|
|
618
|
+
version: packageJson.version,
|
|
619
|
+
enabledModules: Object.keys(config.modules).filter(m => config.modules[m].enabled).length,
|
|
620
|
+
processingTime: Date.now() - startTime
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
624
|
+
res.setHeader('Content-Type', 'text/html');
|
|
625
|
+
res.send(html);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
serverLog.error('Error rendering root page:', error);
|
|
628
|
+
htmlServer.sendErrorResponse(res, 'root', error);
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
// Return JSON response for API clients
|
|
632
|
+
const enabledModules = {};
|
|
633
|
+
Object.keys(config.modules).forEach(moduleName => {
|
|
634
|
+
if (config.modules[moduleName].enabled) {
|
|
635
|
+
if (moduleName === 'tx') {
|
|
636
|
+
// TX module has multiple endpoints
|
|
637
|
+
enabledModules[moduleName] = {
|
|
638
|
+
enabled: true,
|
|
639
|
+
endpoints: config.modules.tx.endpoints.map(e => ({
|
|
640
|
+
path: e.path,
|
|
641
|
+
fhirVersion: e.fhirVersion,
|
|
642
|
+
context: e.context || null
|
|
643
|
+
}))
|
|
644
|
+
};
|
|
645
|
+
} else {
|
|
646
|
+
enabledModules[moduleName] = {
|
|
647
|
+
enabled: true,
|
|
648
|
+
endpoint: moduleName === 'vcl' ? '/VCL' : `/${moduleName}`
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
res.json({
|
|
655
|
+
message: 'FHIR Development Server',
|
|
656
|
+
version: '1.0.0',
|
|
657
|
+
modules: enabledModules,
|
|
658
|
+
endpoints: {
|
|
659
|
+
health: '/health',
|
|
660
|
+
...Object.fromEntries(
|
|
661
|
+
Object.keys(enabledModules)
|
|
662
|
+
.filter(m => m !== 'tx')
|
|
663
|
+
.map(m => [
|
|
664
|
+
m,
|
|
665
|
+
m === 'vcl' ? '/VCL' : `/${m}`
|
|
666
|
+
])
|
|
667
|
+
),
|
|
668
|
+
// Add TX endpoints separately
|
|
669
|
+
...(enabledModules.tx ? {
|
|
670
|
+
tx: config.modules.tx.endpoints.map(e => e.path)
|
|
671
|
+
} : {})
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
613
677
|
// Start the server
|
|
614
678
|
startServer();
|
package/tx/cs/cs-api.js
CHANGED
|
@@ -774,12 +774,19 @@ class CodeSystemFactoryProvider {
|
|
|
774
774
|
}
|
|
775
775
|
|
|
776
776
|
/**
|
|
777
|
-
* @returns {string}
|
|
777
|
+
* @returns {string} name for the code system
|
|
778
778
|
*/
|
|
779
779
|
name() {
|
|
780
780
|
throw new Error("Must override");
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
/**
|
|
784
|
+
* @returns {string} name for the code system, without version information
|
|
785
|
+
*/
|
|
786
|
+
nameBase() {
|
|
787
|
+
return this.name();
|
|
788
|
+
}
|
|
789
|
+
|
|
783
790
|
/**
|
|
784
791
|
* @returns {string} version for the code system
|
|
785
792
|
*/
|
|
@@ -796,6 +803,16 @@ class CodeSystemFactoryProvider {
|
|
|
796
803
|
}
|
|
797
804
|
return ver;
|
|
798
805
|
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* the version parameter might not be the same as version() once
|
|
809
|
+
* all matching rules are done
|
|
810
|
+
* @param version
|
|
811
|
+
*/
|
|
812
|
+
describeVersion(version) {
|
|
813
|
+
return "v"+version;
|
|
814
|
+
}
|
|
815
|
+
|
|
799
816
|
/**
|
|
800
817
|
* @returns {number} how many times the factory has been asked to construct a provider
|
|
801
818
|
*/
|
package/tx/cs/cs-base.js
CHANGED
package/tx/cs/cs-rxnorm.js
CHANGED
|
@@ -3,7 +3,7 @@ const assert = require('assert');
|
|
|
3
3
|
const { CodeSystem } = require('../library/codesystem');
|
|
4
4
|
const { CodeSystemProvider, CodeSystemFactoryProvider } = require('./cs-api');
|
|
5
5
|
const {Designations} = require("../library/designations");
|
|
6
|
-
const {validateArrayParameter} = require("../../library/utilities");
|
|
6
|
+
const {validateArrayParameter, formatDateMMDDYYYY} = require("../../library/utilities");
|
|
7
7
|
|
|
8
8
|
// Context for RxNorm concepts
|
|
9
9
|
class RxNormConcept {
|
|
@@ -806,9 +806,16 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
|
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
id() {
|
|
809
|
-
return this.name();
|
|
809
|
+
return this.name()+"-"+this.version();
|
|
810
810
|
}
|
|
811
811
|
|
|
812
|
+
describeVersion(version) {
|
|
813
|
+
try {
|
|
814
|
+
return formatDateMMDDYYYY(version);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
return "v" + version;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
812
819
|
}
|
|
813
820
|
|
|
814
821
|
// Specific RxNorm implementation
|
package/tx/cs/cs-snomed.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
} = require('../sct/expressions');
|
|
12
12
|
const {DesignationUse} = require("../library/designations");
|
|
13
13
|
const {BaseCSServices} = require("./cs-base");
|
|
14
|
+
const {formatDateMMDDYYYY} = require("../../library/utilities");
|
|
14
15
|
|
|
15
16
|
// Context kinds matching Pascal enum
|
|
16
17
|
const SnomedProviderContextKind = {
|
|
@@ -1262,11 +1263,25 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1262
1263
|
return `SCT ${getEditionCode(this._sharedData.edition)}`;
|
|
1263
1264
|
}
|
|
1264
1265
|
|
|
1266
|
+
nameBase() {
|
|
1267
|
+
return `SCT`;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1265
1270
|
id() {
|
|
1266
|
-
|
|
1271
|
+
const match = this.version().match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
|
|
1272
|
+
return "SCT-"+match[1]+"-"+match[2];
|
|
1267
1273
|
}
|
|
1268
|
-
}
|
|
1269
1274
|
|
|
1275
|
+
describeVersion(version) {
|
|
1276
|
+
const match = version.match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
|
|
1277
|
+
if (!match) return version;
|
|
1278
|
+
|
|
1279
|
+
const edition = getEditionName(match[1]);
|
|
1280
|
+
if (!match[2]) return edition;
|
|
1281
|
+
|
|
1282
|
+
return edition + ' ' + formatDateMMDDYYYY(match[2].substring(4, 6) + match[2].substring(6, 8) + match[2].substring(0, 4));
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1270
1285
|
|
|
1271
1286
|
function getEditionName(edition) {
|
|
1272
1287
|
const editionMap = {
|