fhirsmith 0.18.0 → 0.18.2
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 +72 -10
- package/ips/ips.js +251 -26
- package/ips/jurisdictions.js +65 -0
- package/ips/pipeline.js +67 -6
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +48 -5
- package/ips/render.js +317 -46
- package/ips/scripts.js +46 -0
- package/ips/store.js +55 -0
- package/library/daily-report.js +25 -15
- package/package.json +3 -3
- package/registry/api.js +179 -1
- package/registry/crawler.js +2 -0
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry.js +53 -2
- package/translations/Messages.properties +1 -0
- package/tx/README.md +4 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cm/cm-package.js +21 -15
- package/tx/cs/cs-api.js +3 -1
- package/tx/cs/cs-cs.js +59 -12
- package/tx/cs/cs-icf.js +47 -34
- package/tx/cs/cs-loinc.js +257 -3
- package/tx/cs/cs-snomed.js +544 -55
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +131 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library/tho-precedence.js +40 -0
- package/tx/openapi-operations.js +2 -1
- package/tx/provider.js +46 -29
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +34 -11
- package/tx/sct/structures.js +15 -2
- package/tx/tx.fhir.org.yml +2 -2
- package/tx/vs/vs-package.js +17 -13
- package/tx/vs/vs-vsac.js +1 -1
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +2 -2
- package/tx/workers/validate.js +10 -0
- package/utilities/make-sct-loinc-subset.js +250 -0
package/library/daily-report.js
CHANGED
|
@@ -85,6 +85,15 @@ function formatSpan(ms) {
|
|
|
85
85
|
return parts.join(' ');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** "24 hours", "3 days" - for the report title; rounded, since it's a description of the period */
|
|
89
|
+
function describeSpan(ms) {
|
|
90
|
+
const hours = Math.round(ms / 3600000);
|
|
91
|
+
if (hours < 48) {
|
|
92
|
+
return plural(Math.max(1, hours), 'hour');
|
|
93
|
+
}
|
|
94
|
+
return plural(Math.round(hours / 24), 'day');
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
function plural(n, word) {
|
|
89
98
|
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
90
99
|
}
|
|
@@ -243,8 +252,7 @@ class DailyReport {
|
|
|
243
252
|
*/
|
|
244
253
|
async build(since, now = Date.now()) {
|
|
245
254
|
const parts = [];
|
|
246
|
-
parts.push(`# ${this.serverId} daily report`);
|
|
247
|
-
parts.push(`${localDateTime(now)}, covering the ${formatSpan(now - since)} since ${localDateTime(since)}.`);
|
|
255
|
+
parts.push(`# ${this.serverId} daily report for ${localDateTime(now)} covering the last ${describeSpan(now - since)}`);
|
|
248
256
|
parts.push('## Stability');
|
|
249
257
|
parts.push(this.stabilityParagraph(since, now));
|
|
250
258
|
for (const [name, module] of Object.entries(this.modules)) {
|
|
@@ -347,25 +355,26 @@ class DailyReport {
|
|
|
347
355
|
* the disk they're on.
|
|
348
356
|
*/
|
|
349
357
|
systemParagraph(now) {
|
|
350
|
-
|
|
351
|
-
|
|
358
|
+
// one line each; the two trailing spaces are markdown's line break
|
|
359
|
+
const lines = [];
|
|
360
|
+
lines.push(`Versions: FHIRsmith = v${this.version}. Node = ${process.version} (${os.type()} ${os.release()}, ${os.arch()})`);
|
|
352
361
|
if (this.stats && typeof this.stats.startTime === 'number') {
|
|
353
|
-
let up = `This run
|
|
362
|
+
let up = `Uptime: This run = ${formatSpan(now - this.stats.startTime)}`;
|
|
354
363
|
const overall = typeof this.stats.overallUptime === 'function' ? this.stats.overallUptime(now) : null;
|
|
355
364
|
if (overall) {
|
|
356
365
|
const pct = overall.percent >= 100 ? '100' : (Math.floor(overall.percent * 10) / 10).toFixed(1);
|
|
357
|
-
up +=
|
|
366
|
+
up += `. All runs, ${pct}%`;
|
|
358
367
|
}
|
|
359
|
-
|
|
368
|
+
lines.push(up);
|
|
360
369
|
}
|
|
361
|
-
|
|
370
|
+
lines.push(`Space: ${this.logsText()}`);
|
|
362
371
|
if (this.npmAudit && typeof this.npmAudit.reportText === 'function') {
|
|
363
372
|
const audit = this.npmAudit.reportText();
|
|
364
373
|
if (audit) {
|
|
365
|
-
|
|
374
|
+
lines.push(`Audit: ${audit}`);
|
|
366
375
|
}
|
|
367
376
|
}
|
|
368
|
-
return
|
|
377
|
+
return lines.join(' \n');
|
|
369
378
|
}
|
|
370
379
|
|
|
371
380
|
/**
|
|
@@ -388,7 +397,7 @@ class DailyReport {
|
|
|
388
397
|
return `${plural(failed.length, 'background task')} ${failed.length === 1 ? 'is' : 'are'} in a failed state:\n\n${lines.join('\n')}`;
|
|
389
398
|
}
|
|
390
399
|
|
|
391
|
-
|
|
400
|
+
logsText() {
|
|
392
401
|
const logDir = folders.logsDir();
|
|
393
402
|
try {
|
|
394
403
|
const files = fs.readdirSync(logDir).filter(f => f.endsWith('.log'));
|
|
@@ -402,13 +411,13 @@ class DailyReport {
|
|
|
402
411
|
const free = fstats.bavail * fstats.bsize;
|
|
403
412
|
const size = fstats.blocks * fstats.bsize;
|
|
404
413
|
const freePct = size > 0 ? Math.round((free * 100) / size) : 0;
|
|
405
|
-
disk =
|
|
414
|
+
disk = ` on a disk with ${formatBytes(free)} free of ${formatBytes(size)} (${freePct}% free)`;
|
|
406
415
|
} catch {
|
|
407
|
-
disk = '
|
|
416
|
+
disk = ' on a disk whose free space could not be read';
|
|
408
417
|
}
|
|
409
|
-
return
|
|
418
|
+
return `${plural(files.length, 'log file')} totalling ${formatBytes(total)}${disk}.`;
|
|
410
419
|
} catch (e) {
|
|
411
|
-
return `
|
|
420
|
+
return `the logs folder (${logDir}) could not be read: ${e.message}.`;
|
|
412
421
|
}
|
|
413
422
|
}
|
|
414
423
|
}
|
|
@@ -417,6 +426,7 @@ module.exports = DailyReport;
|
|
|
417
426
|
module.exports.nextMidnight = nextMidnight;
|
|
418
427
|
module.exports.localDate = localDate;
|
|
419
428
|
module.exports.formatSpan = formatSpan;
|
|
429
|
+
module.exports.describeSpan = describeSpan;
|
|
420
430
|
// for modules writing their paragraph: "3 packages", "1 package"
|
|
421
431
|
module.exports.plural = plural;
|
|
422
432
|
module.exports.localDateTime = localDateTime;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirsmith",
|
|
3
|
-
"version": "0.18.
|
|
4
|
-
"txVersion": "1.
|
|
3
|
+
"version": "0.18.2",
|
|
4
|
+
"txVersion": "1.10.1",
|
|
5
5
|
"description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
|
|
6
6
|
"main": "server.js",
|
|
7
7
|
"engines": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"express-rate-limit": "^7.4.1",
|
|
49
49
|
"express-session": "^1.19.0",
|
|
50
50
|
"fast-xml-parser": "^5.3.4",
|
|
51
|
-
"fhir-validator-wrapper": "1.
|
|
51
|
+
"fhir-validator-wrapper": "1.6.1",
|
|
52
52
|
"fhirpath": "^5.2.1",
|
|
53
53
|
"fs-extra": "^11.3.3",
|
|
54
54
|
"ini": "^6.0.0",
|
package/registry/api.js
CHANGED
|
@@ -27,6 +27,20 @@ function securityFlags(security) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Code systems that tx.fhir.org handles specially. When the registry doesn't settle where
|
|
31
|
+
// a request involving one of these goes, it goes to the primary server (see chooseServer)
|
|
32
|
+
const SNOMED_CT = 'http://snomed.info/sct';
|
|
33
|
+
const PRIMARY_SERVER_SYSTEMS = ['http://unitsofmeasure.org', 'http://loinc.org', SNOMED_CT,
|
|
34
|
+
'http://www.nlm.nih.gov/research/umls/rxnorm', 'http://hl7.org/fhir/sid/cvx', 'urn:ietf:bcp:13', 'urn:ietf:bcp:47',
|
|
35
|
+
'urn:ietf:rfc:3986', 'http://www.ama-assn.org/go/cpt', 'urn:oid:1.2.36.1.2001.1005.17', 'urn:iso:std:iso:3166',
|
|
36
|
+
'http://varnomen.hgvs.org', 'http://unstats.un.org/unsd/methods/m49/m49.htm', 'urn:iso:std:iso:4217',
|
|
37
|
+
'http://hl7.org/fhir/sid/ndc', 'http://fhir.ohdsi.org/CodeSystem/concepts', 'http://fdasis.nlm.nih.gov', 'https://www.usps.com/'];
|
|
38
|
+
|
|
39
|
+
function isTxFhirOrg(server) {
|
|
40
|
+
const s = server.replace('http://', 'https://');
|
|
41
|
+
return s.startsWith('https://tx.fhir.org/') || s.startsWith('https://tx-dev.fhir.org/');
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
class RegistryAPI {
|
|
31
45
|
constructor(crawler) {
|
|
32
46
|
this.crawler = crawler;
|
|
@@ -592,6 +606,170 @@ class RegistryAPI {
|
|
|
592
606
|
};
|
|
593
607
|
}
|
|
594
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Choose one server for a request that involves a set of code systems.
|
|
611
|
+
*
|
|
612
|
+
* This is a server side copy of the routing logic in the HL7 Java tooling
|
|
613
|
+
* (TerminologyClientManager.chooseServer in org.hl7.fhir.core), for clients that aren't
|
|
614
|
+
* Java. It doesn't replace that code. The Java client does the work that needs its own
|
|
615
|
+
* context before routing, and so must the caller here:
|
|
616
|
+
*
|
|
617
|
+
* - systems: the code systems involved, as url or url|version, from the codings and the
|
|
618
|
+
* value set's includes and excludes, following imported value sets. Versions fixed by
|
|
619
|
+
* expansion parameters (system-version, force-system-version) should already be applied.
|
|
620
|
+
* If the caller couldn't follow an import, it should just use its default server.
|
|
621
|
+
* - language: only for language sensitive requests (see resolveCodeSystem).
|
|
622
|
+
*
|
|
623
|
+
* Differences from the Java code:
|
|
624
|
+
* - 'Does a candidate really have the code system' is answered from the content mode the
|
|
625
|
+
* server reported in its TerminologyCapabilities, not by searching the server. If it
|
|
626
|
+
* didn't report one, the candidate is assumed to be usable.
|
|
627
|
+
* - The 'primary server' for the systems tx.fhir.org handles specially is the default.
|
|
628
|
+
* - The value set's source server is found by resolving the value set here.
|
|
629
|
+
* - Ties are broken by order (the first system, then registration order), not hash order.
|
|
630
|
+
*
|
|
631
|
+
* @returns {{server: string, log: string[]}} the server to use, and why
|
|
632
|
+
*/
|
|
633
|
+
chooseServer(fhirVersion, systems, defaultServer, valueSet = '', usage = '', language = '') {
|
|
634
|
+
if (!fhirVersion) {
|
|
635
|
+
throw new Error('A FHIR version is required');
|
|
636
|
+
}
|
|
637
|
+
if (!defaultServer) {
|
|
638
|
+
throw new Error('A default server is required');
|
|
639
|
+
}
|
|
640
|
+
const log = [];
|
|
641
|
+
const list = [...new Set((systems || []).filter(s => s))];
|
|
642
|
+
if (valueSet) {
|
|
643
|
+
log.push(`Value set: ${valueSet}`);
|
|
644
|
+
}
|
|
645
|
+
if (list.length === 0) {
|
|
646
|
+
log.push('No code systems: using the default server');
|
|
647
|
+
return { server: defaultServer, log };
|
|
648
|
+
}
|
|
649
|
+
const server = this._chooseServer(fhirVersion, list, defaultServer, valueSet, usage, language, log);
|
|
650
|
+
return { server, log };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
_chooseServer(fhirVersion, systems, defaultServer, valueSet, usage, language, log) {
|
|
654
|
+
// what the registry says about each system on its own
|
|
655
|
+
const choices = systems.map(system => {
|
|
656
|
+
const r = this.resolveCodeSystem(fhirVersion, system, false, usage, language).result;
|
|
657
|
+
const choice = { system, authoritative: [], candidates: [], content: {} };
|
|
658
|
+
for (const e of r.authoritative || []) {
|
|
659
|
+
if (!choice.authoritative.includes(e.url)) choice.authoritative.push(e.url);
|
|
660
|
+
}
|
|
661
|
+
for (const e of r.candidates || []) {
|
|
662
|
+
if (!choice.candidates.includes(e.url)) {
|
|
663
|
+
choice.candidates.push(e.url);
|
|
664
|
+
choice.content[e.url] = e.content;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
log.push(`${system}: authoritative [${choice.authoritative.join(', ')}], candidates [${choice.candidates.join(', ')}]`);
|
|
668
|
+
return choice;
|
|
669
|
+
});
|
|
670
|
+
// the registry knows no server at all for the system
|
|
671
|
+
const unresolved = c => c.authoritative.length === 0 && c.candidates.length === 0;
|
|
672
|
+
|
|
673
|
+
// 1. a server that's authoritative for all of them
|
|
674
|
+
for (const c of choices) {
|
|
675
|
+
for (const s of c.authoritative) {
|
|
676
|
+
if (choices.every(t => t.authoritative.includes(s))) {
|
|
677
|
+
log.push(`Found authoritative server ${s}`);
|
|
678
|
+
return s;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 2. a server that's authoritative for one of them, and a candidate for the others. A
|
|
684
|
+
// system that no server is known for doesn't constrain the choice
|
|
685
|
+
for (const c of choices) {
|
|
686
|
+
for (const s of c.authoritative) {
|
|
687
|
+
if (choices.every(t => unresolved(t) || t.authoritative.includes(s) || t.candidates.includes(s))) {
|
|
688
|
+
log.push(`Found partially authoritative server ${s}`);
|
|
689
|
+
return s;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 3. code systems with special handling on tx.fhir.org, that might not be resolved above.
|
|
695
|
+
// We don't want them going to secondary servers by accident
|
|
696
|
+
const bases = systems.map(s => s.includes('|') ? s.substring(0, s.indexOf('|')) : s);
|
|
697
|
+
if (systems.length > 1 && bases.includes(SNOMED_CT)) {
|
|
698
|
+
log.push(`Several code systems including SNOMED CT: choosing for SNOMED CT alone`);
|
|
699
|
+
return this._chooseServer(fhirVersion, [SNOMED_CT], defaultServer, valueSet, usage, language, log);
|
|
700
|
+
}
|
|
701
|
+
const special = bases.find(b => PRIMARY_SERVER_SYSTEMS.includes(b));
|
|
702
|
+
if (special) {
|
|
703
|
+
log.push(`Use primary server (the default) for ${special}`);
|
|
704
|
+
return defaultServer;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 4. a server that's a candidate for all of them (again, systems no server is known for
|
|
708
|
+
// don't constrain the choice), and that has a usable copy of the code systems
|
|
709
|
+
for (const c of choices) {
|
|
710
|
+
for (const s of c.candidates) {
|
|
711
|
+
if (choices.every(t => unresolved(t) || t.candidates.includes(s)) &&
|
|
712
|
+
this._supportsAll(s, choices, unresolved, log)) {
|
|
713
|
+
log.push(`Found candidate server ${s}`);
|
|
714
|
+
return s;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 5. no agreement - take the one that is most authoritative
|
|
720
|
+
const counts = new Map();
|
|
721
|
+
for (const c of choices) {
|
|
722
|
+
for (const s of c.authoritative) {
|
|
723
|
+
counts.set(s, (counts.get(s) || 0) + 1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
let max = null;
|
|
727
|
+
for (const [s, n] of counts) {
|
|
728
|
+
if (max === null || n > counts.get(max)) max = s;
|
|
729
|
+
}
|
|
730
|
+
if (max !== null) {
|
|
731
|
+
log.push(`Found most authoritative server ${max}`);
|
|
732
|
+
return max;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// 6. nothing. Use the server the value set comes from, if it comes from one, else the default
|
|
736
|
+
const why = systems.length === 1 ? 'Not handled by any servers' : 'Handled by multiple servers';
|
|
737
|
+
if (valueSet) {
|
|
738
|
+
const r = this.resolveValueSet(fhirVersion, valueSet, false, usage).result;
|
|
739
|
+
let source = (r.authoritative || []).concat(r.candidates || []).map(e => e.url)[0];
|
|
740
|
+
if (source === 'https://vsac.nlm.nih.gov') {
|
|
741
|
+
source = defaultServer;
|
|
742
|
+
}
|
|
743
|
+
if (source) {
|
|
744
|
+
log.push(`${why}. Using source @ '${source}'`);
|
|
745
|
+
return source;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
log.push(`${why}. Using the default server`);
|
|
749
|
+
return defaultServer;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Whether a candidate server has a usable copy (complete or fragment) of each code system
|
|
754
|
+
* it's a candidate for. Systems for which tx.fhir.org is a candidate aren't checked
|
|
755
|
+
*/
|
|
756
|
+
_supportsAll(server, choices, unresolved, log) {
|
|
757
|
+
if (isTxFhirOrg(server)) {
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
for (const t of choices) {
|
|
761
|
+
if (unresolved(t) || !t.candidates.includes(server) || t.candidates.some(isTxFhirOrg)) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const content = t.content[server];
|
|
765
|
+
if (content && content !== 'complete' && content !== 'fragment') {
|
|
766
|
+
log.push(`Candidate server ${server} dropped for ${t.system}: its copy of the code system is '${content}'`);
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
595
773
|
/**
|
|
596
774
|
* The ecosystem Discovery API (GET {root} as JSON): one row per server endpoint.
|
|
597
775
|
*
|
|
@@ -774,7 +952,7 @@ class RegistryAPI {
|
|
|
774
952
|
}
|
|
775
953
|
html += '</td>\n';
|
|
776
954
|
|
|
777
|
-
html += `<td>${row.security}
|
|
955
|
+
html += `<td>${escape(row.security)}</td>\n`;
|
|
778
956
|
|
|
779
957
|
html += '</tr>\n';
|
|
780
958
|
}
|
package/registry/crawler.js
CHANGED
|
@@ -160,6 +160,8 @@ class RegistryCrawler {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
newData.doco = masterJson.documentation || '';
|
|
163
|
+
newData.registration = masterJson.registration || '';
|
|
164
|
+
newData.ecosystemDoco = masterJson['ecosystem-documentation'] || '';
|
|
163
165
|
|
|
164
166
|
// Process each registry
|
|
165
167
|
const registries = masterJson.registries || [];
|
|
@@ -15,12 +15,13 @@ contract:
|
|
|
15
15
|
Every response carries `Link: </tx-reg/openapi.json>; rel="service-desc"` (RFC 8631), and the
|
|
16
16
|
HTML pages the matching `<link>` element.
|
|
17
17
|
|
|
18
|
-
There are two operations,
|
|
18
|
+
There are two operations from the ecosystem IG, and one extension, all `GET`:
|
|
19
19
|
|
|
20
20
|
| Endpoint | |
|
|
21
21
|
|---|---|
|
|
22
22
|
| `/tx-reg/` | **Discovery** - the server endpoints in the ecosystem |
|
|
23
23
|
| `/tx-reg/resolve` | **Resolution** - which endpoints to use for a code system or value set |
|
|
24
|
+
| `/tx-reg/choose-server` | **Server choice** - which one server to use for a request involving several code systems (JSON only; [below](#server-choice-get-tx-regchoose-server)) |
|
|
24
25
|
|
|
25
26
|
Both answer JSON, or an HTML page when the request's `Accept` header contains `text/html`.
|
|
26
27
|
`/tx-reg/resolve` in a browser without parameters is a form for trying it. Parameters are read
|
|
@@ -143,7 +144,72 @@ when empty, so an empty object means nobody hosts it.
|
|
|
143
144
|
* Resolve takes `version` as an alternative to `url|version`.
|
|
144
145
|
* Resolve omits `authoritative` and `candidates` when they are empty.
|
|
145
146
|
|
|
146
|
-
##
|
|
147
|
+
## Server choice: `GET /tx-reg/choose-server`
|
|
148
|
+
|
|
149
|
+
Not in the ecosystem IG. A request often involves several code systems - a value set that
|
|
150
|
+
includes LOINC and a national code system, a CodeableConcept with codings from three - and
|
|
151
|
+
resolution answers for one at a time. The HL7 Java tooling combines the answers into a choice
|
|
152
|
+
of one server (`TerminologyClientManager.chooseServer` in org.hl7.fhir.core); this makes the
|
|
153
|
+
same choice here for clients that aren't Java. It doesn't replace the Java code, which still
|
|
154
|
+
does its own choosing.
|
|
155
|
+
|
|
156
|
+
| Parameter | |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `fhirVersion` | Required, as for resolve |
|
|
159
|
+
| `system` | A code system the request involves, `url` or `url\|version`. Repeat it for each (`system=a&system=b`) - the one parameter that isn't first-value-only |
|
|
160
|
+
| `default` | Required. The client's default terminology server: the fallback, and the server for the code systems tx.fhir.org handles specially |
|
|
161
|
+
| `valueSet` | The value set the request is about, `url` or `url\|version`, if there is one |
|
|
162
|
+
| `language` | As for resolve, but only for language sensitive requests (an expansion where the display language matters, a validation that checks a display, a lookup that returns designations) |
|
|
163
|
+
| `usage` | As for resolve |
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"server": "https://tx.ontoserver.csiro.au/fhir",
|
|
168
|
+
"log": [
|
|
169
|
+
"https://healthterminologies.gov.au/fhir/CodeSystem/x: authoritative [https://tx.ontoserver.csiro.au/fhir], candidates []",
|
|
170
|
+
"http://terminology.hl7.org/CodeSystem/v2-0203: authoritative [http://tx.fhir.org/r4], candidates [https://tx.ontoserver.csiro.au/fhir]",
|
|
171
|
+
"Found partially authoritative server https://tx.ontoserver.csiro.au/fhir"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`log` is for people: what resolution says about each code system, then the rule that decided.
|
|
177
|
+
|
|
178
|
+
### What the client does first
|
|
179
|
+
|
|
180
|
+
What the Java tooling works out before it chooses, a client must too:
|
|
181
|
+
|
|
182
|
+
* The code systems: from the codings, and from the value set's includes and excludes,
|
|
183
|
+
following imported value sets. The imports are often only in the client's own context (the
|
|
184
|
+
IG being built), so the registry can't follow them. If the client can't follow one, it
|
|
185
|
+
should use its default server without asking.
|
|
186
|
+
* Versions: those fixed by the expansion parameters (`system-version`,
|
|
187
|
+
`force-system-version`) applied to the systems.
|
|
188
|
+
* Language: for an expansion, the `displayLanguage` of the expansion parameters if no other.
|
|
189
|
+
|
|
190
|
+
### How the choice is made
|
|
191
|
+
|
|
192
|
+
In order:
|
|
193
|
+
|
|
194
|
+
1. A server authoritative for all the code systems.
|
|
195
|
+
2. A server authoritative for one, and authoritative or a candidate for each of the others.
|
|
196
|
+
3. Several code systems including SNOMED CT: the choice for SNOMED CT alone. Otherwise, if a
|
|
197
|
+
code system is one tx.fhir.org handles specially (LOINC, UCUM, RxNorm, CVX, NDC, CPT,
|
|
198
|
+
BCP-13/47, ISO 3166/4217 ...): the default.
|
|
199
|
+
4. A server that is a candidate for all of them, and has a usable copy of each - its
|
|
200
|
+
TerminologyCapabilities says `complete` or `fragment`, or gives no content mode. Not
|
|
201
|
+
checked for a code system tx.fhir.org is also a candidate for.
|
|
202
|
+
5. The server authoritative for the most of them (the first, on a tie).
|
|
203
|
+
6. The server the value set comes from (by resolving `valueSet`), else the default.
|
|
204
|
+
|
|
205
|
+
In 2 and 4, a code system that no server hosts doesn't count against a server.
|
|
206
|
+
|
|
207
|
+
Where this differs from the Java tooling: step 4 uses the content mode the registry scanned
|
|
208
|
+
instead of searching the server, and trusts a server that didn't say; the special handling in
|
|
209
|
+
step 3 sends to `default` rather than to whichever of tx.fhir.org / tx-dev the client has used;
|
|
210
|
+
ties go to the first rather than to hash order. Step 3 is tx.fhir.org specific, and applies to
|
|
211
|
+
any ecosystem for now.
|
|
212
|
+
|
|
147
213
|
|
|
148
214
|
These are HTML, not part of the API, and never secured:
|
|
149
215
|
|
|
@@ -28,7 +28,10 @@ How the module works, for anyone changing it. The user-facing behaviour is in
|
|
|
28
28
|
`RegistryCrawler.crawl()` runs every `crawlInterval` minutes (first 5 seconds after start),
|
|
29
29
|
and never concurrently with itself. A scan builds a fresh `ServerRegistries` from scratch:
|
|
30
30
|
|
|
31
|
-
1. fetch the master registration file (`formatVersion` must be `"1"`);
|
|
31
|
+
1. fetch the master registration file (`formatVersion` must be `"1"`); its optional
|
|
32
|
+
`registration` and `ecosystem-documentation` URLs are the "Register your own server" and
|
|
33
|
+
"Documentation" links on the home page (each is omitted if the file doesn't give it), since
|
|
34
|
+
they depend on the ecosystem;
|
|
32
35
|
2. for each registry it lists, fetch the registry file and build a `ServerRegistry`;
|
|
33
36
|
3. for each server, a `ServerInformation` with its claims (`authoritative`,
|
|
34
37
|
`authoritative-valuesets`, `languages`, `usage`, `exclusions`), and for each of its
|
|
@@ -27,8 +27,9 @@ it, [api.md](api.md) says so.
|
|
|
27
27
|
|
|
28
28
|
**The registration files.** The ecosystem is declared by a *master registration file* that
|
|
29
29
|
lists *registries*, each of which lists *servers*, each of which lists its endpoints (one per
|
|
30
|
-
FHIR version) and its claims. The format is documented
|
|
31
|
-
|
|
30
|
+
FHIR version) and its claims. The format is documented on the IG's ecosystem page; HL7's own
|
|
31
|
+
ecosystem (its locations, and how to register a server in it) is documented in the ig-registry
|
|
32
|
+
repository's [tx-registry-doco.md](https://github.com/FHIR/ig-registry/blob/master/tx-registry-doco.md).
|
|
32
33
|
HL7's master file is https://fhir.github.io/ig-registry/tx-servers.json, and its registries
|
|
33
34
|
are the per-affiliate `*-tx-servers.json` files in the same repository (HL7 International,
|
|
34
35
|
HL7 Australia, HL7 Europe, and so on). A server's registration carries:
|
package/registry/model.js
CHANGED
|
@@ -362,6 +362,8 @@ class ServerRegistries {
|
|
|
362
362
|
constructor() {
|
|
363
363
|
this.address = '';
|
|
364
364
|
this.doco = '';
|
|
365
|
+
this.registration = ''; // URL: how to register your own server in this ecosystem
|
|
366
|
+
this.ecosystemDoco = ''; // URL: documentation for this ecosystem
|
|
365
367
|
this.lastRun = null; // Date object
|
|
366
368
|
this.outcome = '';
|
|
367
369
|
this.registries = []; // Array of ServerRegistry
|
|
@@ -386,6 +388,8 @@ class ServerRegistries {
|
|
|
386
388
|
this.lastRun = source.lastRun;
|
|
387
389
|
this.outcome = source.outcome;
|
|
388
390
|
this.doco = source.doco;
|
|
391
|
+
this.registration = source.registration;
|
|
392
|
+
this.ecosystemDoco = source.ecosystemDoco;
|
|
389
393
|
|
|
390
394
|
source.registries.forEach(sourceRegistry => {
|
|
391
395
|
const existing = this.getRegistry(sourceRegistry.code);
|
|
@@ -402,6 +406,8 @@ class ServerRegistries {
|
|
|
402
406
|
version: '1',
|
|
403
407
|
address: this.address,
|
|
404
408
|
doco: this.doco,
|
|
409
|
+
registration: this.registration,
|
|
410
|
+
'ecosystem-documentation': this.ecosystemDoco,
|
|
405
411
|
'last-run': this.lastRun?.toISOString() || '',
|
|
406
412
|
outcome: this.outcome,
|
|
407
413
|
registries: this.registries.map(r => r.toJSON())
|
|
@@ -416,6 +422,8 @@ class ServerRegistries {
|
|
|
416
422
|
const instance = new ServerRegistries();
|
|
417
423
|
instance.address = json.address || '';
|
|
418
424
|
instance.doco = json.doco || '';
|
|
425
|
+
instance.registration = json.registration || '';
|
|
426
|
+
instance.ecosystemDoco = json['ecosystem-documentation'] || '';
|
|
419
427
|
instance.lastRun = json['last-run'] ? new Date(json['last-run']) : null;
|
|
420
428
|
instance.outcome = json.outcome || '';
|
|
421
429
|
instance.registries = (json.registries || []).map(r => ServerRegistry.fromJSON(r));
|
package/registry/openapi.yaml
CHANGED
|
@@ -25,6 +25,9 @@ info:
|
|
|
25
25
|
* **Resolution** (`GET /resolve`): which server should be used for this code system
|
|
26
26
|
or value set?
|
|
27
27
|
|
|
28
|
+
It also offers **server choice** (`GET /choose-server`): given all the code systems a
|
|
29
|
+
request involves, and the client's default server, which one server should it go to?
|
|
30
|
+
|
|
28
31
|
Servers can claim to be **authoritative** for code systems and value sets (by URL
|
|
29
32
|
mask) in their registration. A server that hosts the content but makes no claim is a
|
|
30
33
|
**candidate**. A server is only ever returned for content it actually hosts.
|
|
@@ -182,6 +185,77 @@ paths:
|
|
|
182
185
|
schema: { $ref: '#/components/schemas/Error' }
|
|
183
186
|
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
184
187
|
|
|
188
|
+
/choose-server:
|
|
189
|
+
get:
|
|
190
|
+
tags: [resolution]
|
|
191
|
+
operationId: chooseServer
|
|
192
|
+
summary: Choose one server for a request involving several code systems
|
|
193
|
+
description: |
|
|
194
|
+
For clients that aren't the HL7 Java tooling: the same choice the Java tooling makes
|
|
195
|
+
(TerminologyClientManager.chooseServer), made from the registry's own data. Returns
|
|
196
|
+
the server to send the request to, and a log of how it was chosen.
|
|
197
|
+
|
|
198
|
+
The client works out the code systems the request involves - from the codings, and
|
|
199
|
+
the value set's includes and excludes, following imported value sets - and applies
|
|
200
|
+
any versions fixed by expansion parameters. If it can't follow an import, it should
|
|
201
|
+
use its default server without asking.
|
|
202
|
+
|
|
203
|
+
In order, the choice is:
|
|
204
|
+
|
|
205
|
+
1. a server authoritative for all the code systems
|
|
206
|
+
2. a server authoritative for one, and authoritative or a candidate for the others
|
|
207
|
+
3. for several code systems including SNOMED CT, the choice for SNOMED CT alone; for
|
|
208
|
+
a code system tx.fhir.org handles specially (LOINC, UCUM, RxNorm ...), the default
|
|
209
|
+
4. a server that is a candidate for all of them, and reports a usable (complete or
|
|
210
|
+
fragment) copy of each, or no content mode at all
|
|
211
|
+
5. the server authoritative for the most of them
|
|
212
|
+
6. the server the value set comes from (resolved by `valueSet`), else the default
|
|
213
|
+
|
|
214
|
+
In 2 and 4, a code system that no server hosts doesn't count against a server.
|
|
215
|
+
parameters:
|
|
216
|
+
- name: fhirVersion
|
|
217
|
+
in: query
|
|
218
|
+
required: true
|
|
219
|
+
description: The FHIR version, as a release code (`R4`, `R4B`, `R5` ...) or a version (`4.0.1`, `4.0`).
|
|
220
|
+
schema: { type: string }
|
|
221
|
+
example: R4
|
|
222
|
+
- name: system
|
|
223
|
+
in: query
|
|
224
|
+
description: |
|
|
225
|
+
A code system the request involves, `url` or `url|version`. Repeat for each one
|
|
226
|
+
(`system=a&system=b`). With none, the answer is the default server.
|
|
227
|
+
schema: { type: array, items: { type: string } }
|
|
228
|
+
style: form
|
|
229
|
+
explode: true
|
|
230
|
+
example: [http://loinc.org, http://snomed.info/sct|http://snomed.info/sct/32506021000036107]
|
|
231
|
+
- name: default
|
|
232
|
+
in: query
|
|
233
|
+
required: true
|
|
234
|
+
description: The client's default terminology server (the one it would use without the ecosystem).
|
|
235
|
+
schema: { type: string, format: uri }
|
|
236
|
+
example: http://tx.fhir.org/r4
|
|
237
|
+
- name: valueSet
|
|
238
|
+
in: query
|
|
239
|
+
description: The value set the request is about, `url` or `url|version`, if there is one. Used for the log, and in step 6.
|
|
240
|
+
schema: { type: string }
|
|
241
|
+
- $ref: '#/components/parameters/language'
|
|
242
|
+
- name: usage
|
|
243
|
+
in: query
|
|
244
|
+
description: What the client is doing, for servers that restrict their use (as for `/resolve`).
|
|
245
|
+
schema: { type: string }
|
|
246
|
+
responses:
|
|
247
|
+
'200':
|
|
248
|
+
description: The chosen server
|
|
249
|
+
content:
|
|
250
|
+
application/json:
|
|
251
|
+
schema: { $ref: '#/components/schemas/ChooseServerResult' }
|
|
252
|
+
'400':
|
|
253
|
+
description: A required parameter is missing, or a URL is malformed
|
|
254
|
+
content:
|
|
255
|
+
application/json:
|
|
256
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
257
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
258
|
+
|
|
185
259
|
components:
|
|
186
260
|
securitySchemes:
|
|
187
261
|
bearer:
|
|
@@ -311,6 +385,16 @@ components:
|
|
|
311
385
|
type: array
|
|
312
386
|
items: { $ref: '#/components/schemas/ResolvedServer' }
|
|
313
387
|
|
|
388
|
+
ChooseServerResult:
|
|
389
|
+
type: object
|
|
390
|
+
required: [server, log]
|
|
391
|
+
properties:
|
|
392
|
+
server: { type: string, format: uri, description: The FHIR endpoint to send the request to }
|
|
393
|
+
log:
|
|
394
|
+
type: array
|
|
395
|
+
items: { type: string }
|
|
396
|
+
description: How the server was chosen - what the registry says about each code system, then the rule that decided. For people, not for parsing
|
|
397
|
+
|
|
314
398
|
ResolvedServer:
|
|
315
399
|
allOf:
|
|
316
400
|
- $ref: '#/components/schemas/SecurityFlags'
|