fhirsmith 0.14.2 → 0.15.1

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 (43) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/config-template.json +10 -0
  3. package/library/changelog.js +57 -0
  4. package/library/npm-audit.js +62 -9
  5. package/package.json +2 -2
  6. package/packages/npm-check-page.js +108 -0
  7. package/packages/npm-check.js +450 -0
  8. package/packages/openapi.yaml +158 -5
  9. package/packages/package-crawler.js +203 -20
  10. package/packages/packages.js +250 -8
  11. package/packages/readme.md +55 -2
  12. package/packages/utilities/dedupe-versions.js +242 -0
  13. package/registry/crawler.js +17 -0
  14. package/registry/fhirsmith-releases.js +3 -6
  15. package/registry/model.js +3 -0
  16. package/registry/registry.js +28 -3
  17. package/translations/Messages.properties +3 -0
  18. package/tx/README.md +20 -4
  19. package/tx/cm/cm-api.js +25 -10
  20. package/tx/cm/cm-database.js +0 -4
  21. package/tx/cm/cm-package.js +53 -14
  22. package/tx/cs/cs-api.js +6 -1
  23. package/tx/cs/cs-icd11.js +23 -6
  24. package/tx/cs/cs-provider-api.js +22 -7
  25. package/tx/cs/cs-provider-list.js +10 -9
  26. package/tx/cs/icd11-expressions.js +5 -6
  27. package/tx/importers/icd11-schema.md +1 -0
  28. package/tx/importers/import-icd11.js +39 -0
  29. package/tx/library/resource-ids.js +107 -0
  30. package/tx/library.js +96 -24
  31. package/tx/ocl/cm-ocl.cjs +23 -15
  32. package/tx/ocl/cs-ocl.cjs +9 -10
  33. package/tx/ocl/vs-ocl.cjs +17 -17
  34. package/tx/provider.js +80 -13
  35. package/tx/tx.fhir.org.yml +17 -9
  36. package/tx/tx.js +7 -1
  37. package/tx/vs/vs-api.js +25 -10
  38. package/tx/vs/vs-database.js +0 -4
  39. package/tx/vs/vs-package.js +48 -31
  40. package/tx/vs/vs-vsac.js +20 -5
  41. package/tx/workers/lookup.js +7 -4
  42. package/tx/workers/metadata.js +2 -1
  43. package/tx/workers/read.js +3 -18
package/tx/ocl/vs-ocl.cjs CHANGED
@@ -114,6 +114,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
114
114
  id: cached.canonicalUrl
115
115
  }, 'R5');
116
116
  this.#applyCachedCompose(valueSetObj, paramsKey);
117
+ this.#prefixId(valueSetObj);
117
118
  // Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet
118
119
  this.valueSetMap.set(valueSetObj.url, valueSetObj);
119
120
  if (valueSetObj.version) {
@@ -224,7 +225,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
224
225
  }
225
226
  }
226
227
 
227
- assignIds(ids) {
228
+ prefixIds() {
228
229
  if (!this.spaceId) {
229
230
  return;
230
231
  }
@@ -233,13 +234,18 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
233
234
  this._idMap.clear();
234
235
 
235
236
  for (const vs of unique) {
236
- if (!vs.id.startsWith(`${this.spaceId}-`)) {
237
- const nextId = `${this.spaceId}-${vs.id}`;
238
- vs.id = nextId;
239
- vs.jsonObj.id = nextId;
240
- }
237
+ this.#prefixId(vs);
241
238
  this._idMap.set(vs.id, vs);
242
- ids.add(`ValueSet/${vs.id}`);
239
+ }
240
+ }
241
+
242
+ // value sets are served in this provider's id space - including the ones that arrive
243
+ // after the spaceId is assigned
244
+ #prefixId(vs) {
245
+ if (this.spaceId && vs.id && !vs.id.startsWith(`${this.spaceId}-`)) {
246
+ const nextId = `${this.spaceId}-${vs.id}`;
247
+ vs.id = nextId;
248
+ vs.jsonObj.id = nextId;
243
249
  }
244
250
  }
245
251
 
@@ -345,16 +351,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
345
351
  }
346
352
 
347
353
  #getLocalValueSetById(id) {
348
- if (this._idMap.has(id)) {
349
- return this._idMap.get(id);
350
- }
351
-
352
- if (this.spaceId && id.startsWith(`${this.spaceId}-`)) {
353
- const unprefixed = id.substring(this.spaceId.length + 1);
354
- return this._idMap.get(id) || this._idMap.get(unprefixed) || this.valueSetMap.get(unprefixed) || null;
355
- }
356
-
357
- return this._idMap.get(id) || this.valueSetMap.get(id) || null;
354
+ // _idMap is keyed by the ids the value sets are served with - prefixed, once the
355
+ // spaceId is assigned
356
+ return this._idMap.get(id) || null;
358
357
  }
359
358
 
360
359
  // eslint-disable-next-line no-unused-vars
@@ -391,6 +390,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
391
390
  }
392
391
 
393
392
  #indexValueSet(vs) {
393
+ this.#prefixId(vs);
394
394
  const existing = this.valueSetMap.get(vs.url) || null;
395
395
 
396
396
  // When fresh discovery metadata replaces a cold-cached entry, carry over
package/tx/provider.js CHANGED
@@ -9,6 +9,7 @@ const {PackageContentLoader} = require("../library/package-manager");
9
9
  const {PackageValueSetProvider} = require("./vs/vs-package");
10
10
  const ValueSet = require("./library/valueset");
11
11
  const {PackageConceptMapProvider} = require("./cm/cm-package");
12
+ const { applyIdPrefix, CORE_PREFIX } = require("./library/resource-ids");
12
13
 
13
14
  /**
14
15
  * This class holds what information is in context
@@ -153,6 +154,12 @@ class Provider {
153
154
  }
154
155
 
155
156
 
157
+ /**
158
+ * Load the FHIR core package for this endpoint. Its resources are served in the 'core' id
159
+ * space (CodeSystem/core-v3-MaritalStatus): the core packages for different FHIR versions
160
+ * share ids, and so does other content (hl7.terminology), so no endpoint can serve them on
161
+ * their own ids (see tx/library/resource-ids.js)
162
+ */
156
163
  async loadNpm(packageManager, cacheFolder, details, isDefault, mode) {
157
164
  // Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2")
158
165
  let packageId = details;
@@ -171,6 +178,9 @@ class Provider {
171
178
  await contentLoader.initialize();
172
179
 
173
180
  const resources = await contentLoader.getResourcesByType("CodeSystem");
181
+ let i = 0;
182
+ // the index has the urls as they are in the package; the version is taken out of the url
183
+ // when the code system is loaded (see normalizeVersionedCanonicals in package-manager.js)
174
184
  for (const resource of resources) {
175
185
  let cs;
176
186
  try {
@@ -183,13 +193,24 @@ class Provider {
183
193
  continue;
184
194
  }
185
195
  cs.sourcePackage = contentLoader.pid();
196
+ if (typeof resource.url === 'string' && resource.url.includes('|')) {
197
+ cs.urlHadVersion = true; // see addCodeSystem
198
+ }
199
+ // these objects belong to this endpoint, so they can be given ids here
200
+ i++;
201
+ if (!cs.id) {
202
+ cs.id = String(i);
203
+ }
204
+ applyIdPrefix(cs, CORE_PREFIX);
186
205
  this.addCodeSystem(cs);
187
206
  }
188
207
  const vs = new PackageValueSetProvider(contentLoader);
189
208
  await vs.initialize();
209
+ vs.assignIds(CORE_PREFIX);
190
210
  this.valueSetProviders.push(vs);
191
211
  const cm = new PackageConceptMapProvider(contentLoader);
192
212
  await cm.initialize();
213
+ cm.assignIds(CORE_PREFIX);
193
214
  this.conceptMapProviders.push(cm);
194
215
  }
195
216
 
@@ -582,27 +603,38 @@ class Provider {
582
603
  return pid != null && pid.startsWith("hl7.terminology");
583
604
  }
584
605
 
606
+ /**
607
+ * Add a code system to the ones this endpoint knows, by url and by url|version.
608
+ *
609
+ * THO precedence, as in CanonicalResourceManager.see() in the Java core: content that
610
+ * moved from the FHIR core packages to terminology.hl7.org has been QA'd there since, and
611
+ * its versions went backwards (coverage-selfpay is 4.0.1 in hl7.fhir.r4.core but 1.0.1 in
612
+ * hl7.terminology), so version precedence can't decide between them. Instead, a code system
613
+ * from an hl7.terminology package replaces every code system with the same url from a FHIR
614
+ * core package - none of the core versions stay available.
615
+ *
616
+ * Except: hl7.fhir.r4.core has the v2 tables 0006, 0360 and 0391 in two versions each,
617
+ * with the version in the url (http://terminology.hl7.org/CodeSystem/v2-0006|2.1 - see
618
+ * urlHadVersion in loadNpm). hl7.terminology doesn't publish those versions, so they stay
619
+ * available by url|version; THO's code system is the default for the url.
620
+ */
585
621
  addCodeSystem(cs) {
586
- // Special case, mirroring CanonicalResourceManager.see() in the Java core:
587
- // when content moved from the FHIR core packages to terminology.hl7.org,
588
- // the versions went backwards (e.g. coverage-selfpay is 4.0.1 in
589
- // hl7.fhir.r4.core (2019) but 1.0.1 in hl7.terminology (2024)), so plain
590
- // version precedence would leave the stale core copy as the default. A
591
- // resource from an hl7.terminology package displaces any same-URL
592
- // resource that came from a FHIR core package, regardless of version.
593
622
  if (Provider.#isTHOPackage(cs.sourcePackage)) {
594
623
  for (const [key, t] of [...this.codeSystems]) {
595
- // only the bare-url (default) entry is displaced; explicitly versioned
596
- // entries (key = url|version) stay addressable, because the core packages
597
- // carry historical versions (e.g. the normalized R4 v2 tables 0006/0360/0391
598
- // at 2.1/2.3.1/2.4/2.6/2.7) that hl7.terminology does not publish at all
599
- if (key === t.url && t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)) {
624
+ if (t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)
625
+ && (key === t.url || !t.urlHadVersion)) {
600
626
  this.codeSystems.delete(key);
601
627
  }
602
628
  }
629
+ } else if (VersionUtilities.isCorePackage(cs.sourcePackage) && !cs.urlHadVersion) {
630
+ // THO's code system is always the default for its url
631
+ const t = this.codeSystems.get(cs.url);
632
+ if (t && Provider.#isTHOPackage(t.sourcePackage)) {
633
+ return;
634
+ }
603
635
  }
604
636
  const existing = this.codeSystems.get(cs.url);
605
- // the reverse guard: a core-package resource never displaces a THO one
637
+ // a core-package resource never displaces a THO one as the default
606
638
  const yieldsToTHO = existing && Provider.#isTHOPackage(existing.sourcePackage)
607
639
  && VersionUtilities.isCorePackage(cs.sourcePackage);
608
640
  if (!existing || (!yieldsToTHO && cs.isMoreRecent(existing))) {
@@ -619,6 +651,41 @@ class Provider {
619
651
  }
620
652
  }
621
653
 
654
+ /**
655
+ * THO precedence for value sets and concept maps, as for code systems (see addCodeSystem):
656
+ * once the library's providers have been added after this endpoint's core package, the core
657
+ * package's value sets and concept maps whose urls an hl7.terminology package also provides
658
+ * are left out. (No core value set or concept map has the version in its url.)
659
+ */
660
+ applyTHOPrecedence() {
661
+ const thoValueSets = new Set();
662
+ for (const vp of this.valueSetProviders) {
663
+ if (vp instanceof PackageValueSetProvider && Provider.#isTHOPackage(vp.sourcePackage())) {
664
+ for (const vs of vp.valueSetMap.values()) {
665
+ thoValueSets.add(vs.url);
666
+ }
667
+ }
668
+ }
669
+ const thoConceptMaps = new Set();
670
+ for (const cmp of this.conceptMapProviders) {
671
+ if (cmp instanceof PackageConceptMapProvider && Provider.#isTHOPackage(cmp.sourcePackage())) {
672
+ for (const cm of cmp.conceptMapMap.values()) {
673
+ thoConceptMaps.add(cm.url);
674
+ }
675
+ }
676
+ }
677
+ for (const vp of this.valueSetProviders) {
678
+ if (vp instanceof PackageValueSetProvider && VersionUtilities.isCorePackage(vp.sourcePackage())) {
679
+ vp.excludeUrls(thoValueSets);
680
+ }
681
+ }
682
+ for (const cmp of this.conceptMapProviders) {
683
+ if (cmp instanceof PackageConceptMapProvider && VersionUtilities.isCorePackage(cmp.sourcePackage())) {
684
+ cmp.excludeUrls(thoConceptMaps);
685
+ }
686
+ }
687
+ }
688
+
622
689
  deleteCodeSystem(cs) {
623
690
  this.codeSystems.delete(cs.vurl);
624
691
  this.codeSystems.delete(cs.url);
@@ -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,15 +37,15 @@ 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
- - icd11:icd11-2026-01.db
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
48
+ - icd11:icd11-2026-01-b.db
41
49
 
42
50
  ignored:
43
51
  - urn:iso:std:iso:3166#20210120
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');
@@ -223,7 +227,9 @@ class TXModule {
223
227
  description: config.description || 'FHIR Terminology Server',
224
228
  contactUrl: config.contactUrl,
225
229
  contact: config.contact,
226
- 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),
227
233
  host: config.host ? config.host : "localhost"
228
234
  });
229
235
 
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
@@ -1114,10 +1114,6 @@ class ValueSetDatabase {
1114
1114
  return { query, params };
1115
1115
  }
1116
1116
 
1117
- // eslint-disable-next-line no-unused-vars
1118
- assignIds(ids) {
1119
- // nothing - we don't do any assigning.
1120
- }
1121
1117
 
1122
1118
  /**
1123
1119
  * Get a list of all ValueSet URLs in the database
@@ -6,6 +6,7 @@ const { ValueSetDatabase } = require('./vs-database');
6
6
  const { checkContained } = require('../library/canonical-resource');
7
7
  const { VersionUtilities } = require('../../library/version-utilities');
8
8
  const {validateParameter} = require("../../library/utilities");
9
+ const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
9
10
 
10
11
  /**
11
12
  * Package-based ValueSet provider using shared database layer
@@ -78,6 +79,8 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
78
79
  Object.defineProperty(vs, 'isCached', { value: true, enumerable: false, configurable: true, writable: true });
79
80
  }
80
81
  }
82
+ // nothing, unless the spaceId was assigned before the provider was initialized
83
+ this.#applyIdPrefix();
81
84
  this.initialized = true;
82
85
  }
83
86
 
@@ -212,7 +215,9 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
212
215
  this._validateSearchParams(searchParams);
213
216
 
214
217
  if (this.USE_DATABASE_SEARCH) {
215
- 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)));
216
221
  } else {
217
222
  const matches = [];
218
223
  const seen = new Set(); // Track by URL to avoid duplicates from versioned keys
@@ -398,38 +403,19 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
398
403
  }
399
404
 
400
405
  async fetchValueSetById(id) {
401
- 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;
402
410
  }
403
411
 
404
- // eslint-disable-next-line no-unused-vars
405
- assignIds(ids) {
406
- if (!this.spaceId) {
407
- return;
408
- }
409
-
410
- const prefix = this.spaceId + '-';
411
- const alreadyPrefixed = new Set();
412
-
413
- // Get all current entries - we'll iterate and modify
414
- const entries = Array.from(this.valueSetMap.entries());
415
-
416
- // eslint-disable-next-line no-unused-vars
417
- for (const [key, vs] of entries) {
418
- // Skip if we've already processed this ValueSet instance
419
- if (alreadyPrefixed.has(vs)) {
420
- continue;
421
- }
422
-
423
- // Update the id on the ValueSet itself
424
- if (vs.id && !vs.id.startsWith(prefix)) {
425
- const oldId = vs.id;
426
- vs.id = prefix + oldId;
427
-
428
- // Add to map under the new id as well
429
- this.valueSetMap.set(vs.id, vs);
430
- }
412
+ prefixIds() {
413
+ this.#applyIdPrefix();
414
+ }
431
415
 
432
- alreadyPrefixed.add(vs);
416
+ #applyIdPrefix() {
417
+ for (const vs of new Set(this.valueSetMap.values())) {
418
+ applyIdPrefix(vs, this.spaceId);
433
419
  }
434
420
  }
435
421
 
@@ -438,7 +424,38 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
438
424
  }
439
425
 
440
426
  async listAllValueSets() {
441
- 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
+ }
442
459
  }
443
460
  }
444
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
  }
@@ -7,7 +7,7 @@
7
7
  // POST /CodeSystem/{id}/$lookup
8
8
  //
9
9
 
10
- const { TerminologyWorker } = require('./worker');
10
+ const { TerminologyWorker, Unknown_Code_in_VersionSCT, SCTVersion } = require('./worker');
11
11
  const { FhirCodeSystemProvider } = require('../cs/cs-cs');
12
12
  const { Designations} = require("../library/designations");
13
13
  const {TxParameters} = require("../params");
@@ -251,13 +251,16 @@ class LookupWorker extends TerminologyWorker {
251
251
  const locateResult = await csProvider.locate(code);
252
252
 
253
253
  if (!locateResult || !locateResult.context) {
254
- let message = `Unable to find code '${code}' in ${csProvider.system()} version ${csProvider.version() || 'unknown'}`
254
+ const system = csProvider.system();
255
+ const version = csProvider.version();
256
+ const msgId = Unknown_Code_in_VersionSCT(system, version);
257
+ let message = this.i18n.translate(msgId, params.HTTPLanguages, [code, system, version, SCTVersion(system, version)]);
255
258
  if (locateResult?.message) {
256
259
  message += ' ('+locateResult.message+')';
257
260
  }
258
261
  // invalid-code, not not-found: the code system was found, the code in it was not.
259
262
  // not-found is for a code system or value set that could not be resolved at all.
260
- throw new Issue('error', 'not-found', null, null, message, 'invalid-code', 404);
263
+ throw new Issue('error', 'not-found', null, msgId, message, 'invalid-code', 404);
261
264
  }
262
265
 
263
266
  const ctxt = locateResult.context;
@@ -268,7 +271,7 @@ class LookupWorker extends TerminologyWorker {
268
271
  // name (required)
269
272
  responseParams.push({
270
273
  name: 'name',
271
- valueString: csProvider.name()
274
+ valueString: csProvider.name(params.workingLanguages())
272
275
  });
273
276
  responseParams.push({
274
277
  name: 'code',
@@ -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}`,
@@ -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