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
@@ -5,6 +5,7 @@ const { ConceptMapDatabase } = require('./cm-database');
5
5
  const { VersionUtilities } = require('../../library/version-utilities');
6
6
  const {validateParameter} = require("../../library/utilities");
7
7
  const {ConceptMap} = require("../library/conceptmap");
8
+ const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
8
9
 
9
10
  /**
10
11
  * Package-based ConceptMap provider using shared database layer
@@ -34,6 +35,7 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
34
35
  if (this.initialized) {
35
36
  return;
36
37
  }
38
+ await this.packageLoader.initialize();
37
39
 
38
40
  const dbExists = await this.database.exists();
39
41
 
@@ -43,6 +45,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
43
45
  }
44
46
 
45
47
  this.conceptMapMap = await this.database.loadAllConceptMaps();
48
+ // nothing, unless the spaceId was assigned before the provider was initialized
49
+ this.#applyIdPrefix();
46
50
  this.initialized = true;
47
51
  }
48
52
 
@@ -63,7 +67,14 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
63
67
  for (const entry of conceptMapEntries) {
64
68
  const conceptMap = await this.packageLoader.loadFile(entry);
65
69
  if (conceptMap.url) {
66
- conceptMaps.push(new ConceptMap(conceptMap, this.packageLoader.fhirVersion())); // get in converted to R5 format
70
+ try {
71
+ conceptMaps.push(new ConceptMap(conceptMap, this.packageLoader.fhirVersion())); // get in converted to R5 format
72
+ } catch (e) {
73
+ // one unacceptable concept map (e.g. one with contained resources, which the
74
+ // server doesn't support) must not stop the rest of the package loading
75
+ require('../../library/logger').getInstance().child({ module: 'tx' })
76
+ .warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ConceptMap ${conceptMap.url}: ${e.message}`);
77
+ }
67
78
  }
68
79
  }
69
80
 
@@ -121,7 +132,9 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
121
132
  this._validateSearchParams(searchParams);
122
133
 
123
134
  if (this.USE_DATABASE_SEARCH) {
124
- return await this.database.search(this.spaceId, searchParams, elements);
135
+ const results = await this.database.search(this.spaceId, searchParams, elements);
136
+ return this.#excludedIds.size === 0 ? results
137
+ : results.filter(r => !this.#excludedIds.has(stripIdPrefix(r.id, this.spaceId)));
125
138
  } else {
126
139
  const matches = [];
127
140
  const seen = new Set(); // Track by URL to avoid duplicates from versioned keys
@@ -187,9 +200,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
187
200
 
188
201
  if (isMatch) {
189
202
  seen.add(vsUrl);
190
- // Return with prefixed id
191
- const result = { ...json, id: `${this.spaceId}-${json.id}` };
192
- matches.push(result);
203
+ // the id is already prefixed (see assignIds)
204
+ matches.push(json);
193
205
  }
194
206
  }
195
207
 
@@ -291,19 +303,53 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
291
303
  }
292
304
 
293
305
  async fetchConceptMapById(id) {
294
- if (!this.spaceId) {
295
- return this.conceptMapMap.get(id);
296
- } else if (id.startsWith(this.spaceId+"-")) {
297
- let key = id.substring(this.spaceId.length + 1);
298
- return this.conceptMapMap.get(key);
299
- } else {
300
- return null;
306
+ // the map is keyed by the concept map's own id (and its url); the concept map itself
307
+ // carries the prefixed id, which is how a url key that happens to match is excluded
308
+ const cm = this.conceptMapMap.get(stripIdPrefix(id, this.spaceId));
309
+ return cm && cm.id === id ? cm : null;
310
+ }
311
+
312
+ sourcePackage() {
313
+ return this.packageLoader.id();
314
+ }
315
+
316
+ // the concept maps left out by excludeUrls, by their own (unprefixed) id
317
+ #excludedIds = new Set();
318
+
319
+ /**
320
+ * Leave out the concept maps with any of these urls: they can't be fetched, they aren't
321
+ * found by search, and they aren't used for translation. The FHIR core package's concept
322
+ * maps are left out where hl7.terminology provides the same url (see
323
+ * Provider.applyTHOPrecedence). Must be called once the provider is initialized - the
324
+ * database is shared, so it isn't changed.
325
+ *
326
+ * @param {Set<string>} urls
327
+ */
328
+ excludeUrls(urls) {
329
+ const excluded = new Set();
330
+ for (const cm of this.conceptMapMap.values()) {
331
+ if (cm && urls.has(cm.url)) {
332
+ excluded.add(cm);
333
+ }
334
+ }
335
+ for (const [key, cm] of [...this.conceptMapMap]) {
336
+ if (excluded.has(cm)) {
337
+ this.conceptMapMap.delete(key);
338
+ }
301
339
  }
340
+ for (const cm of excluded) {
341
+ this.#excludedIds.add(stripIdPrefix(cm.id, this.spaceId) ?? cm.id);
342
+ }
343
+ }
344
+
345
+ prefixIds() {
346
+ this.#applyIdPrefix();
302
347
  }
303
348
 
304
- // eslint-disable-next-line no-unused-vars
305
- assignIds(ids) {
306
- // nothing - we don't do any assigning.
349
+ #applyIdPrefix() {
350
+ for (const cm of new Set(this.conceptMapMap.values())) {
351
+ applyIdPrefix(cm, this.spaceId);
352
+ }
307
353
  }
308
354
 
309
355
  async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem) {
@@ -1,22 +1,37 @@
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 AbstractCodeSystemProvider {
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, or autonumbered. Every code system this provider provides has an id that starts
11
+ * with spaceId + '-' (see tx/library/resource-ids.js)
8
12
  */
9
13
  spaceId;
10
14
 
11
15
  /**
12
- * ensure that the ids on the code systems are unique, if they are
13
- * in the global namespace
16
+ * Put this provider in an id space: set spaceId, and prefix the ids of the code systems it
17
+ * provides with spaceId + '-'. A provider is put in one id space, once - asking for a
18
+ * different one later is an error (see assignIdSpace)
14
19
  *
15
- * @param {Set<String>} ids
20
+ * @param {string} spaceId
16
21
  */
17
- // eslint-disable-next-line no-unused-vars
18
- assignIds(ids) {
19
- throw new Error('assignIds must be implemented by subclass');
22
+ assignIds(spaceId) {
23
+ if (assignIdSpace(this, spaceId)) {
24
+ this.prefixIds();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Prefix the ids of the code systems this provider provides with spaceId + '-', once spaceId
30
+ * is set (by assignIds). Code systems the provider provides later (see
31
+ * getCodeSystemChanges) must be given ids in the same space.
32
+ */
33
+ prefixIds() {
34
+ throw new Error('prefixIds must be implemented by subclass');
20
35
  }
21
36
 
22
37
  /**
@@ -1,4 +1,5 @@
1
1
  const { AbstractCodeSystemProvider } = require('./cs-provider-api');
2
+ const { applyIdPrefix } = require('../library/resource-ids');
2
3
 
3
4
  /**
4
5
  * Package-based ValueSet provider using shared database layer
@@ -11,18 +12,18 @@ class ListCodeSystemProvider extends AbstractCodeSystemProvider {
11
12
  codeSystems = [];
12
13
 
13
14
  /**
14
- * ensure that the ids on the code systems are unique, if they are
15
- * in the global namespace
16
- *
17
- * @param {Set<String>} ids
15
+ * prefix the ids of the code systems with this provider's spaceId. A code system without
16
+ * an id gets its position in the list
18
17
  */
19
- // eslint-disable-next-line no-unused-vars
20
- assignIds(ids) {
18
+ prefixIds() {
19
+ let i = 0;
21
20
  for (const cs of this.codeSystems) {
22
- if (!cs.id || ids.has("CodeSystem/"+cs.id)) {
23
- cs.id = ""+ids.size;
21
+ i++;
22
+ const json = cs.jsonObj || cs;
23
+ if (!json.id) {
24
+ json.id = String(i);
24
25
  }
25
- ids.add("CodeSystem/"+cs.id);
26
+ applyIdPrefix(cs, this.spaceId);
26
27
  }
27
28
  }
28
29
 
@@ -36,12 +36,27 @@ class RxNormPrep {
36
36
 
37
37
  // Iterator context
38
38
  class RxNormIteratorContext {
39
- constructor(query, params = {}) {
39
+ /**
40
+ * @param {string|null} query - the SQL that produces the rows, or null for an iterator with no rows
41
+ * @param {object} params - the query parameters
42
+ * @param {number} [total] - the number of rows the query will return, if known. The expander
43
+ * reads it to refuse an over-limit whole-code-system expansion before it is materialised
44
+ */
45
+ constructor(query, params = {}, total = undefined) {
40
46
  this.query = query;
41
47
  this.params = params;
42
48
  this.cursor = 0;
43
- this.results = null;
44
- this.executed = false;
49
+ if (query) {
50
+ this.results = null;
51
+ this.executed = false;
52
+ this.total = total;
53
+ } else {
54
+ // nothing to run: an empty statement isn't a no-op in node-sqlite3, it fails with
55
+ // "SQLITE_MISUSE: not an error"
56
+ this.results = [];
57
+ this.executed = true;
58
+ this.total = 0;
59
+ }
45
60
  }
46
61
 
47
62
  more() {
@@ -288,10 +303,12 @@ class RxNormServices extends CodeSystemProvider {
288
303
  if (!context) {
289
304
  // Iterate all codes
290
305
  const query = `SELECT ${this.getCodeField()}, STR FROM rxnconso WHERE SAB = ? AND TTY <> 'SY' ORDER BY ${this.getCodeField()}`;
291
- return new RxNormIteratorContext(query, { sab: this.getSAB() });
306
+ return new RxNormIteratorContext(query, { sab: this.getSAB() }, this.totalCodeCount);
292
307
  } else {
293
- // No hierarchical iteration for specific contexts in this implementation
294
- return new RxNormIteratorContext('', {});
308
+ // No hierarchical iteration for specific contexts in this implementation. The expander
309
+ // asks every concept for its children (includeCodeAndDescendants), so this has to be an
310
+ // iterator with no rows - not one with an empty query, which sqlite rejects
311
+ return new RxNormIteratorContext(null, {});
295
312
  }
296
313
  }
297
314
 
@@ -10,6 +10,7 @@
10
10
  <meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
11
11
 
12
12
  <link rel="stylesheet" href="/fhir.css"/>
13
+ [%api-head%]
13
14
 
14
15
  <!-- Bootstrap core CSS -->
15
16
  <link rel="stylesheet" href="/assets/css/bootstrap.css"/>
@@ -91,6 +92,7 @@
91
92
  <a href="[%endpoint-path%]/op.html" style="color: gold">Operations</a> &nbsp;|&nbsp;
92
93
  <a href="[%endpoint-path%]/ecl" style="color: gold">ECL</a> &nbsp;|&nbsp;
93
94
  <a href="[%endpoint-path%]/problems.html" style="color: gold">Problems</a> &nbsp;|&nbsp;
95
+ [%api-link%]
94
96
  [%library-link%]
95
97
  &nbsp;
96
98
  </div>
@@ -1,4 +1,45 @@
1
1
  const {VersionUtilities, VersionPrecision} = require("../../library/version-utilities");
2
+ const {Issue} = require("./operation-outcome");
3
+
4
+ /**
5
+ * Contained resources. The terminology server supports exactly one use of them: a ValueSet
6
+ * that contains ValueSets, which its compose refers to as #id. Any other contained resource
7
+ * - a contained CodeSystem or ConceptMap, anything contained in a CodeSystem or ConceptMap,
8
+ * or a contained ValueSet that itself contains something - is rejected, whatever path the
9
+ * resource arrives by (request, tx-resource, cache, package). Supporting them would mean
10
+ * resolving references to them everywhere, and the terminology ecosystem only requires
11
+ * servers to support contained value sets in value sets.
12
+ *
13
+ * @param {Object} jsonObj - the resource (R5 form)
14
+ * @throws {Issue} if the resource contains anything not supported
15
+ */
16
+ function checkContained(jsonObj) {
17
+ const contained = jsonObj ? jsonObj.contained : undefined;
18
+ if (contained === undefined || contained === null) {
19
+ return;
20
+ }
21
+ const type = jsonObj.resourceType;
22
+ const which = `${type}${jsonObj.id ? '/' + jsonObj.id : ''}${jsonObj.url ? ' (' + jsonObj.url + ')' : ''}`;
23
+ if (!Array.isArray(contained)) {
24
+ throw new Issue('error', 'structure', `${type}.contained`, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
25
+ `${which}: contained must be an array`, 'invalid-data', 400);
26
+ }
27
+ contained.forEach((c, i) => {
28
+ const ct = c && typeof c === 'object' ? c.resourceType : undefined;
29
+ let problem = null;
30
+ if (type !== 'ValueSet') {
31
+ problem = `${which} contains a ${ct || 'resource'}: this server only supports ValueSets contained in a ValueSet`;
32
+ } else if (ct !== 'ValueSet') {
33
+ problem = `${which} contains a ${ct || 'resource with no resourceType'}: this server only supports ValueSets contained in a ValueSet`;
34
+ } else if (Array.isArray(c.contained) && c.contained.length > 0) {
35
+ problem = `${which}: the contained ValueSet${c.id ? ' #' + c.id : ''} itself contains resources, which is not allowed`;
36
+ }
37
+ if (problem) {
38
+ throw new Issue('error', 'not-supported', `${type}.contained[${i}]`, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
39
+ problem, 'not-supported', 400);
40
+ }
41
+ });
42
+ }
2
43
 
3
44
  /**
4
45
  * Base class for metadata resources to provide common interface
@@ -31,6 +72,14 @@ class CanonicalResource {
31
72
  this.fhirVersion = fhirVersion;
32
73
  }
33
74
 
75
+ /**
76
+ * Rejects contained resources this server doesn't support - see checkContained.
77
+ * Subclasses call this once the resource is in R5 form.
78
+ */
79
+ checkContained() {
80
+ checkContained(this.jsonObj);
81
+ }
82
+
34
83
  get resourceType() {
35
84
  return this.jsonObj.resourceType;
36
85
  }
@@ -163,4 +212,4 @@ class CanonicalResource {
163
212
  }
164
213
  }
165
214
 
166
- module.exports = { CanonicalResource };
215
+ module.exports = { CanonicalResource, checkContained };
@@ -55,6 +55,7 @@ class CodeSystem extends CanonicalResource {
55
55
  if (e.issueCode) { wrapped.issueCode = e.issueCode; }
56
56
  throw wrapped;
57
57
  }
58
+ this.checkContained();
58
59
  if (!noMaps) {
59
60
  this.buildMaps();
60
61
  }
@@ -19,6 +19,7 @@ class ConceptMap extends CanonicalResource {
19
19
  // Convert to R5 format internally (modifies input for performance)
20
20
  this.jsonObj = conceptMapToR5(jsonObj, fhirVersion);
21
21
  this.validate();
22
+ this.checkContained();
22
23
  this.id = this.jsonObj.id;
23
24
  // Precalculated at construction so callers (e.g. the resource cache) have a
24
25
  // cheap O(1) sense of how large this resource is.
@@ -0,0 +1,107 @@
1
+ //
2
+ // Resource ids in the server's id spaces
3
+ //
4
+ // Every CodeSystem, ValueSet and ConceptMap the server serves by id lives in the id space of
5
+ // the provider that loaded it, and its id is the provider's prefix, a '-', then the id the
6
+ // resource had in its source: "tho-v3-ActCode", "core-v3-MaritalStatus". The prefix is the
7
+ // provider's spaceId: given in the library YAML (npm:hl7.terminology=tho), autonumbered by
8
+ // the library when the YAML doesn't give one, and "core" for the FHIR core package each
9
+ // endpoint loads for itself.
10
+ //
11
+ // No resource is served under its original id. Two sources can carry resources with the same
12
+ // id (hl7.terminology and hl7.fhir.r4.core both have a CodeSystem v3-MaritalStatus), so an
13
+ // unprefixed id can't say which one it means - and anything that looks a resource up by its
14
+ // raw id must fail, rather than find whichever one happens to come first.
15
+ //
16
+
17
+ // resources whose id has been prefixed - so prefixing is idempotent, and a raw id that
18
+ // happens to start with the prefix still gets one
19
+ const prefixed = new WeakSet();
20
+
21
+ /**
22
+ * Prefix a resource's id with spaceId. The resource is either a wrapper with a jsonObj
23
+ * (CodeSystem, ValueSet, ConceptMap) or a plain json object. Does nothing to a resource
24
+ * that has already been prefixed, or that has no id.
25
+ *
26
+ * @param {Object} resource
27
+ * @param {string} spaceId
28
+ */
29
+ function applyIdPrefix(resource, spaceId) {
30
+ if (!resource || !spaceId || prefixed.has(resource)) {
31
+ return;
32
+ }
33
+ const json = resource.jsonObj || resource;
34
+ if (!json.id) {
35
+ return;
36
+ }
37
+ const id = `${spaceId}-${json.id}`;
38
+ json.id = id;
39
+ if (resource.jsonObj && Object.prototype.hasOwnProperty.call(resource, 'id')) {
40
+ resource.id = id;
41
+ }
42
+ prefixed.add(resource);
43
+ }
44
+
45
+ /**
46
+ * The id a resource had in its source, for an id served in spaceId - or null, if the id
47
+ * isn't in that space (including when it's a raw id, or there's no spaceId).
48
+ *
49
+ * @param {string} id
50
+ * @param {string} spaceId
51
+ * @returns {string|null}
52
+ */
53
+ function stripIdPrefix(id, spaceId) {
54
+ if (!spaceId || typeof id !== 'string') {
55
+ return null;
56
+ }
57
+ const prefix = `${spaceId}-`;
58
+ return id.startsWith(prefix) && id.length > prefix.length ? id.substring(prefix.length) : null;
59
+ }
60
+
61
+ /**
62
+ * Whether a prefix given in the library YAML is acceptable. It's the start of an id, so it
63
+ * may only use id characters, and no '-', so that the prefix of any id is unambiguous.
64
+ * 'core' is the endpoints' own core package, and 'x' is the code system factories
65
+ * (CodeSystem/x-...), so neither is available.
66
+ *
67
+ * @param {string} prefix
68
+ * @returns {string|null} why the prefix isn't acceptable, or null if it is
69
+ */
70
+ function checkIdPrefix(prefix) {
71
+ if (!/^[A-Za-z0-9.]{1,32}$/.test(prefix)) {
72
+ return `an id prefix may only contain letters, digits and '.' (up to 32 characters)`;
73
+ }
74
+ if (prefix === CORE_PREFIX || prefix === 'x') {
75
+ return `the id prefix '${prefix}' is reserved`;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Put a provider in an id space - the shared part of the providers' assignIds(spaceId).
82
+ * A provider is put in one id space, once: its resources are prefixed when it is, so
83
+ * moving it to another would leave them in the old one. Asking again for the same space
84
+ * does nothing.
85
+ *
86
+ * @param {Object} provider - a code system, value set or concept map provider
87
+ * @param {string} spaceId
88
+ * @returns {boolean} true if the provider has just been put in the space, and must now
89
+ * prefix its resources' ids
90
+ */
91
+ function assignIdSpace(provider, spaceId) {
92
+ if (!spaceId) {
93
+ throw new Error(`${provider.constructor.name}: no id space given`);
94
+ }
95
+ if (provider.spaceId) {
96
+ if (provider.spaceId === spaceId) {
97
+ return false;
98
+ }
99
+ throw new Error(`${provider.constructor.name} is already in the id space '${provider.spaceId}', so it can't be put in '${spaceId}'`);
100
+ }
101
+ provider.spaceId = spaceId;
102
+ return true;
103
+ }
104
+
105
+ const CORE_PREFIX = 'core';
106
+
107
+ module.exports = { applyIdPrefix, stripIdPrefix, checkIdPrefix, assignIdSpace, CORE_PREFIX };
@@ -25,12 +25,28 @@ class ValueSet extends CanonicalResource {
25
25
  // Convert to R5 format internally (modifies input for performance)
26
26
  this.jsonObj = valueSetToR5(jsonObj, fhirVersion);
27
27
  this.validate();
28
+ this.checkContained();
28
29
  this.buildMaps();
29
30
  // Precalculated at construction so callers (e.g. the resource cache) have a
30
31
  // cheap O(1) sense of how large this resource is.
31
32
  this._conceptCount = ValueSet._countConcepts(this.jsonObj, this._containsCount);
32
33
  }
33
34
 
35
+ /**
36
+ * What circular reference detection knows this value set by: its versioned url, or, for
37
+ * a contained value set (see TerminologyWorker.findValueSet), its container's key plus
38
+ * #id - a contained value set's own url, if it has one, says nothing about which
39
+ * container it came from. null for a value set with no url that isn't contained.
40
+ * @returns {string|null}
41
+ */
42
+ get contextKey() {
43
+ if (this.isContained && this.container) {
44
+ const base = this.container.contextKey || this.container.vurl || '(unidentified)';
45
+ return `${base}#${this.jsonObj.id || ''}`;
46
+ }
47
+ return this.vurl || null;
48
+ }
49
+
34
50
  /**
35
51
  * Number of concepts this ValueSet carries: the enumerated concepts in its
36
52
  * compose (include + exclude `concept` lists) plus any concepts in an inline