fhirsmith 0.14.2 → 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 +23 -0
- package/library/changelog.js +57 -0
- package/package.json +1 -1
- package/registry/crawler.js +17 -0
- package/registry/fhirsmith-releases.js +3 -6
- package/registry/model.js +3 -0
- package/registry/registry.js +21 -3
- package/tx/README.md +20 -4
- package/tx/cm/cm-api.js +25 -10
- package/tx/cm/cm-database.js +0 -4
- package/tx/cm/cm-package.js +53 -14
- package/tx/cs/cs-provider-api.js +22 -7
- package/tx/cs/cs-provider-list.js +10 -9
- package/tx/library/resource-ids.js +107 -0
- package/tx/library.js +96 -24
- 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/provider.js +80 -13
- package/tx/tx.fhir.org.yml +16 -8
- package/tx/tx.js +7 -1
- package/tx/vs/vs-api.js +25 -10
- package/tx/vs/vs-database.js +0 -4
- package/tx/vs/vs-package.js +48 -31
- package/tx/vs/vs-vsac.js +20 -5
- package/tx/workers/metadata.js +2 -1
- package/tx/workers/read.js +3 -18
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to Health Intersections FHIRsmith will be documented in this
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.15.0] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Library YAML: a source can give the id prefix for the resources it loads (`npm:hl7.terminology=tho`), so their ids stay the same when sources are added, removed or reordered. Sources without one are numbered, as before. See Resource Ids in tx/README.md
|
|
13
|
+
- `CapabilityStatement.software.releaseDate` (and the TerminologyCapabilities equivalent) is the date the running version was released, taken from its heading in CHANGELOG.md, which is now included in the Docker image. A snapshot reports the time the server started
|
|
14
|
+
- Registry: the crawler records each server's `software.releaseDate`. The Software page uses it to date software other than FHIRsmith, and FHIRsmith releases (including development builds) that the release list doesn't know about yet
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Breaking - resource ids:** every CodeSystem, ValueSet and ConceptMap is served with an id in the id space of its source - `prefix-id`, e.g. `CodeSystem/tho-v3-ActCode` - and the FHIR core package each endpoint loads uses `core` (`CodeSystem/core-administrative-gender`). An unprefixed id is not found by read or by any `[type]/[id]/$operation`. Library CodeSystems used to keep the ids they had in their packages (ValueSets and ConceptMaps were already numbered), and so could clash with the endpoint's core CodeSystems
|
|
19
|
+
- THO overrides the FHIR core packages the same way the Java validator does: where hl7.terminology has a url, the core package's CodeSystems, ValueSets and ConceptMaps with that url are not loaded at all. Older core versions used to stay available by `url|version`, so the server reported versions the validator doesn't know (e.g. v3-MaritalStatus `2018-08-12`). The exception is the R4 v2 tables 0006, 0360 and 0391, which R4 core has in two versions each with the version in the url: those versions stay available, and THO's is the default
|
|
20
|
+
- `software.releaseDate` is no longer the time of the request
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Search: two CodeSystems with the same id from different packages (e.g. v3-MaritalStatus from hl7.fhir.r4.core and hl7.terminology) linked to the same resource, and a read of that id always returned the core one
|
|
25
|
+
- On the R4 endpoint, a ValueSet asked for without a version (e.g. `http://terminology.hl7.org/ValueSet/v3-MaritalStatus`) was the core package's copy rather than hl7.terminology's
|
|
26
|
+
|
|
27
|
+
### Tx Conformance Statement
|
|
28
|
+
|
|
29
|
+
FHIRsmith v0.15.0 passed all 3585 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11+closure, tests v1.9.6, runner v7.0.0)
|
|
30
|
+
|
|
8
31
|
## [0.14.2] - 2026-10-06
|
|
9
32
|
|
|
10
33
|
### Security
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// library/changelog.js
|
|
2
|
+
// Release dates from CHANGELOG.md. A release is dated in its heading when it is made -
|
|
3
|
+
// "## [0.14.2] - 2026-10-06" (or "## [v0.13.4] - ...") - so the changelog that ships with
|
|
4
|
+
// the code is where a server finds out when its own version was released. A snapshot's
|
|
5
|
+
// heading has no date yet, so it has no release date.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PATH = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
11
|
+
const HEADING = /^##\s*\[v?(\d+\.\d+\.\d+)\]\s*-\s*(\d{4}-\d{2}-\d{2})\s*$/gm;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} [changelogPath]
|
|
15
|
+
* @returns {Map<string, string>} version (no leading v) -> release date (YYYY-MM-DD).
|
|
16
|
+
* Throws if the file can't be read.
|
|
17
|
+
*/
|
|
18
|
+
function readReleaseDates(changelogPath = DEFAULT_PATH) {
|
|
19
|
+
const text = fs.readFileSync(changelogPath, 'utf8');
|
|
20
|
+
const dates = new Map();
|
|
21
|
+
let m;
|
|
22
|
+
HEADING.lastIndex = 0;
|
|
23
|
+
while ((m = HEADING.exec(text)) !== null) {
|
|
24
|
+
if (!dates.has(m[1])) {
|
|
25
|
+
dates.set(m[1], m[2]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return dates;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The release date of a version, or null if it isn't a dated release (a snapshot, or no
|
|
33
|
+
* changelog to read)
|
|
34
|
+
*/
|
|
35
|
+
function releaseDateOf(version, changelogPath = DEFAULT_PATH) {
|
|
36
|
+
try {
|
|
37
|
+
return readReleaseDates(changelogPath).get(String(version || '').replace(/^v/, '')) || null;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The release date a server reports for its version: the date of the release, or, for a
|
|
45
|
+
* version that hasn't been released (a snapshot), the time the server started - which is
|
|
46
|
+
* when that build went into service.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} version
|
|
49
|
+
* @param {Date} started - when the server started
|
|
50
|
+
* @param {string} [changelogPath]
|
|
51
|
+
* @returns {string} YYYY-MM-DD for a release, an ISO dateTime for a snapshot
|
|
52
|
+
*/
|
|
53
|
+
function reportedReleaseDate(version, started, changelogPath = DEFAULT_PATH) {
|
|
54
|
+
return releaseDateOf(version, changelogPath) || started.toISOString();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { readReleaseDates, releaseDateOf, reportedReleaseDate };
|
package/package.json
CHANGED
package/registry/crawler.js
CHANGED
|
@@ -339,6 +339,20 @@ class RegistryCrawler {
|
|
|
339
339
|
return version;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* CapabilityStatement.software.releaseDate, if it means anything. A date within a few
|
|
344
|
+
* minutes of now is the server's clock, not a release date: FHIRsmith up to 0.14.2 filled
|
|
345
|
+
* it in with the time of the request, and other software may do the same.
|
|
346
|
+
*/
|
|
347
|
+
reportedReleaseDate(software, now = Date.now()) {
|
|
348
|
+
const value = software && software.releaseDate;
|
|
349
|
+
const t = value ? new Date(value).getTime() : NaN;
|
|
350
|
+
if (isNaN(t) || Math.abs(now - t) < 10 * 60 * 1000) {
|
|
351
|
+
return '';
|
|
352
|
+
}
|
|
353
|
+
return String(value);
|
|
354
|
+
}
|
|
355
|
+
|
|
342
356
|
/**
|
|
343
357
|
* A server that can't be reached this time is still running whatever it was running
|
|
344
358
|
* last time we saw it - keep that, so the software page doesn't lose track of it
|
|
@@ -353,6 +367,7 @@ class RegistryCrawler {
|
|
|
353
367
|
if (prev.address === version.address && prev.software) {
|
|
354
368
|
version.software = prev.software;
|
|
355
369
|
version.softwareVersion = prev.softwareVersion || '';
|
|
370
|
+
version.softwareReleaseDate = prev.softwareReleaseDate || '';
|
|
356
371
|
return;
|
|
357
372
|
}
|
|
358
373
|
}
|
|
@@ -371,6 +386,7 @@ class RegistryCrawler {
|
|
|
371
386
|
version.version = capability.fhirVersion || '3.0.2';
|
|
372
387
|
version.software = capability.software ? capability.software.name : "unknown";
|
|
373
388
|
version.softwareVersion = capability.software && capability.software.version ? String(capability.software.version) : '';
|
|
389
|
+
version.softwareReleaseDate = this.reportedReleaseDate(capability.software);
|
|
374
390
|
|
|
375
391
|
// Get terminology capabilities (R3 uses Parameters resource)
|
|
376
392
|
try {
|
|
@@ -416,6 +432,7 @@ class RegistryCrawler {
|
|
|
416
432
|
version.version = capability.fhirVersion || defVersion;
|
|
417
433
|
version.software = capability.software ? capability.software.name : "unknown";
|
|
418
434
|
version.softwareVersion = capability.software && capability.software.version ? String(capability.software.version) : '';
|
|
435
|
+
version.softwareReleaseDate = this.reportedReleaseDate(capability.software);
|
|
419
436
|
|
|
420
437
|
let set = new Set();
|
|
421
438
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// succeeds (or if it can't be reached), the dated headings in this server's own
|
|
7
7
|
// CHANGELOG.md are used instead - those only go up to this server's own version.
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const { readReleaseDates } = require('../library/changelog');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const axios = require('axios');
|
|
12
12
|
|
|
@@ -72,12 +72,9 @@ class FhirsmithReleases {
|
|
|
72
72
|
*/
|
|
73
73
|
loadFromChangelog(changelogPath = path.join(__dirname, '..', 'CHANGELOG.md')) {
|
|
74
74
|
try {
|
|
75
|
-
const text = fs.readFileSync(changelogPath, 'utf8');
|
|
76
75
|
const list = [];
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
while ((m = re.exec(text)) !== null) {
|
|
80
|
-
list.push({ version: m[1], date: new Date(m[2] + 'T00:00:00Z') });
|
|
76
|
+
for (const [version, date] of readReleaseDates(changelogPath)) {
|
|
77
|
+
list.push({ version, date: new Date(date + 'T00:00:00Z') });
|
|
81
78
|
}
|
|
82
79
|
this.setReleases(list, 'CHANGELOG.md');
|
|
83
80
|
} catch (error) {
|
package/registry/model.js
CHANGED
|
@@ -12,6 +12,7 @@ class ServerVersionInformation {
|
|
|
12
12
|
this.lastTat = '';
|
|
13
13
|
this.software = ''; // what software is running
|
|
14
14
|
this.softwareVersion = ''; // CapabilityStatement.software.version, if the server reports one
|
|
15
|
+
this.softwareReleaseDate = ''; // CapabilityStatement.software.releaseDate, if it is a real date
|
|
15
16
|
this.codeSystems = []; // Array of strings (sorted, unique)
|
|
16
17
|
this.valueSets = []; // Array of strings (sorted, unique)
|
|
17
18
|
}
|
|
@@ -60,6 +61,7 @@ class ServerVersionInformation {
|
|
|
60
61
|
terminologies: this.codeSystems,
|
|
61
62
|
software: this.software,
|
|
62
63
|
'software-version': this.softwareVersion,
|
|
64
|
+
'software-release-date': this.softwareReleaseDate,
|
|
63
65
|
valuesets: this.valueSets
|
|
64
66
|
};
|
|
65
67
|
}
|
|
@@ -74,6 +76,7 @@ class ServerVersionInformation {
|
|
|
74
76
|
instance.lastTat = json.lastTat || '';
|
|
75
77
|
instance.software = json.software;
|
|
76
78
|
instance.softwareVersion = json['software-version'] || '';
|
|
79
|
+
instance.softwareReleaseDate = json['software-release-date'] || '';
|
|
77
80
|
instance.codeSystems = json.terminologies || [];
|
|
78
81
|
instance.valueSets = json.valuesets || [];
|
|
79
82
|
return instance;
|
package/registry/registry.js
CHANGED
|
@@ -512,6 +512,7 @@ class RegistryModule {
|
|
|
512
512
|
fhirVersion: version.version || '',
|
|
513
513
|
software: version.software && version.software !== 'unknown' ? version.software : '',
|
|
514
514
|
softwareVersion: version.softwareVersion || '',
|
|
515
|
+
softwareReleaseDate: version.softwareReleaseDate || '',
|
|
515
516
|
error: version.error,
|
|
516
517
|
lastSuccess: version.lastSuccess
|
|
517
518
|
});
|
|
@@ -556,7 +557,7 @@ class RegistryModule {
|
|
|
556
557
|
|
|
557
558
|
_renderReleaseCells(row, now) {
|
|
558
559
|
if (!isFhirsmith(row.software) || !this.releases) {
|
|
559
|
-
return
|
|
560
|
+
return this._renderReportedReleaseCells(row, now);
|
|
560
561
|
}
|
|
561
562
|
const d = this.releases.describe(row.softwareVersion, now);
|
|
562
563
|
const behind = d.behind ? ` (${d.behind} release${d.behind === 1 ? '' : 's'} behind)` : '';
|
|
@@ -571,11 +572,28 @@ class RegistryModule {
|
|
|
571
572
|
`<td><span class="${cls}">${describeAge(d.ageDays)}${behind}</span></td>`;
|
|
572
573
|
}
|
|
573
574
|
case 'dev':
|
|
574
|
-
return
|
|
575
|
+
return `<td>${this._reportedReleaseDay(row)}</td><td>development build` +
|
|
575
576
|
(d.release ? ` after v${escape(d.release.version)}` : '') + behind + '</td>';
|
|
576
577
|
default:
|
|
577
|
-
return
|
|
578
|
+
return this._renderReportedReleaseCells(row, now);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// what the server says about its release (CapabilityStatement.software.releaseDate) -
|
|
583
|
+
// the only source for software other than FHIRsmith
|
|
584
|
+
_reportedReleaseDay(row) {
|
|
585
|
+
// the day as the server gives it - converting to UTC can move it a day
|
|
586
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.softwareReleaseDate || '');
|
|
587
|
+
return m ? m[1] : '';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_renderReportedReleaseCells(row, now) {
|
|
591
|
+
const day = this._reportedReleaseDay(row);
|
|
592
|
+
if (!day) {
|
|
593
|
+
return '<td></td><td></td>';
|
|
578
594
|
}
|
|
595
|
+
const days = Math.max(0, Math.floor((now - new Date(row.softwareReleaseDate).getTime()) / (24 * 60 * 60 * 1000)));
|
|
596
|
+
return `<td>${day}</td><td>${describeAge(days)}</td>`;
|
|
579
597
|
}
|
|
580
598
|
|
|
581
599
|
/**
|
package/tx/README.md
CHANGED
|
@@ -237,12 +237,28 @@ shouldn't be - too big. Note that that files in the list below are considered to
|
|
|
237
237
|
Each source entry follows the format:
|
|
238
238
|
|
|
239
239
|
```
|
|
240
|
-
type[!]:details
|
|
240
|
+
type[!]:details[=prefix]
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
- **type**: The source type (see Source Types below)
|
|
244
244
|
- **!** (optional): Append to type to mark this source as the default for its code system. When multiple versions of the same code system are loaded, the default is used when no version is specified.
|
|
245
245
|
- **details**: Type-specific configuration (filename, package name, or internal provider name)
|
|
246
|
+
- **prefix** (optional): The id prefix for the resources the source provides - see [Resource Ids](#resource-ids). Everything after the last `=` in the entry is the prefix, so a `url` source whose url contains a `=` must give one.
|
|
247
|
+
|
|
248
|
+
### Resource Ids
|
|
249
|
+
|
|
250
|
+
Every CodeSystem, ValueSet and ConceptMap is served with an id in the id space of the source it came from: the source's prefix, `-`, and the id the resource has in the source. With
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
- npm:hl7.terminology=tho
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
THO's v3-ActCode code system is `CodeSystem/tho-v3-ActCode`. No resource is served on the id it has in its source - different sources can use the same ids (hl7.terminology and the FHIR core packages both have a CodeSystem `v3-MaritalStatus`), so an unprefixed id can't say which one is meant, and it finds nothing.
|
|
257
|
+
|
|
258
|
+
- A prefix may contain letters, digits and `.` (no `-`), up to 32 characters, and each prefix may only be given once
|
|
259
|
+
- If a source isn't given a prefix, it gets a number. Numbers depend on the order of the sources (and on which sources provide which kinds of resource), so they change when the YAML changes. Give every source that provides resources a prefix if its ids should stay the same - i.e. on any server where people link to resources by id
|
|
260
|
+
- `core` is the FHIR core package each endpoint loads for its FHIR version (`CodeSystem/core-administrative-gender`), and `x` is the code systems the server implements natively (`CodeSystem/x-...`); neither can be given as a prefix
|
|
261
|
+
- Sources that only provide code systems natively (`internal`, `snomed`, `loinc` etc.) don't serve resources by id, so a prefix does nothing for them
|
|
246
262
|
|
|
247
263
|
### Source Types
|
|
248
264
|
|
|
@@ -464,9 +480,9 @@ sources:
|
|
|
464
480
|
- snomed:sct_us_20250901.cache
|
|
465
481
|
|
|
466
482
|
# FHIR packages
|
|
467
|
-
- npm:hl7.terminology
|
|
468
|
-
- npm:hl7.fhir.us.core
|
|
469
|
-
- npm:us.nlm.vsac
|
|
483
|
+
- npm:hl7.terminology=tho
|
|
484
|
+
- npm:hl7.fhir.us.core=us
|
|
485
|
+
- npm:us.nlm.vsac=vsac
|
|
470
486
|
```
|
|
471
487
|
|
|
472
488
|
Also see tx.fhir.org.yml for the production configuration for tx.fhir.org
|
package/tx/cm/cm-api.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
+
const { assignIdSpace } = require('../library/resource-ids');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Abstract base class for Concept Map providers
|
|
3
5
|
* Defines the interface that all Concept Map providers must implement
|
|
4
6
|
*/
|
|
5
7
|
class AbstractConceptMapProvider {
|
|
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 concept map this
|
|
11
|
+
* provider provides has an id that starts 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 concept maps 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 concept maps this provider provides with spaceId + '-', once spaceId
|
|
30
|
+
* is set (by assignIds). Concept maps the provider loads later must be given ids
|
|
31
|
+
* in the same space.
|
|
32
|
+
*/
|
|
33
|
+
prefixIds() {
|
|
34
|
+
throw new Error('prefixIds must be implemented by AbstractConceptMapProvider subclass');
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
/**
|
|
@@ -32,11 +47,11 @@ class AbstractConceptMapProvider {
|
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
/**
|
|
35
|
-
* Fetches a specific Concept Map by id.
|
|
36
|
-
*
|
|
50
|
+
* Fetches a specific Concept Map by id. The id is in this provider's space (spaceId + '-' +
|
|
51
|
+
* the concept map's own id); an id that isn't - including an unprefixed one - finds nothing
|
|
37
52
|
*
|
|
38
53
|
* @param {string} id - The id of the Concept Map
|
|
39
|
-
* @returns {Promise<ConceptMap>} The requested Concept Map
|
|
54
|
+
* @returns {Promise<ConceptMap>} The requested Concept Map, or null
|
|
40
55
|
* @throws {Error} Must be implemented by subclasses
|
|
41
56
|
*/
|
|
42
57
|
// eslint-disable-next-line no-unused-vars
|
package/tx/cm/cm-database.js
CHANGED
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
|
|
|
@@ -128,7 +132,9 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
128
132
|
this._validateSearchParams(searchParams);
|
|
129
133
|
|
|
130
134
|
if (this.USE_DATABASE_SEARCH) {
|
|
131
|
-
|
|
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)));
|
|
132
138
|
} else {
|
|
133
139
|
const matches = [];
|
|
134
140
|
const seen = new Set(); // Track by URL to avoid duplicates from versioned keys
|
|
@@ -194,9 +200,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
194
200
|
|
|
195
201
|
if (isMatch) {
|
|
196
202
|
seen.add(vsUrl);
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
matches.push(result);
|
|
203
|
+
// the id is already prefixed (see assignIds)
|
|
204
|
+
matches.push(json);
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -298,19 +303,53 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
298
303
|
}
|
|
299
304
|
|
|
300
305
|
async fetchConceptMapById(id) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
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();
|
|
309
347
|
}
|
|
310
348
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
349
|
+
#applyIdPrefix() {
|
|
350
|
+
for (const cm of new Set(this.conceptMapMap.values())) {
|
|
351
|
+
applyIdPrefix(cm, this.spaceId);
|
|
352
|
+
}
|
|
314
353
|
}
|
|
315
354
|
|
|
316
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
|
|
|
@@ -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 };
|