fhirsmith 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +3 -2
  3. package/config-template.json +9 -0
  4. package/library/changelog.js +57 -0
  5. package/library/fhir-openapi-schema.js +441 -0
  6. package/library/html-server.js +9 -3
  7. package/library/npm-audit.js +265 -0
  8. package/library/openapi-doc.js +557 -0
  9. package/library/utilities.js +21 -0
  10. package/package.json +10 -4
  11. package/packages/openapi.js +13 -0
  12. package/packages/openapi.yaml +603 -0
  13. package/packages/packages-template.html +4 -2
  14. package/packages/packages.js +286 -81
  15. package/packages/readme.md +120 -0
  16. package/registry/api.js +117 -41
  17. package/registry/crawler.js +41 -0
  18. package/registry/fhirsmith-releases.js +167 -0
  19. package/registry/model.js +13 -0
  20. package/registry/openapi.js +13 -0
  21. package/registry/openapi.yaml +311 -0
  22. package/registry/readme.md +107 -208
  23. package/registry/registry-template.html +4 -1
  24. package/registry/registry.js +198 -19
  25. package/server.js +19 -5
  26. package/testing/openapi-schemas.config.js +58 -0
  27. package/testing/openapi-schemas.json +1920 -0
  28. package/testing/openapi.js +15 -0
  29. package/testing/openapi.yaml +273 -0
  30. package/testing/readme.md +43 -1
  31. package/testing/search.js +1 -1
  32. package/testing/testing-template.html +2 -0
  33. package/testing/testing.js +24 -0
  34. package/translations/Messages.properties +1 -0
  35. package/tx/README.md +78 -4
  36. package/tx/cm/cm-api.js +25 -10
  37. package/tx/cm/cm-database.js +10 -5
  38. package/tx/cm/cm-package.js +61 -15
  39. package/tx/cs/cs-provider-api.js +22 -7
  40. package/tx/cs/cs-provider-list.js +10 -9
  41. package/tx/cs/cs-rxnorm.js +23 -6
  42. package/tx/html/tx-template.html +2 -0
  43. package/tx/library/canonical-resource.js +50 -1
  44. package/tx/library/codesystem.js +1 -0
  45. package/tx/library/conceptmap.js +1 -0
  46. package/tx/library/resource-ids.js +107 -0
  47. package/tx/library/valueset.js +16 -0
  48. package/tx/library.js +113 -26
  49. package/tx/ocl/cm-ocl.cjs +23 -15
  50. package/tx/ocl/cs-ocl.cjs +9 -10
  51. package/tx/ocl/vs-ocl.cjs +17 -17
  52. package/tx/openapi-operations.js +363 -0
  53. package/tx/openapi-schemas.config.js +118 -0
  54. package/tx/openapi-schemas.json +7903 -0
  55. package/tx/openapi.js +289 -0
  56. package/tx/openapi.yaml +151 -0
  57. package/tx/operation-context.js +15 -0
  58. package/tx/params.js +3 -12
  59. package/tx/provider.js +90 -14
  60. package/tx/tx-html.js +6 -0
  61. package/tx/tx.fhir.org.yml +16 -8
  62. package/tx/tx.js +62 -1
  63. package/tx/vs/vs-api.js +25 -10
  64. package/tx/vs/vs-database.js +14 -9
  65. package/tx/vs/vs-package.js +58 -31
  66. package/tx/vs/vs-vsac.js +20 -5
  67. package/tx/workers/expand.js +35 -16
  68. package/tx/workers/metadata.js +2 -14
  69. package/tx/workers/read.js +3 -18
  70. package/tx/workers/validate.js +65 -19
  71. package/tx/workers/worker.js +29 -8
  72. package/utilities/extract-schema-json.js +167 -0
  73. package/utilities/generate-openapi-schemas.js +49 -0
  74. package/xig/xig.js +2 -4
@@ -1,6 +1,14 @@
1
1
  base:
2
2
  url: https://storage.googleapis.com/tx-fhir-org
3
3
 
4
+ # Each source is type[!]:details[=prefix] - see tx/README.md.
5
+ #
6
+ # The prefix (after the last '=') is the id space for the resources the source provides:
7
+ # with npm:hl7.terminology=tho, THO's v3-ActCode is CodeSystem/tho-v3-ActCode. Every
8
+ # resource is served with a prefixed id - never the id it has in its source, which other
9
+ # sources may share - and each endpoint's own FHIR core package uses 'core'. A source with
10
+ # no prefix gets a number, which changes when sources are added, removed or reordered, so
11
+ # any source whose resources people link to by id should have one.
4
12
  sources:
5
13
  - internal:lang
6
14
  - internal:country
@@ -29,14 +37,14 @@ sources:
29
37
  - snomed:sct_test_20250909b.cache
30
38
  - cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
31
39
  - omop:omop_v20250227.db
32
- - npm:hl7.terminology
33
- - npm:fhir.tx.support
34
- - npm:ihe.formatcode.fhir
35
- - npm:fhir.dicom
36
- - npm:hl7.fhir.us.core
37
- - npm:us.cdc.phinvads
38
- - npm:hl7.fhir.uv.sdc
39
- - internal:vsac
40
+ - npm:hl7.terminology=tho
41
+ - npm:fhir.tx.support=tx
42
+ - npm:ihe.formatcode.fhir=ihe
43
+ - npm:fhir.dicom=dicom
44
+ - npm:hl7.fhir.us.core=us
45
+ - npm:us.cdc.phinvads=cdc
46
+ - npm:hl7.fhir.uv.sdc=sdc
47
+ - internal:vsac=vsac
40
48
  - icd11:icd11-2026-01.db
41
49
 
42
50
  ignored:
package/tx/tx.js CHANGED
@@ -5,6 +5,10 @@
5
5
  // with support for multiple endpoints at different FHIR versions.
6
6
  //
7
7
 
8
+ const { reportedReleaseDate } = require('../library/changelog');
9
+
10
+ // when this server process started (the release date of a snapshot build)
11
+ const SERVER_STARTED = new Date(Date.now() - process.uptime() * 1000);
8
12
  const express = require('express');
9
13
  const path = require('path');
10
14
  const fs = require('fs');
@@ -54,6 +58,7 @@ const {BundleXML} = require("./xml/bundle-xml");
54
58
  const ConceptUsageTracker = require("./usage-tracker");
55
59
  const ProblemFinder = require("./problems");
56
60
  const { buildOperationOutcome, outcomeFromError } = require('./library/operation-outcome');
61
+ const txOpenApi = require('./openapi');
57
62
  // const {writeFileSync} = require("fs");
58
63
 
59
64
  class TXModule {
@@ -222,7 +227,9 @@ class TXModule {
222
227
  description: config.description || 'FHIR Terminology Server',
223
228
  contactUrl: config.contactUrl,
224
229
  contact: config.contact,
225
- releaseDate: config.releaseDate,
230
+ // when this version was released, from its CHANGELOG.md heading - or, for a snapshot,
231
+ // when the server started
232
+ releaseDate: config.releaseDate || reportedReleaseDate(packageJson.version, SERVER_STARTED),
226
233
  host: config.host ? config.host : "localhost"
227
234
  });
228
235
 
@@ -521,6 +528,11 @@ class TXModule {
521
528
 
522
529
  app.use(express.urlencoded({ extended: true }));
523
530
 
531
+ // The OpenAPI description, on the endpoints it describes (R5)
532
+ if (txOpenApi.describes(fhirVersion)) {
533
+ this.setupOpenApi(router, endpointInfo);
534
+ }
535
+
524
536
  // Set up routes
525
537
  this.setupRoutes(router, endpointInfo.path);
526
538
 
@@ -540,6 +552,55 @@ class TXModule {
540
552
  this.log.info(`Endpoint ${endpointPath} registered`);
541
553
  }
542
554
 
555
+ /**
556
+ * The OpenAPI description of an endpoint: /openapi.json, /openapi.yaml, and /openapi (an
557
+ * HTML reference for browsers, the JSON otherwise), and an RFC 8631 Link header on every
558
+ * response that points to them. These are sent directly, not through the res.json the
559
+ * endpoint wraps for FHIR resources.
560
+ *
561
+ * @param {express.Router} router
562
+ * @param {Object} endpointInfo
563
+ */
564
+ setupOpenApi(router, endpointInfo) {
565
+ const doc = txOpenApi.forEndpoint(endpointInfo.path);
566
+ const sendJson = (res) => res.type('application/json').send(doc.getJson());
567
+ router.use((req, res, next) => {
568
+ res.setHeader('Link', `<${req.baseUrl}/openapi.json>; rel="service-desc", <${req.baseUrl}/openapi>; rel="service-doc"`);
569
+ next();
570
+ });
571
+ const count = (start) => this.countRequest(endpointInfo.path, 'openapi', Date.now() - start);
572
+ router.get('/openapi.json', (req, res) => {
573
+ const start = Date.now();
574
+ try {
575
+ sendJson(res);
576
+ } finally {
577
+ count(start);
578
+ }
579
+ });
580
+ router.get('/openapi.yaml', (req, res) => {
581
+ const start = Date.now();
582
+ try {
583
+ res.type('application/yaml').send(doc.getYaml());
584
+ } finally {
585
+ count(start);
586
+ }
587
+ });
588
+ router.get('/openapi', async (req, res) => {
589
+ const start = Date.now();
590
+ try {
591
+ if (!txHtml.acceptsHtml(req)) {
592
+ return sendJson(res);
593
+ }
594
+ const txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, endpointInfo.path);
595
+ const html = await txhtml.renderPage('API', doc.renderHtml(), endpointInfo, req.txStartTime);
596
+ res.setHeader('Content-Type', 'text/html');
597
+ res.send(html);
598
+ } finally {
599
+ count(start);
600
+ }
601
+ });
602
+ }
603
+
543
604
  /**
544
605
  * Set up routes for an endpoint
545
606
  * @param {express.Router} router - Express router
package/tx/vs/vs-api.js CHANGED
@@ -1,10 +1,14 @@
1
+ const { assignIdSpace } = require('../library/resource-ids');
2
+
1
3
  /**
2
4
  * Abstract base class for value set providers
3
5
  * Defines the interface that all value set providers must implement
4
6
  */
5
7
  class AbstractValueSetProvider {
6
8
  /**
7
- * {int} Unique number assigned to this provider
9
+ * {string} The prefix of this provider's id space (set by assignIds) - given for its source in the library
10
+ * YAML, autonumbered, or 'core' for an endpoint's core package. Every value set this
11
+ * provider provides has an id that starts with spaceId + '-' (see tx/library/resource-ids.js)
8
12
  */
9
13
  spaceId;
10
14
 
@@ -12,14 +16,25 @@ class AbstractValueSetProvider {
12
16
  throw new Error('code must be implemented by AbstractValueSetProvider subclass');
13
17
  }
14
18
  /**
15
- * ensure that the ids on the value sets are unique, if they are
16
- * in the global namespace
19
+ * Put this provider in an id space: set spaceId, and prefix the ids of the value sets it
20
+ * provides with spaceId + '-'. A provider is put in one id space, once - asking for a
21
+ * different one later is an error (see assignIdSpace)
17
22
  *
18
- * @param {Set<String>} ids
23
+ * @param {string} spaceId
19
24
  */
20
- // eslint-disable-next-line no-unused-vars
21
- assignIds(ids) {
22
- throw new Error('assignIds must be implemented by AbstractValueSetProvider subclass');
25
+ assignIds(spaceId) {
26
+ if (assignIdSpace(this, spaceId)) {
27
+ this.prefixIds();
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Prefix the ids of the value sets this provider provides with spaceId + '-', once spaceId
33
+ * is set (by assignIds). Value sets the provider loads later must be given ids
34
+ * in the same space.
35
+ */
36
+ prefixIds() {
37
+ throw new Error('prefixIds must be implemented by AbstractValueSetProvider subclass');
23
38
  }
24
39
 
25
40
  /**
@@ -35,11 +50,11 @@ class AbstractValueSetProvider {
35
50
  }
36
51
 
37
52
  /**
38
- * Fetches a specific value set by id. ValueSet providers must enforce that value set ids are unique
39
- * either globally (as enforced by assignIds) or in their space
53
+ * Fetches a specific value set by id. The id is in this provider's space (spaceId + '-' + the
54
+ * value set's own id); an id that isn't - including an unprefixed one - finds nothing
40
55
  *
41
56
  * @param {string} id - The id of the value set
42
- * @returns {Promise<ValueSet>} The requested value set
57
+ * @returns {Promise<ValueSet>} The requested value set, or null
43
58
  * @throws {Error} Must be implemented by subclasses
44
59
  */
45
60
  // eslint-disable-next-line no-unused-vars
@@ -743,7 +743,17 @@ class ValueSetDatabase {
743
743
  const valueSetMap = new Map();
744
744
 
745
745
  for (const row of rows) {
746
- const valueSet = new ValueSet(JSON.parse(row.content));
746
+ let valueSet;
747
+ try {
748
+ valueSet = new ValueSet(JSON.parse(row.content));
749
+ } catch (e) {
750
+ // one unacceptable value set (e.g. one containing a CodeSystem, which the
751
+ // server doesn't support) must not take the whole package's value sets with it
752
+ require('../../library/logger').getInstance().child({ module: 'tx' })
753
+ .warn(`${source || 'ValueSet database'}: skipping ValueSet ${row.url || row.id}${row.version ? '|' + row.version : ''}: ${e.message}`);
754
+ this.vsCount--;
755
+ continue;
756
+ }
747
757
  valueSet.sourcePackage = source;
748
758
  // Attach the stored content hash so callers can detect changes
749
759
  // without recomputing over the full JSON.
@@ -839,10 +849,9 @@ class ValueSetDatabase {
839
849
  });
840
850
  } else {
841
851
  // Fall back to parsing JSON
842
- results = rows.map(row => {
843
- const vs = map.get(row.id);
844
- return vs;
845
- });
852
+ // a value set the map doesn't have was rejected when the map was loaded
853
+ // (see loadAllValueSets), so it isn't a result
854
+ results = rows.map(row => map.get(row.id)).filter(vs => vs);
846
855
  }
847
856
 
848
857
  resolve(results);
@@ -1105,10 +1114,6 @@ class ValueSetDatabase {
1105
1114
  return { query, params };
1106
1115
  }
1107
1116
 
1108
- // eslint-disable-next-line no-unused-vars
1109
- assignIds(ids) {
1110
- // nothing - we don't do any assigning.
1111
- }
1112
1117
 
1113
1118
  /**
1114
1119
  * Get a list of all ValueSet URLs in the database
@@ -3,8 +3,10 @@ const fs = require('fs').promises;
3
3
  const { AbstractValueSetProvider } = require('./vs-api');
4
4
  const { PackageContentLoader } = require('../../library/package-manager');
5
5
  const { ValueSetDatabase } = require('./vs-database');
6
+ const { checkContained } = require('../library/canonical-resource');
6
7
  const { VersionUtilities } = require('../../library/version-utilities');
7
8
  const {validateParameter} = require("../../library/utilities");
9
+ const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
8
10
 
9
11
  /**
10
12
  * Package-based ValueSet provider using shared database layer
@@ -77,6 +79,8 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
77
79
  Object.defineProperty(vs, 'isCached', { value: true, enumerable: false, configurable: true, writable: true });
78
80
  }
79
81
  }
82
+ // nothing, unless the spaceId was assigned before the provider was initialized
83
+ this.#applyIdPrefix();
80
84
  this.initialized = true;
81
85
  }
82
86
 
@@ -126,6 +130,15 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
126
130
  for (const entry of valueSetEntries) {
127
131
  const valueSet = await this.packageLoader.loadFile(entry);
128
132
  if (valueSet.url) {
133
+ try {
134
+ // don't store a value set the server can't use (e.g. one that contains a
135
+ // CodeSystem): it would be listed by search, and fail when read
136
+ checkContained(valueSet);
137
+ } catch (e) {
138
+ require('../../library/logger').getInstance().child({ module: 'tx' })
139
+ .warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ValueSet ${valueSet.url}: ${e.message}`);
140
+ continue;
141
+ }
129
142
  valueSets.push(valueSet);
130
143
  }
131
144
  }
@@ -202,7 +215,9 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
202
215
  this._validateSearchParams(searchParams);
203
216
 
204
217
  if (this.USE_DATABASE_SEARCH) {
205
- return await this.database.search(this.spaceId, this.valueSetMap, searchParams, elements);
218
+ const results = await this.database.search(this.spaceId, this.valueSetMap, searchParams, elements);
219
+ return this.#excludedIds.size === 0 ? results
220
+ : results.filter(r => !this.#excludedIds.has(stripIdPrefix((r.jsonObj || r).id, this.spaceId)));
206
221
  } else {
207
222
  const matches = [];
208
223
  const seen = new Set(); // Track by URL to avoid duplicates from versioned keys
@@ -388,38 +403,19 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
388
403
  }
389
404
 
390
405
  async fetchValueSetById(id) {
391
- return this.valueSetMap.get(id);
406
+ // the map is keyed by the value set's own id (and its url); the value set itself
407
+ // carries the prefixed id, which is how a url key that happens to match is excluded
408
+ const vs = this.valueSetMap.get(stripIdPrefix(id, this.spaceId));
409
+ return vs && vs.id === id ? vs : null;
392
410
  }
393
411
 
394
- // eslint-disable-next-line no-unused-vars
395
- assignIds(ids) {
396
- if (!this.spaceId) {
397
- return;
398
- }
399
-
400
- const prefix = this.spaceId + '-';
401
- const alreadyPrefixed = new Set();
402
-
403
- // Get all current entries - we'll iterate and modify
404
- const entries = Array.from(this.valueSetMap.entries());
405
-
406
- // eslint-disable-next-line no-unused-vars
407
- for (const [key, vs] of entries) {
408
- // Skip if we've already processed this ValueSet instance
409
- if (alreadyPrefixed.has(vs)) {
410
- continue;
411
- }
412
-
413
- // Update the id on the ValueSet itself
414
- if (vs.id && !vs.id.startsWith(prefix)) {
415
- const oldId = vs.id;
416
- vs.id = prefix + oldId;
417
-
418
- // Add to map under the new id as well
419
- this.valueSetMap.set(vs.id, vs);
420
- }
412
+ prefixIds() {
413
+ this.#applyIdPrefix();
414
+ }
421
415
 
422
- alreadyPrefixed.add(vs);
416
+ #applyIdPrefix() {
417
+ for (const vs of new Set(this.valueSetMap.values())) {
418
+ applyIdPrefix(vs, this.spaceId);
423
419
  }
424
420
  }
425
421
 
@@ -428,7 +424,38 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
428
424
  }
429
425
 
430
426
  async listAllValueSets() {
431
- return await this.database.listAllValueSets();
427
+ const urls = await this.database.listAllValueSets();
428
+ return this.#excludedUrls.size === 0 ? urls : urls.filter(url => !this.#excludedUrls.has(url));
429
+ }
430
+
431
+ // the value sets left out by excludeUrls - by url, and by their own (unprefixed) id
432
+ #excludedUrls = new Set();
433
+ #excludedIds = new Set();
434
+
435
+ /**
436
+ * Leave out the value sets with any of these urls: they can't be fetched, and they aren't
437
+ * found by search. The FHIR core package's value sets are left out where hl7.terminology
438
+ * provides the same url (see Provider.applyTHOPrecedence). Must be called once the
439
+ * provider is initialized - the database is shared, so it isn't changed.
440
+ *
441
+ * @param {Set<string>} urls
442
+ */
443
+ excludeUrls(urls) {
444
+ const excluded = new Set();
445
+ for (const vs of this.valueSetMap.values()) {
446
+ if (vs && urls.has(vs.url)) {
447
+ excluded.add(vs);
448
+ }
449
+ }
450
+ for (const [key, vs] of [...this.valueSetMap]) {
451
+ if (excluded.has(vs)) {
452
+ this.valueSetMap.delete(key);
453
+ }
454
+ }
455
+ for (const vs of excluded) {
456
+ this.#excludedUrls.add(vs.url);
457
+ this.#excludedIds.add(stripIdPrefix(vs.id, this.spaceId) ?? vs.id);
458
+ }
432
459
  }
433
460
  }
434
461
 
package/tx/vs/vs-vsac.js CHANGED
@@ -6,6 +6,7 @@ const { ValueSetDatabase } = require('./vs-database');
6
6
  const { VersionUtilities } = require('../../library/version-utilities');
7
7
  const folders = require('../../library/folder-setup');
8
8
  const {debugLog} = require("../operation-context");
9
+ const { applyIdPrefix, stripIdPrefix } = require("../library/resource-ids");
9
10
 
10
11
  // Persisted watermark for the phase-1b _lastUpdated scan.
11
12
  const VSAC_LAST_UPDATED_KEY = 'vsac_last_updated_date';
@@ -422,6 +423,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
422
423
  }
423
424
  }
424
425
  }
426
+ // the value sets are served in this provider's id space
427
+ for (const vs of new Set(newMap.values())) {
428
+ applyIdPrefix(vs, this.spaceId);
429
+ }
425
430
  // Atomic replacement of the map
426
431
  this.valueSetMap = newMap;
427
432
  }
@@ -466,7 +471,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
466
471
  }
467
472
 
468
473
  async fetchValueSetById(id) {
469
- return await this.checkFullVS(this.valueSetMap.get(id));
474
+ // the map is keyed by the value set's own id (and its url); the value set itself
475
+ // carries the prefixed id, which is how a url key that happens to match is excluded
476
+ const vs = this.valueSetMap.get(stripIdPrefix(id, this.spaceId));
477
+ return vs && vs.id === id ? await this.checkFullVS(vs) : null;
470
478
  }
471
479
  /**
472
480
  * Searches for value sets based on criteria
@@ -556,9 +564,16 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
556
564
  await this.database.close();
557
565
  }
558
566
 
559
- // eslint-disable-next-line no-unused-vars
560
- assignIds(ids) {
561
- // nothing?
567
+ prefixIds() {
568
+ this.#applyIdPrefix();
569
+ }
570
+
571
+ // the map is replaced every time it's reloaded from the database (see _reloadMap), and that
572
+ // can happen before or after the spaceId is assigned - so both prefix whatever is there
573
+ #applyIdPrefix() {
574
+ for (const vs of new Set(this.valueSetMap.values())) {
575
+ applyIdPrefix(vs, this.spaceId);
576
+ }
562
577
  }
563
578
 
564
579
  // when we get a valueset from vsac via search, the compose is not
@@ -898,7 +913,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
898
913
  html += `<tr>`;
899
914
  html += `<td>${escape(timeOnly(row.ts))}</td>`;
900
915
  html += `<td><span style="color:${colour}">${label}</span></td>`;
901
- html += `<td>${escape(this.urlTail(row.url) || '')} v <a href="../ValueSet/${escape(this.urlTail(row.url) || '')}-${escape(row.version || '')}">${escape(row.version || '')}</a></td>`;
916
+ html += `<td>${escape(this.urlTail(row.url) || '')} v <a href="../ValueSet/${escape(this.spaceId ? this.spaceId + '-' : '')}${escape(this.urlTail(row.url) || '')}-${escape(row.version || '')}">${escape(row.version || '')}</a></td>`;
902
917
  html += `</tr>`;
903
918
  }
904
919
  }
@@ -620,19 +620,23 @@ class ValueSetExpander {
620
620
  return count;
621
621
  }
622
622
 
623
+ /**
624
+ * Exclude the codes of an (expanded) value set. Excludes are processed before includes,
625
+ * so the codes are recorded as excluded - as excludeCode does - and the includes then
626
+ * skip them. (This used to remove them from what had been included so far, which at
627
+ * that point is nothing, so excluding a value set excluded nothing.)
628
+ */
623
629
  excludeValueSet(vs, expansion, imports, offset) {
624
- for (const c of vs.expansion.contains) {
625
- this.worker.deadCheck('excludeValueSet');
626
- const s = this.keyC(c);
627
- if (this.passesImports(imports, c.system, c.code, offset) && this.map.has(s)) {
628
- const idx = this.fullList.indexOf(this.map.get(s));
629
- if (idx >= 0) {
630
- this.fullList.splice(idx, 1);
630
+ const walk = (list) => {
631
+ for (const c of list || []) {
632
+ this.worker.deadCheck('excludeValueSet');
633
+ if (c.code && this.passesImports(imports, c.system, c.code, offset)) {
634
+ this.excluded.add((this.doingVersion && !this.params.versionsMatch ? c.system + '|' + c.version : c.system) + '#' + c.code);
631
635
  }
632
- this.map.delete(s);
633
- this.decTotal();
636
+ walk(c.contains);
634
637
  }
635
- }
638
+ };
639
+ walk(vs.expansion.contains);
636
640
  }
637
641
 
638
642
  async checkSource(cset, exp, filter, srcURL, ts, vsInfo , source) {
@@ -1012,8 +1016,10 @@ class ValueSetExpander {
1012
1016
  let vs = await this.worker.findValueSet(s, '', vsSrc);
1013
1017
  const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
1014
1018
  this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
1015
- if (!vs.isContained && ivs.valueSet.vurl) {
1016
- this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl);
1019
+ // ivs.valueSet is the expansion (plain JSON), so it has no vurl - as for the
1020
+ // includes, build it
1021
+ if (!vs.isContained && this.worker.makeVurl(ivs.valueSet)) {
1022
+ this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
1017
1023
  }
1018
1024
  valueSets.push(ivs);
1019
1025
  }
@@ -1314,13 +1320,22 @@ class ValueSetExpander {
1314
1320
  }
1315
1321
 
1316
1322
  async expand(source, filter, noCacheThisOne) {
1317
- this.noCacheThisOne = noCacheThisOne;
1318
- this.totalStatus = 'uninitialised';
1319
- this.total = 0;
1320
-
1321
1323
  Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
1322
1324
  Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
1325
+ // circular reference detection: this value set is in the chain being processed until
1326
+ // its expansion is done
1323
1327
  this.worker.seeValueSet(source, this.params);
1328
+ try {
1329
+ return await this.expandSeen(source, filter, noCacheThisOne);
1330
+ } finally {
1331
+ this.worker.unseeValueSet(source);
1332
+ }
1333
+ }
1334
+
1335
+ async expandSeen(source, filter, noCacheThisOne) {
1336
+ this.noCacheThisOne = noCacheThisOne;
1337
+ this.totalStatus = 'uninitialised';
1338
+ this.total = 0;
1324
1339
  this.valueSet = source;
1325
1340
 
1326
1341
  const result = structuredClone(source.jsonObj);
@@ -1348,6 +1363,10 @@ class ValueSetExpander {
1348
1363
  if (result.expansion) {
1349
1364
  return result; // just return the expansion
1350
1365
  }
1366
+ if (!source.jsonObj.compose) {
1367
+ throw new Issue('error', 'invalid', null, 'VALUESET_NO_COMPOSE',
1368
+ this.worker.i18n.translate('VALUESET_NO_COMPOSE', this.params.httpLanguages, [source.contextKey || source.vurlOrMsg]), 'vs-invalid', 422);
1369
+ }
1351
1370
 
1352
1371
  if (this.params.generateNarrative && !this.noDetails) {
1353
1372
  div_ = div();
@@ -202,7 +202,8 @@ class MetadataHandler {
202
202
  software: {
203
203
  name: this.config.softwareName || 'FHIR Terminology Server',
204
204
  version: serverVersion,
205
- releaseDate: this.config.releaseDate || now
205
+ // only when it's known - this used to fall back to the time of the request
206
+ ...(this.config.releaseDate ? { releaseDate: this.config.releaseDate } : {})
206
207
  },
207
208
  implementation: {
208
209
  description: `FHIR Server running at ${baseUrl}`,
@@ -437,7 +438,6 @@ class MetadataHandler {
437
438
  // Parameters the $expand handler reads - see handleTypeLevelExpand in
438
439
  // tx/workers/expand.js and TxParameters.seeParameter in tx/params.js.
439
440
  // Keep this list in step with those when adding a parameter.
440
- { name: '_incomplete' },
441
441
  { name: 'abstract' },
442
442
  { name: 'activeOnly' },
443
443
  { name: 'check-system-version' },
@@ -453,20 +453,12 @@ class MetadataHandler {
453
453
  { name: 'filter' },
454
454
  { name: 'force-system-version' },
455
455
  { name: 'force-valueset-version' },
456
- { name: 'inactive' },
457
- { name: 'includeAlternateCodes' },
458
456
  { name: 'includeDefinition' },
459
457
  { name: 'includeDesignations' },
460
- { name: 'incomplete-ok' },
461
458
  {
462
459
  name: 'limit',
463
460
  documentation: 'Upper bound on the size of the expansion; the operation fails as too-costly beyond it'
464
461
  },
465
- { name: 'limitedExpansion' },
466
- {
467
- name: 'mode',
468
- documentation: '=lenient-display-validation'
469
- },
470
462
  { name: 'no-cache' },
471
463
  { name: 'offset' },
472
464
  { name: 'profile' },
@@ -479,10 +471,6 @@ class MetadataHandler {
479
471
  { name: 'url' },
480
472
  { name: 'useSupplement' },
481
473
  { name: 'valueSet' },
482
- {
483
- name: 'valueSetMode',
484
- documentation: '= CHECK_MEMBERSHIP_ONLY | NO_MEMBERSHIP_CHECK'
485
- },
486
474
  { name: 'valueSetVersion' }
487
475
  ]
488
476
  };
@@ -61,21 +61,6 @@ class ReadWorker extends TerminologyWorker {
61
61
  }
62
62
  }
63
63
 
64
- /**
65
- * The resource as served at [type]/[id] must carry that id. Providers that
66
- * share the server with others prefix their ids with a space id ("1-foo") but
67
- * may keep the resource itself under its original id ("foo") - package
68
- * ConceptMaps do - and then the HTML tabs, which link to [type]/[resource.id],
69
- * point at a resource that doesn't exist (issue #256). Copy rather than
70
- * mutate: the provider's object is shared and keyed on its original id.
71
- */
72
- withId(json, id) {
73
- if (!json || json.id === id) {
74
- return json;
75
- }
76
- return { ...json, id: id };
77
- }
78
-
79
64
  /**
80
65
  * Handle CodeSystem read
81
66
  */
@@ -83,7 +68,7 @@ class ReadWorker extends TerminologyWorker {
83
68
  let cs = this.provider.getCodeSystemById(this.opContext, id);
84
69
  if (cs != null) {
85
70
  req.sourcePackage = cs.sourcePackage;
86
- return res.json(this.withId(cs.jsonObj, id));
71
+ return res.json(cs.jsonObj);
87
72
  }
88
73
 
89
74
  if (id.startsWith("x-")) {
@@ -142,7 +127,7 @@ class ReadWorker extends TerminologyWorker {
142
127
  const vs = await vsp.fetchValueSetById(id);
143
128
  if (vs) {
144
129
  req.sourcePackage = vs.sourcePackage;
145
- return res.json(this.withId(vs.jsonObj, id));
130
+ return res.json(vs.jsonObj);
146
131
  }
147
132
  }
148
133
 
@@ -158,7 +143,7 @@ class ReadWorker extends TerminologyWorker {
158
143
  const cm = await cmsp.fetchConceptMapById(id);
159
144
  if (cm) {
160
145
  req.sourcePackage = cm.sourcePackage;
161
- return res.json(this.withId(cm.jsonObj, id));
146
+ return res.json(cm.jsonObj);
162
147
  }
163
148
  }
164
149