fhirsmith 0.18.1 → 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/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'
@@ -20,6 +20,7 @@ const { RegistryAuth } = require('./auth');
20
20
  // parameters are ignored) because existing ecosystem clients rely on that.
21
21
  const DISCOVERY_PARAMS = ['registry', 'server', 'fhirVersion', 'url', 'authoritativeOnly', 'language'];
22
22
  const RESOLVE_PARAMS = ['fhirVersion', 'url', 'version', 'valueSet', 'authoritativeOnly', 'language', 'usage'];
23
+ const CHOOSE_SERVER_PARAMS = ['fhirVersion', 'system', 'default', 'valueSet', 'usage', 'language'];
23
24
 
24
25
  // A link to a URL from a registry list or a crawled server: those are other people's
25
26
  // content, so only an http(s) URL is linked, and it is escaped
@@ -325,6 +326,8 @@ class RegistryModule {
325
326
  // Main registry page
326
327
  this.router.get('/', apiLimiter, apiGuard, session, this.handleMainPage.bind(this));
327
328
  this.router.get('/resolve', apiLimiter, apiGuard, session, this.handleResolveEndpoint.bind(this));
329
+ // JSON only - there's no page for a browser, so it's always guarded
330
+ this.router.get('/choose-server', apiLimiter, guard, this.handleChooseServerEndpoint.bind(this));
328
331
  this.router.get('/log', session, this.handleLogEndpoint.bind(this));
329
332
  this.router.get('/software', session, this.handleSoftwarePage.bind(this));
330
333
 
@@ -807,8 +810,15 @@ class RegistryModule {
807
810
  const lastRunDate = new Date(stats.lastRun);
808
811
  lastUpdatedText = `Last Updated: ${lastRunDate.toLocaleString()}`;
809
812
  }
810
- html += `<p>${lastUpdatedText}. <a href="https://github.com/FHIR/ig-registry/blob/master/tx-registry-doco.md">Register your own server</a>`+
811
- ` - see <a href="https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig/ecosystem.html">Documentation</a></p>`;
813
+ // what goes here depends on the ecosystem, so it comes from the master registration file
814
+ html += `<p>${lastUpdatedText}.`;
815
+ if (data.registration) {
816
+ html += ` <a href="${safeHref(data.registration)}">Register your own server</a>`;
817
+ }
818
+ if (data.ecosystemDoco) {
819
+ html += `${data.registration ? ' -' : ''} see <a href="${safeHref(data.ecosystemDoco)}">Documentation</a>`;
820
+ }
821
+ html += '</p>';
812
822
 
813
823
  html += '<table class="grid">';
814
824
  html += '<thead><tr>';
@@ -1503,6 +1513,46 @@ class RegistryModule {
1503
1513
  }
1504
1514
  }
1505
1515
 
1516
+ /**
1517
+ * GET /choose-server: which one server to use for a request involving a set of code
1518
+ * systems (see RegistryAPI.chooseServer). system repeats; the other parameters take their
1519
+ * first value
1520
+ */
1521
+ handleChooseServerEndpoint(req, res) {
1522
+ const start = Date.now();
1523
+ try {
1524
+ const params = this._normalizeQueryParams(req.query);
1525
+ const {fhirVersion, valueSet, usage, language} = params;
1526
+ const defaultServer = params.default;
1527
+ const raw = req.query.system;
1528
+ const systems = (raw == null ? [] : Array.isArray(raw) ? raw : [raw]).map(String).filter(s => s);
1529
+
1530
+ if (!fhirVersion) {
1531
+ return res.status(400).json({error: 'A FHIR version is required'});
1532
+ }
1533
+ if (!defaultServer || !this._isValidUrl(defaultServer, ['http:', 'https:'])) {
1534
+ return res.status(400).json({error: 'A default server (an http or https URL) is required'});
1535
+ }
1536
+ for (const system of systems) {
1537
+ if (!this._isValidUrl(system.split('|')[0])) {
1538
+ return res.status(400).json({error: 'Invalid code system URL format'});
1539
+ }
1540
+ }
1541
+ if (valueSet && !this._isValidUrl(valueSet.split('|')[0])) {
1542
+ return res.status(400).json({error: 'Invalid value set URL format'});
1543
+ }
1544
+
1545
+ const result = this.api.chooseServer(fhirVersion, systems, defaultServer, valueSet, usage, language);
1546
+ this.logger.info(`Chose ${result.server} for ${systems.join(', ') || '(no systems)'} (FHIR ${fhirVersion}, usage=${usage}, language=${language || 'none'})`);
1547
+ res.json(result);
1548
+ } catch (error) {
1549
+ this.logger.error('Error in choose-server endpoint:', error);
1550
+ res.status(400).json({error: error.message});
1551
+ } finally {
1552
+ this.stats.countRequest('choose-server', Date.now() - start);
1553
+ }
1554
+ }
1555
+
1506
1556
  buildResolveResultContent(result, fhirVersion, resourceUrl, usage, language) {
1507
1557
  let html = '';
1508
1558
 
@@ -1833,6 +1883,7 @@ class RegistryModule {
1833
1883
 
1834
1884
  RegistryModule.DISCOVERY_PARAMS = DISCOVERY_PARAMS;
1835
1885
  RegistryModule.RESOLVE_PARAMS = RESOLVE_PARAMS;
1886
+ RegistryModule.CHOOSE_SERVER_PARAMS = CHOOSE_SERVER_PARAMS;
1836
1887
 
1837
1888
  module.exports = RegistryModule;
1838
1889
  module.exports.commonBaseUrl = commonBaseUrl;
package/tx/README.md CHANGED
@@ -312,6 +312,10 @@ THO's v3-ActCode code system is `CodeSystem/tho-v3-ActCode`. No resource is serv
312
312
  - `core` is the FHIR core package each endpoint loads for its FHIR version (`CodeSystem/core-administrative-gender`), and `x` is the code systems the server implements natively (`CodeSystem/x-...`); neither can be given as a prefix
313
313
  - Sources that only provide code systems natively (`internal`, `snomed`, `loinc` etc.) don't serve resources by id, so a prefix does nothing for them
314
314
 
315
+ #### hl7.terminology and the FHIR core packages
316
+
317
+ Much of the content of the FHIR core packages has moved to terminology.hl7.org (`hl7.terminology`), and its versions were reset when it moved (`coverage-selfpay` is 4.0.1 in `hl7.fhir.r4.core`, and 1.0.1 in `hl7.terminology`), so version order can't decide between the two. Where `hl7.terminology` provides a code system, value set or concept map with the same url as the endpoint's core package, THO's is always the one a reference with no version gets. The core package's versions are still available to a reference that names them (`http://hl7.org/fhir/concept-property-type|5.0.0`), unless THO provides that same version itself. Core value sets and concept maps that THO also provides are not read by id, found by search, or used for `$translate` unless asked for by version.
318
+
315
319
  ### Source Types
316
320
 
317
321
  #### `internal` - Built-in Code Systems
@@ -8,6 +8,7 @@ const { VersionUtilities } = require('../../library/version-utilities');
8
8
  const {validateParameter} = require("../../library/utilities");
9
9
  const {ConceptMap} = require("../library/conceptmap");
10
10
  const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
11
+ const { keepForPinnedReference } = require('../library/tho-precedence');
11
12
 
12
13
  /**
13
14
  * Package-based ConceptMap provider using shared database layer
@@ -169,7 +170,7 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
169
170
 
170
171
  // Only process each ConceptMap once (use URL to deduplicate)
171
172
  const vsUrl = json.url;
172
- if (seen.has(vsUrl)) {
173
+ if (seen.has(vsUrl) || this.#pinnedOnly.has(vs)) {
173
174
  continue;
174
175
  }
175
176
 
@@ -330,35 +331,39 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
330
331
  return this.packageLoader.id();
331
332
  }
332
333
 
333
- // the concept maps left out by excludeUrls, by their own (unprefixed) id
334
+ // the concept maps that keepPinnedOnly restricted, by their own (unprefixed) id, and themselves
334
335
  #excludedIds = new Set();
336
+ #pinnedOnly = new Set();
335
337
 
336
338
  /**
337
- * Leave out the concept maps with any of these urls: they can't be fetched, they aren't
338
- * found by search, and they aren't used for translation. The FHIR core package's concept
339
- * maps are left out where hl7.terminology provides the same url (see
340
- * Provider.applyTHOPrecedence). Must be called once the provider is initialized - the
341
- * database is shared, so it isn't changed.
339
+ * The concept maps with these urls are hl7.terminology's to provide (see
340
+ * Provider.applyTHOPrecedence): from this (core) package they are only available to a
341
+ * reference that names their version, where THO doesn't provide that version itself. They
342
+ * aren't the default for their url, can't be read by id, aren't found by search, and aren't
343
+ * used for translation unless asked for. Must be called once the provider is initialized -
344
+ * the database is shared, so it isn't changed.
342
345
  *
343
- * @param {Set<string>} urls
346
+ * @param {Map<string, Set<string>>} thoVersions - url -> the versions hl7.terminology provides
344
347
  */
345
- excludeUrls(urls) {
346
- const excluded = new Set();
348
+ keepPinnedOnly(thoVersions) {
349
+ const affected = new Set();
347
350
  for (const cm of this.conceptMapMap.values()) {
348
- if (cm && urls.has(cm.url)) {
349
- excluded.add(cm);
351
+ if (cm && thoVersions.has(cm.url)) {
352
+ affected.add(cm);
350
353
  }
351
354
  }
352
355
  for (const [key, cm] of [...this.conceptMapMap]) {
353
- if (excluded.has(cm)) {
356
+ if (affected.has(cm) && !keepForPinnedReference(key, cm, thoVersions.get(cm.url))) {
354
357
  this.conceptMapMap.delete(key);
355
358
  }
356
359
  }
357
- for (const cm of excluded) {
360
+ for (const cm of affected) {
361
+ this.#pinnedOnly.add(cm);
358
362
  this.#excludedIds.add(stripIdPrefix(cm.id, this.spaceId) ?? cm.id);
359
363
  }
360
364
  }
361
365
 
366
+
362
367
  prefixIds() {
363
368
  this.#applyIdPrefix();
364
369
  }
@@ -371,7 +376,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
371
376
 
372
377
  async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem) {
373
378
  for (let cm of this.conceptMapMap.values()) {
374
- if (cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
379
+ // a concept map that THO also provides is only used when it's asked for by version
380
+ if (!this.#pinnedOnly.has(cm) && cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
375
381
  conceptMaps.push(cm);
376
382
  }
377
383
  }