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.
Files changed (47) hide show
  1. package/CHANGELOG.md +72 -10
  2. package/ips/ips.js +251 -26
  3. package/ips/jurisdictions.js +65 -0
  4. package/ips/pipeline.js +67 -6
  5. package/ips/profile-editor.js +283 -4
  6. package/ips/readme.md +48 -5
  7. package/ips/render.js +317 -46
  8. package/ips/scripts.js +46 -0
  9. package/ips/store.js +55 -0
  10. package/library/daily-report.js +25 -15
  11. package/package.json +3 -3
  12. package/registry/api.js +179 -1
  13. package/registry/crawler.js +2 -0
  14. package/registry/documentation/api.md +68 -2
  15. package/registry/documentation/internals.md +4 -1
  16. package/registry/documentation/readme.md +3 -2
  17. package/registry/model.js +8 -0
  18. package/registry/openapi.yaml +84 -0
  19. package/registry/registry.js +53 -2
  20. package/translations/Messages.properties +1 -0
  21. package/tx/README.md +4 -0
  22. package/tx/closure/closure-store.js +14 -0
  23. package/tx/cm/cm-package.js +21 -15
  24. package/tx/cs/cs-api.js +3 -1
  25. package/tx/cs/cs-cs.js +59 -12
  26. package/tx/cs/cs-icf.js +47 -34
  27. package/tx/cs/cs-loinc.js +257 -3
  28. package/tx/cs/cs-snomed.js +544 -55
  29. package/tx/importers/csv-records.js +35 -0
  30. package/tx/importers/import-loinc.module.js +177 -41
  31. package/tx/importers/import-sct.module.js +131 -1
  32. package/tx/importers/subset-loinc.module.js +25 -6
  33. package/tx/library/codesystem.js +4 -1
  34. package/tx/library/designations.js +23 -30
  35. package/tx/library/tho-precedence.js +40 -0
  36. package/tx/openapi-operations.js +2 -1
  37. package/tx/provider.js +46 -29
  38. package/tx/sct/documentation.md +45 -21
  39. package/tx/sct/ecl.js +34 -11
  40. package/tx/sct/structures.js +15 -2
  41. package/tx/tx.fhir.org.yml +2 -2
  42. package/tx/vs/vs-package.js +17 -13
  43. package/tx/vs/vs-vsac.js +1 -1
  44. package/tx/workers/closure.js +20 -0
  45. package/tx/workers/expand.js +2 -2
  46. package/tx/workers/validate.js +10 -0
  47. package/utilities/make-sct-loinc-subset.js +250 -0
@@ -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
- const sentences = [];
351
- sentences.push(`${this.serverId} is running FHIRsmith v${this.version} on Node.js ${process.version} (${os.type()} ${os.release()}, ${os.arch()}).`);
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 has been up for ${formatSpan(now - this.stats.startTime)}, since ${localDateTime(this.stats.startTime)}`;
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 += `; over all its recorded runs (since ${localDate(overall.since)}) the server has been up ${pct}% of the time`;
366
+ up += `. All runs, ${pct}%`;
358
367
  }
359
- sentences.push(up + '.');
368
+ lines.push(up);
360
369
  }
361
- sentences.push(this.logsSentence());
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
- sentences.push(audit);
374
+ lines.push(`Audit: ${audit}`);
366
375
  }
367
376
  }
368
- return sentences.join(' ');
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
- logsSentence() {
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 = `, on a disk with ${formatBytes(free)} free of ${formatBytes(size)} (${freePct}% free)`;
414
+ disk = ` on a disk with ${formatBytes(free)} free of ${formatBytes(size)} (${freePct}% free)`;
406
415
  } catch {
407
- disk = ', on a disk whose free space could not be read';
416
+ disk = ' on a disk whose free space could not be read';
408
417
  }
409
- return `The logs folder (${logDir}) holds ${plural(files.length, 'log file')} totalling ${formatBytes(total)}${disk}.`;
418
+ return `${plural(files.length, 'log file')} totalling ${formatBytes(total)}${disk}.`;
410
419
  } catch (e) {
411
- return `The logs folder (${logDir}) could not be read: ${e.message}.`;
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.0",
4
- "txVersion": "1.9.6",
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.5.0",
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}/td>\n`;
955
+ html += `<td>${escape(row.security)}</td>\n`;
778
956
 
779
957
  html += '</tr>\n';
780
958
  }
@@ -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, both `GET`:
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
- ## Operational pages
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 in the ig-registry repository's
31
- [tx-registry-doco.md](https://github.com/FHIR/ig-registry/blob/master/tx-registry-doco.md).
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));
@@ -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'