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.
- package/CHANGELOG.md +58 -0
- package/README.md +3 -2
- package/config-template.json +9 -0
- package/library/changelog.js +57 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/utilities.js +21 -0
- package/package.json +10 -4
- package/packages/openapi.js +13 -0
- package/packages/openapi.yaml +603 -0
- package/packages/packages-template.html +4 -2
- package/packages/packages.js +286 -81
- package/packages/readme.md +120 -0
- package/registry/api.js +117 -41
- package/registry/crawler.js +41 -0
- package/registry/fhirsmith-releases.js +167 -0
- package/registry/model.js +13 -0
- package/registry/openapi.js +13 -0
- package/registry/openapi.yaml +311 -0
- package/registry/readme.md +107 -208
- package/registry/registry-template.html +4 -1
- package/registry/registry.js +198 -19
- package/server.js +19 -5
- package/testing/openapi-schemas.config.js +58 -0
- package/testing/openapi-schemas.json +1920 -0
- package/testing/openapi.js +15 -0
- package/testing/openapi.yaml +273 -0
- package/testing/readme.md +43 -1
- package/testing/search.js +1 -1
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +24 -0
- package/translations/Messages.properties +1 -0
- package/tx/README.md +78 -4
- package/tx/cm/cm-api.js +25 -10
- package/tx/cm/cm-database.js +10 -5
- package/tx/cm/cm-package.js +61 -15
- package/tx/cs/cs-provider-api.js +22 -7
- package/tx/cs/cs-provider-list.js +10 -9
- package/tx/cs/cs-rxnorm.js +23 -6
- package/tx/html/tx-template.html +2 -0
- package/tx/library/canonical-resource.js +50 -1
- package/tx/library/codesystem.js +1 -0
- package/tx/library/conceptmap.js +1 -0
- package/tx/library/resource-ids.js +107 -0
- package/tx/library/valueset.js +16 -0
- package/tx/library.js +113 -26
- package/tx/ocl/cm-ocl.cjs +23 -15
- package/tx/ocl/cs-ocl.cjs +9 -10
- package/tx/ocl/vs-ocl.cjs +17 -17
- package/tx/openapi-operations.js +363 -0
- package/tx/openapi-schemas.config.js +118 -0
- package/tx/openapi-schemas.json +7903 -0
- package/tx/openapi.js +289 -0
- package/tx/openapi.yaml +151 -0
- package/tx/operation-context.js +15 -0
- package/tx/params.js +3 -12
- package/tx/provider.js +90 -14
- package/tx/tx-html.js +6 -0
- package/tx/tx.fhir.org.yml +16 -8
- package/tx/tx.js +62 -1
- package/tx/vs/vs-api.js +25 -10
- package/tx/vs/vs-database.js +14 -9
- package/tx/vs/vs-package.js +58 -31
- package/tx/vs/vs-vsac.js +20 -5
- package/tx/workers/expand.js +35 -16
- package/tx/workers/metadata.js +2 -14
- package/tx/workers/read.js +3 -18
- package/tx/workers/validate.js +65 -19
- package/tx/workers/worker.js +29 -8
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- package/xig/xig.js +2 -4
package/tx/cm/cm-package.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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) {
|
package/tx/cs/cs-provider-api.js
CHANGED
|
@@ -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
|
-
* {
|
|
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
|
-
*
|
|
13
|
-
* in
|
|
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 {
|
|
20
|
+
* @param {string} spaceId
|
|
16
21
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
*
|
|
15
|
-
* in the
|
|
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
|
-
|
|
20
|
-
|
|
18
|
+
prefixIds() {
|
|
19
|
+
let i = 0;
|
|
21
20
|
for (const cs of this.codeSystems) {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
i++;
|
|
22
|
+
const json = cs.jsonObj || cs;
|
|
23
|
+
if (!json.id) {
|
|
24
|
+
json.id = String(i);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
applyIdPrefix(cs, this.spaceId);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
package/tx/cs/cs-rxnorm.js
CHANGED
|
@@ -36,12 +36,27 @@ class RxNormPrep {
|
|
|
36
36
|
|
|
37
37
|
// Iterator context
|
|
38
38
|
class RxNormIteratorContext {
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
package/tx/html/tx-template.html
CHANGED
|
@@ -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> |
|
|
92
93
|
<a href="[%endpoint-path%]/ecl" style="color: gold">ECL</a> |
|
|
93
94
|
<a href="[%endpoint-path%]/problems.html" style="color: gold">Problems</a> |
|
|
95
|
+
[%api-link%]
|
|
94
96
|
[%library-link%]
|
|
95
97
|
|
|
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 };
|
package/tx/library/codesystem.js
CHANGED
package/tx/library/conceptmap.js
CHANGED
|
@@ -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 };
|
package/tx/library/valueset.js
CHANGED
|
@@ -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
|