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/tx.fhir.org.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
* {
|
|
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
|
-
*
|
|
16
|
-
* in
|
|
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 {
|
|
23
|
+
* @param {string} spaceId
|
|
19
24
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
39
|
-
*
|
|
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
|
package/tx/vs/vs-database.js
CHANGED
|
@@ -743,7 +743,17 @@ class ValueSetDatabase {
|
|
|
743
743
|
const valueSetMap = new Map();
|
|
744
744
|
|
|
745
745
|
for (const row of rows) {
|
|
746
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
package/tx/vs/vs-package.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
}
|
package/tx/workers/expand.js
CHANGED
|
@@ -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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
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();
|
package/tx/workers/metadata.js
CHANGED
|
@@ -202,7 +202,8 @@ class MetadataHandler {
|
|
|
202
202
|
software: {
|
|
203
203
|
name: this.config.softwareName || 'FHIR Terminology Server',
|
|
204
204
|
version: serverVersion,
|
|
205
|
-
|
|
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
|
};
|
package/tx/workers/read.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
146
|
+
return res.json(cm.jsonObj);
|
|
162
147
|
}
|
|
163
148
|
}
|
|
164
149
|
|