fhirsmith 0.9.6 → 0.10.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 +51 -0
- package/library/folder-content-loader.js +91 -0
- package/npmprojector/npmprojector.js +2 -6
- package/package.json +1 -1
- package/packages/package-crawler.js +123 -6
- package/packages/packages.js +104 -0
- package/publisher/publisher.js +290 -12
- package/registry/crawler.js +85 -6
- package/registry/registry.js +24 -7
- package/server.js +11 -2
- package/stats.js +26 -18
- package/translations/Messages.properties +2 -1
- package/tx/cs/cs-cs.js +8 -0
- package/tx/cs/cs-loinc.js +1 -0
- package/tx/cs/cs-provider-list.js +2 -1
- package/tx/cs/cs-snomed.js +142 -59
- package/tx/data/snomed-testing.cache +0 -0
- package/tx/html/home-metrics.liquid +10 -10
- package/tx/library/canonical-resource.js +4 -2
- package/tx/library/codesystem.js +31 -0
- package/tx/library/conceptmap.js +24 -0
- package/tx/library/designations.js +27 -20
- package/tx/library/renderer.js +303 -22
- package/tx/library/ucum-types.js +4 -1
- package/tx/library/valueset.js +46 -0
- package/tx/library.js +65 -21
- package/tx/operation-context.js +122 -27
- package/tx/params.js +36 -8
- package/tx/provider.js +6 -3
- package/tx/tx-html.js +34 -0
- package/tx/tx.js +92 -30
- package/tx/vs/vs-vsac.js +157 -9
- package/tx/workers/cache-control.js +186 -0
- package/tx/workers/{related.js → compare.js} +27 -27
- package/tx/workers/expand.js +100 -96
- package/tx/workers/lookup.js +6 -0
- package/tx/workers/metadata.js +11 -6
- package/tx/workers/read.js +1 -1
- package/tx/workers/translate.js +20 -29
- package/tx/workers/validate.js +18 -10
- package/tx/workers/worker.js +134 -47
- package/tx/xversion/xv-bundle.js +1 -2
- package/tx/xversion/xv-codesystem.js +5 -2
- package/tx/xversion/xv-parameters.js +4 -4
- package/tx/xversion/xv-resource.js +2 -2
- package/tx/xversion/xv-terminologyCapabilities.js +11 -6
- package/tx/xversion/xv-valueset.js +7 -7
- package/publisher/task-draft.js +0 -463
- package/tx/data/OperationDefinition-ValueSet-related.json +0 -133
package/tx/library.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const fs = require('fs').promises;
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const yaml = require('yaml'); // npm install yaml
|
|
4
5
|
const { PackageManager, PackageContentLoader } = require('../library/package-manager');
|
|
6
|
+
const { FolderContentLoader } = require('../library/folder-content-loader');
|
|
5
7
|
const { CodeSystem } = require("./library/codesystem");
|
|
6
8
|
const {CountryCodeFactoryProvider} = require("./cs/cs-country");
|
|
7
9
|
const {Iso4217FactoryProvider} = require("./cs/cs-currency");
|
|
@@ -295,6 +297,10 @@ class Library {
|
|
|
295
297
|
await this.loadOcl(details, isDefault, mode);
|
|
296
298
|
break;
|
|
297
299
|
|
|
300
|
+
case 'folder':
|
|
301
|
+
await this.loadFolder(details, isDefault, mode);
|
|
302
|
+
break;
|
|
303
|
+
|
|
298
304
|
default:
|
|
299
305
|
throw new Error(`Unknown source type: ${type}`);
|
|
300
306
|
}
|
|
@@ -598,6 +604,23 @@ class Library {
|
|
|
598
604
|
version = parts[1];
|
|
599
605
|
}
|
|
600
606
|
const packagePath = await packageManager.fetch(packageId, version);
|
|
607
|
+
await this.#loadPackageFromPath(packagePath, mode, csOnly);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async loadUrl(packageManager, url, isDefault, mode, csOnly) {
|
|
611
|
+
const packagePath = await packageManager.fetchUrl(url);
|
|
612
|
+
await this.#loadPackageFromPath(packagePath, mode, csOnly);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Shared loader for npm- and url-sourced packages. Given a fetched package
|
|
617
|
+
* path, loads its CodeSystems (and, unless csOnly, its ValueSets and
|
|
618
|
+
* ConceptMaps) into this library. Factored out of loadNpm/loadUrl so the two
|
|
619
|
+
* cannot drift apart (a past divergence here used Map-style .set() on the
|
|
620
|
+
* array-backed codeSystems list, breaking url sources).
|
|
621
|
+
* @private
|
|
622
|
+
*/
|
|
623
|
+
async #loadPackageFromPath(packagePath, mode, csOnly) {
|
|
601
624
|
if (mode === "fetch" || mode === "cs") {
|
|
602
625
|
return;
|
|
603
626
|
}
|
|
@@ -634,43 +657,64 @@ class Library {
|
|
|
634
657
|
this.#logPackage(contentLoader.id(), contentLoader.version(), csc, vs ? vs.valueSetMap.size : 0);
|
|
635
658
|
}
|
|
636
659
|
|
|
637
|
-
|
|
638
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Loads CodeSystem / ValueSet / ConceptMap resources from any *.json file in a folder.
|
|
662
|
+
* The folder is scanned (top level only); each JSON file is read and routed to the
|
|
663
|
+
* appropriate provider based on its resourceType. Files that fail to parse or whose
|
|
664
|
+
* resourceType isn't one of the three terminology types are silently skipped.
|
|
665
|
+
*
|
|
666
|
+
* Relative paths are resolved against the project root (same convention as loadUcum etc.).
|
|
667
|
+
*
|
|
668
|
+
* @param {string} details - The folder to scan
|
|
669
|
+
* @param {boolean} isDefault - Unused; folder sources don't register factories
|
|
670
|
+
* @param {string} mode - One of "fetch", "cs", "npm"
|
|
671
|
+
*/
|
|
672
|
+
// eslint-disable-next-line no-unused-vars
|
|
673
|
+
async loadFolder(details, isDefault, mode) {
|
|
639
674
|
if (mode === "fetch" || mode === "cs") {
|
|
640
675
|
return;
|
|
641
676
|
}
|
|
642
|
-
|
|
643
|
-
const
|
|
677
|
+
|
|
678
|
+
const folderPath = path.isAbsolute(details)
|
|
679
|
+
? details
|
|
680
|
+
: path.resolve(path.join(__dirname, '..', details));
|
|
681
|
+
|
|
682
|
+
// Park the Package*Provider SQLite caches under the terminology cache rather
|
|
683
|
+
// than polluting the user's source folder, and wipe between runs so edits to
|
|
684
|
+
// the source folder are reliably picked up.
|
|
685
|
+
const hash = crypto.createHash('sha1').update(folderPath).digest('hex').substring(0, 16);
|
|
686
|
+
const cacheSubdir = path.join(this.cacheFolder, 'folder-source-' + hash);
|
|
687
|
+
await fs.rm(cacheSubdir, { recursive: true, force: true });
|
|
688
|
+
await fs.mkdir(cacheSubdir, { recursive: true });
|
|
689
|
+
|
|
690
|
+
const contentLoader = new FolderContentLoader(folderPath, cacheSubdir);
|
|
644
691
|
await contentLoader.initialize();
|
|
645
692
|
|
|
646
|
-
this.packageSources.push(contentLoader.id()+"#"+contentLoader.version());
|
|
693
|
+
this.packageSources.push(contentLoader.id() + "#" + contentLoader.version());
|
|
647
694
|
|
|
648
|
-
|
|
649
|
-
const
|
|
695
|
+
const cp = new ListCodeSystemProvider();
|
|
696
|
+
const csEntries = await contentLoader.getResourcesByType("CodeSystem");
|
|
650
697
|
let csc = 0;
|
|
651
|
-
for (const
|
|
652
|
-
const cs = new CodeSystem(await contentLoader.loadFile(
|
|
698
|
+
for (const entry of csEntries) {
|
|
699
|
+
const cs = new CodeSystem(await contentLoader.loadFile(entry, contentLoader.fhirVersion()));
|
|
653
700
|
if (this.#isIgnored(cs.url, cs.version)) {
|
|
654
701
|
this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
|
|
655
702
|
continue;
|
|
656
703
|
}
|
|
657
704
|
cs.sourcePackage = contentLoader.pid();
|
|
658
|
-
cp.codeSystems.
|
|
659
|
-
cp.codeSystems.set(cs.vurl, cs);
|
|
705
|
+
cp.codeSystems.push(cs);
|
|
660
706
|
csc++;
|
|
661
707
|
}
|
|
662
708
|
this.codeSystemProviders.push(cp);
|
|
663
|
-
let vs = null;
|
|
664
|
-
if (!csOnly) {
|
|
665
|
-
vs = new PackageValueSetProvider(contentLoader);
|
|
666
|
-
await vs.initialize();
|
|
667
|
-
this.valueSetProviders.push(vs);
|
|
668
|
-
const cm = new PackageConceptMapProvider(contentLoader);
|
|
669
|
-
await cm.initialize();
|
|
670
|
-
this.conceptMapProviders.push(cm);
|
|
671
|
-
}
|
|
672
709
|
|
|
673
|
-
|
|
710
|
+
const vs = new PackageValueSetProvider(contentLoader);
|
|
711
|
+
await vs.initialize();
|
|
712
|
+
this.valueSetProviders.push(vs);
|
|
713
|
+
const cm = new PackageConceptMapProvider(contentLoader);
|
|
714
|
+
await cm.initialize();
|
|
715
|
+
this.conceptMapProviders.push(cm);
|
|
716
|
+
|
|
717
|
+
this.#logPackage(contentLoader.id(), contentLoader.version(), csc, vs.valueSetMap.size);
|
|
674
718
|
}
|
|
675
719
|
|
|
676
720
|
/**
|
package/tx/operation-context.js
CHANGED
|
@@ -3,6 +3,7 @@ const inspector = require("inspector");
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const {Languages} = require("../library/languages");
|
|
5
5
|
const {Issue} = require("./library/operation-outcome");
|
|
6
|
+
const Logger = require("../library/logger");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Check if running under a debugger
|
|
@@ -59,6 +60,37 @@ class ResourceCache {
|
|
|
59
60
|
this.stats = stats;
|
|
60
61
|
this.cache = new Map();
|
|
61
62
|
this.locks = new Map(); // For thread-safety with async operations
|
|
63
|
+
this.log = Logger.getInstance().child({module: 'tx-cache'});
|
|
64
|
+
// Running total of concepts across every resource in every entry, maintained
|
|
65
|
+
// incrementally on each mutation so cache sizing/limits are O(1) to consult.
|
|
66
|
+
// Each entry also carries its own `concepts` subtotal so removal/replacement
|
|
67
|
+
// can adjust the total without rescanning.
|
|
68
|
+
this.totalConcepts = 0;
|
|
69
|
+
// High-water marks (never decremented) - the most caches and the most concepts
|
|
70
|
+
// this cache has held at once, for capacity reporting.
|
|
71
|
+
this.maxConcepts = 0;
|
|
72
|
+
this.maxSizeValue = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update the high-water marks after a mutation that may have grown the cache.
|
|
77
|
+
*/
|
|
78
|
+
_trackMax() {
|
|
79
|
+
if (this.totalConcepts > this.maxConcepts) {
|
|
80
|
+
this.maxConcepts = this.totalConcepts;
|
|
81
|
+
}
|
|
82
|
+
if (this.cache.size > this.maxSizeValue) {
|
|
83
|
+
this.maxSizeValue = this.cache.size;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Concept count of a single cached resource, or 0 if it doesn't expose one.
|
|
89
|
+
* @param {Object} resource
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
_conceptsOf(resource) {
|
|
93
|
+
return resource && typeof resource.conceptCount === 'function' ? resource.conceptCount() : 0;
|
|
62
94
|
}
|
|
63
95
|
|
|
64
96
|
/**
|
|
@@ -70,8 +102,10 @@ class ResourceCache {
|
|
|
70
102
|
const entry = this.cache.get(cacheId);
|
|
71
103
|
if (entry) {
|
|
72
104
|
entry.lastUsed = Date.now();
|
|
105
|
+
this.log.info(`cache-id '${cacheId}': hit, returning ${entry.resources.length} resource(s): ${entry.resources.map(r => this._resourceKey(r)).join(', ')}`);
|
|
73
106
|
return [...entry.resources]; // Return a copy
|
|
74
107
|
}
|
|
108
|
+
this.log.info(`cache-id '${cacheId}': miss (no entry)`);
|
|
75
109
|
return [];
|
|
76
110
|
}
|
|
77
111
|
|
|
@@ -92,22 +126,32 @@ class ResourceCache {
|
|
|
92
126
|
add(cacheId, resources) {
|
|
93
127
|
if (!resources || resources.length === 0) return;
|
|
94
128
|
|
|
95
|
-
const entry = this.cache.get(cacheId) || { resources: [], lastUsed: Date.now() };
|
|
129
|
+
const entry = this.cache.get(cacheId) || { resources: [], lastUsed: Date.now(), concepts: 0 };
|
|
96
130
|
|
|
97
|
-
// Merge resources, avoiding duplicates by url+version
|
|
131
|
+
// Merge resources, avoiding duplicates by url+version. Keep the entry's concept
|
|
132
|
+
// subtotal and the cache-wide total in step with each insertion/replacement.
|
|
98
133
|
for (const resource of resources) {
|
|
99
134
|
const key = this._resourceKey(resource);
|
|
135
|
+
const newConcepts = this._conceptsOf(resource);
|
|
100
136
|
const existingIndex = entry.resources.findIndex(r => this._resourceKey(r) === key);
|
|
101
137
|
if (existingIndex >= 0) {
|
|
102
|
-
// Replace existing
|
|
138
|
+
// Replace existing: adjust by the difference
|
|
139
|
+
const delta = newConcepts - this._conceptsOf(entry.resources[existingIndex]);
|
|
103
140
|
entry.resources[existingIndex] = resource;
|
|
141
|
+
entry.concepts += delta;
|
|
142
|
+
this.totalConcepts += delta;
|
|
143
|
+
this.log.info(`cache-id '${cacheId}': replaced ${key}`);
|
|
104
144
|
} else {
|
|
105
145
|
entry.resources.push(resource);
|
|
146
|
+
entry.concepts += newConcepts;
|
|
147
|
+
this.totalConcepts += newConcepts;
|
|
148
|
+
this.log.info(`cache-id '${cacheId}': added ${key}`);
|
|
106
149
|
}
|
|
107
150
|
}
|
|
108
151
|
|
|
109
152
|
entry.lastUsed = Date.now();
|
|
110
153
|
this.cache.set(cacheId, entry);
|
|
154
|
+
this._trackMax();
|
|
111
155
|
}
|
|
112
156
|
|
|
113
157
|
/**
|
|
@@ -116,10 +160,23 @@ class ResourceCache {
|
|
|
116
160
|
* @param {Array} resources - Resources to set
|
|
117
161
|
*/
|
|
118
162
|
set(cacheId, resources) {
|
|
163
|
+
this.log.info(`cache-id '${cacheId}': set (replace all) with ${resources.length} resource(s): ${resources.map(r => this._resourceKey(r)).join(', ')}`);
|
|
164
|
+
// Drop the old entry's contribution, then count the replacement.
|
|
165
|
+
const existing = this.cache.get(cacheId);
|
|
166
|
+
if (existing) {
|
|
167
|
+
this.totalConcepts -= existing.concepts || 0;
|
|
168
|
+
}
|
|
169
|
+
let concepts = 0;
|
|
170
|
+
for (const resource of resources) {
|
|
171
|
+
concepts += this._conceptsOf(resource);
|
|
172
|
+
}
|
|
173
|
+
this.totalConcepts += concepts;
|
|
119
174
|
this.cache.set(cacheId, {
|
|
120
175
|
resources: [...resources],
|
|
121
|
-
lastUsed: Date.now()
|
|
176
|
+
lastUsed: Date.now(),
|
|
177
|
+
concepts
|
|
122
178
|
});
|
|
179
|
+
this._trackMax();
|
|
123
180
|
}
|
|
124
181
|
|
|
125
182
|
/**
|
|
@@ -127,6 +184,10 @@ class ResourceCache {
|
|
|
127
184
|
* @param {string} cacheId - The cache identifier
|
|
128
185
|
*/
|
|
129
186
|
clear(cacheId) {
|
|
187
|
+
const entry = this.cache.get(cacheId);
|
|
188
|
+
if (entry) {
|
|
189
|
+
this.totalConcepts -= entry.concepts || 0;
|
|
190
|
+
}
|
|
130
191
|
this.cache.delete(cacheId);
|
|
131
192
|
}
|
|
132
193
|
|
|
@@ -135,6 +196,7 @@ class ResourceCache {
|
|
|
135
196
|
*/
|
|
136
197
|
clearAll() {
|
|
137
198
|
this.cache.clear();
|
|
199
|
+
this.totalConcepts = 0;
|
|
138
200
|
}
|
|
139
201
|
|
|
140
202
|
/**
|
|
@@ -150,6 +212,7 @@ class ResourceCache {
|
|
|
150
212
|
for (const [cacheId, entry] of this.cache.entries()) {
|
|
151
213
|
if (now - entry.lastUsed > maxAge) {
|
|
152
214
|
i++;
|
|
215
|
+
this.totalConcepts -= entry.concepts || 0;
|
|
153
216
|
this.cache.delete(cacheId);
|
|
154
217
|
}
|
|
155
218
|
}
|
|
@@ -166,6 +229,41 @@ class ResourceCache {
|
|
|
166
229
|
return this.cache.size;
|
|
167
230
|
}
|
|
168
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Total number of concepts held across every entry in the cache. Maintained
|
|
234
|
+
* incrementally, so this is O(1).
|
|
235
|
+
* @returns {number}
|
|
236
|
+
*/
|
|
237
|
+
conceptCount() {
|
|
238
|
+
return this.totalConcepts;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Highest number of concepts this cache has held at once (high-water mark).
|
|
243
|
+
* @returns {number}
|
|
244
|
+
*/
|
|
245
|
+
maxConceptCount() {
|
|
246
|
+
return this.maxConcepts;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Highest number of caches (cache-ids) this cache has held at once.
|
|
251
|
+
* @returns {number}
|
|
252
|
+
*/
|
|
253
|
+
maxSize() {
|
|
254
|
+
return this.maxSizeValue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Number of concepts held under a single cache-id (0 if unknown).
|
|
259
|
+
* @param {string} cacheId
|
|
260
|
+
* @returns {number}
|
|
261
|
+
*/
|
|
262
|
+
conceptCountFor(cacheId) {
|
|
263
|
+
const entry = this.cache.get(cacheId);
|
|
264
|
+
return entry ? (entry.concepts || 0) : 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
169
267
|
/**
|
|
170
268
|
* Generate a key for a resource based on url and version
|
|
171
269
|
* @param {Object} resource - The resource
|
|
@@ -204,6 +302,10 @@ class ExpansionCache {
|
|
|
204
302
|
this.cache = new Map();
|
|
205
303
|
this.maxSize = maxSize;
|
|
206
304
|
this.memoryThresholdBytes = memoryThresholdMB * 1024 * 1024;
|
|
305
|
+
// When true, every expansion is cached regardless of how long it took
|
|
306
|
+
// (bypasses MIN_CACHE_TIME_MS). Used by the test runner to force the cache
|
|
307
|
+
// path so cache correctness (e.g. language in the key) is exercised.
|
|
308
|
+
this.forceCaching = false;
|
|
207
309
|
}
|
|
208
310
|
|
|
209
311
|
/**
|
|
@@ -284,8 +386,9 @@ class ExpansionCache {
|
|
|
284
386
|
* @returns {boolean} True if cached, false if duration too short
|
|
285
387
|
*/
|
|
286
388
|
set(key, expansion, durationMs) {
|
|
287
|
-
// Only cache if expansion took significant time
|
|
288
|
-
|
|
389
|
+
// Only cache if expansion took significant time, unless forceCaching is on
|
|
390
|
+
// (in which case everything is cached regardless of duration).
|
|
391
|
+
if (!this.forceCaching && durationMs < ExpansionCache.MIN_CACHE_TIME_MS) {
|
|
289
392
|
return false;
|
|
290
393
|
}
|
|
291
394
|
|
|
@@ -357,21 +460,6 @@ class ExpansionCache {
|
|
|
357
460
|
return false;
|
|
358
461
|
}
|
|
359
462
|
|
|
360
|
-
/**
|
|
361
|
-
* Force-store an expansion regardless of duration (for testing)
|
|
362
|
-
* @param {string} key - Hash key
|
|
363
|
-
* @param {Object} expansion - The expanded ValueSet
|
|
364
|
-
*/
|
|
365
|
-
forceSet(key, expansion) {
|
|
366
|
-
this.cache.set(key, {
|
|
367
|
-
expansion: expansion,
|
|
368
|
-
createdAt: Date.now(),
|
|
369
|
-
lastUsed: Date.now(),
|
|
370
|
-
durationMs: 0,
|
|
371
|
-
hitCount: 0
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
463
|
/**
|
|
376
464
|
* Clear a specific entry
|
|
377
465
|
* @param {string} key - Hash key
|
|
@@ -388,22 +476,22 @@ class ExpansionCache {
|
|
|
388
476
|
}
|
|
389
477
|
|
|
390
478
|
/**
|
|
391
|
-
* Get cache statistics
|
|
479
|
+
* Get cache statistics.
|
|
480
|
+
* NB: named getStats(), not stats() — the `stats` field (the ServerStats
|
|
481
|
+
* passed to the constructor) would shadow a method called `stats`, making it
|
|
482
|
+
* unreachable.
|
|
392
483
|
* @returns {Object} Stats object
|
|
393
484
|
*/
|
|
394
|
-
|
|
485
|
+
getStats() {
|
|
395
486
|
let totalHits = 0;
|
|
396
|
-
let totalDuration = 0;
|
|
397
487
|
for (const entry of this.cache.values()) {
|
|
398
488
|
totalHits += entry.hitCount;
|
|
399
|
-
totalDuration += entry.durationMs;
|
|
400
489
|
}
|
|
401
490
|
return {
|
|
402
491
|
size: this.cache.size,
|
|
403
492
|
maxSize: this.maxSize,
|
|
404
493
|
memoryThresholdMB: this.memoryThresholdBytes > 0 ? this.memoryThresholdBytes / (1024 * 1024) : 0,
|
|
405
|
-
totalHits
|
|
406
|
-
totalDurationSaved: totalHits > 0 ? totalDuration * totalHits : 0
|
|
494
|
+
totalHits
|
|
407
495
|
};
|
|
408
496
|
}
|
|
409
497
|
|
|
@@ -447,6 +535,12 @@ class OperationContext {
|
|
|
447
535
|
this.logEntries = [];
|
|
448
536
|
this.resourceCache = resourceCache;
|
|
449
537
|
this.expansionCache = expansionCache;
|
|
538
|
+
// Server-issued cache-id carried on the request (X-Cache-Id header). Set once
|
|
539
|
+
// per request from the header so every worker can consult it uniformly via
|
|
540
|
+
// setupAdditionalResources, regardless of how that worker assembles its
|
|
541
|
+
// Parameters (buildParameters, raw req.body, query/form). An explicit
|
|
542
|
+
// cache-id *parameter* still takes precedence over this.
|
|
543
|
+
this.cacheId = null;
|
|
450
544
|
this.debugging = isDebugging();
|
|
451
545
|
// Providers opened during this operation that need their underlying
|
|
452
546
|
// resources (sqlite connections, etc.) released when the operation ends.
|
|
@@ -480,6 +574,7 @@ class OperationContext {
|
|
|
480
574
|
newContext.timeTracker = this.timeTracker.link();
|
|
481
575
|
newContext.logEntries = [...this.logEntries];
|
|
482
576
|
newContext.debugging = this.debugging;
|
|
577
|
+
newContext.cacheId = this.cacheId;
|
|
483
578
|
newContext.usageTracker = this.usageTracker;
|
|
484
579
|
// Share the same provider-cleanup list so providers opened by the copy
|
|
485
580
|
// are released when the parent operation ends.
|
package/tx/params.js
CHANGED
|
@@ -53,6 +53,11 @@ class TxParameters {
|
|
|
53
53
|
|
|
54
54
|
this.FHTTPLanguages = null;
|
|
55
55
|
this.FDisplayLanguages = null;
|
|
56
|
+
// Whether languages were explicitly supplied by the request (vs a
|
|
57
|
+
// synthesised default). Consumed by hasHTTPLanguages/hasDisplayLanguages so
|
|
58
|
+
// the requested language folds into the expansion cache key.
|
|
59
|
+
this.FHasHTTPLanguages = false;
|
|
60
|
+
this.FHasDisplayLanguages = false;
|
|
56
61
|
this.FValueSetVersionRules = null;
|
|
57
62
|
this.FUid = '';
|
|
58
63
|
|
|
@@ -87,11 +92,15 @@ class TxParameters {
|
|
|
87
92
|
if (!params.parameter) {
|
|
88
93
|
return;
|
|
89
94
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
95
|
+
if (this.hasParam(params, "__Content-Language")) {
|
|
96
|
+
const lang = this.paramstr(params, "__Content-Language");
|
|
97
|
+
this.HTTPLanguages = Languages.fromAcceptLanguage(lang, this.languageDefinitions, !this.validating);
|
|
98
|
+
if (lang) this.FHasHTTPLanguages = true;
|
|
92
99
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
100
|
+
if (this.hasParam(params, "__Accept-Language")) {
|
|
101
|
+
const lang = this.paramstr(params, "__Accept-Language");
|
|
102
|
+
this.HTTPLanguages = Languages.fromAcceptLanguage(lang, this.languageDefinitions, !this.validating);
|
|
103
|
+
if (lang) this.FHasHTTPLanguages = true;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
for (let p of params.parameter) {
|
|
@@ -124,7 +133,9 @@ class TxParameters {
|
|
|
124
133
|
|
|
125
134
|
case 'displayLanguage': {
|
|
126
135
|
try {
|
|
127
|
-
|
|
136
|
+
const lang = getValuePrimitive(p);
|
|
137
|
+
this.DisplayLanguages = Languages.fromAcceptLanguage(lang, this.languageDefinitions, !this.validating);
|
|
138
|
+
if (lang) this.FHasDisplayLanguages = true;
|
|
128
139
|
} catch (error) {
|
|
129
140
|
throw new Issue("error", "processing", null, 'INVALID_DISPLAY_NAME', this.i18n.translate('INVALID_DISPLAY_NAME', this.HTTPLanguages, [getValuePrimitive(p)]), "invalid-display").handleAsOO(400);
|
|
130
141
|
}
|
|
@@ -139,7 +150,10 @@ class TxParameters {
|
|
|
139
150
|
break;
|
|
140
151
|
}
|
|
141
152
|
case 'no-cache': {
|
|
142
|
-
|
|
153
|
+
// Write FUid (the field the cache key reads via hashSource); writing
|
|
154
|
+
// `this.uid` was a no-op so no-cache=true never busted the cache.
|
|
155
|
+
// Accept both the string ('true') and boolean (valueBoolean) forms.
|
|
156
|
+
if (strToBool(getValuePrimitive(p), false)) this.FUid = crypto.randomUUID();
|
|
143
157
|
break;
|
|
144
158
|
}
|
|
145
159
|
case '_incomplete':
|
|
@@ -292,11 +306,11 @@ class TxParameters {
|
|
|
292
306
|
}
|
|
293
307
|
|
|
294
308
|
get hasHTTPLanguages() {
|
|
295
|
-
return this.
|
|
309
|
+
return this.FHasHTTPLanguages;
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
get hasDisplayLanguages() {
|
|
299
|
-
return this.
|
|
313
|
+
return this.FHasDisplayLanguages;
|
|
300
314
|
}
|
|
301
315
|
|
|
302
316
|
get hasDesignations() {
|
|
@@ -422,6 +436,7 @@ e
|
|
|
422
436
|
if (value) {
|
|
423
437
|
if (name === 'displayLanguage' && (!this.FDisplayLanguages || overwrite)) {
|
|
424
438
|
this.DisplayLanguages = Languages.fromAcceptLanguage(getValuePrimitive(value), this.languageDefinitions, !this.validating)
|
|
439
|
+
if (getValuePrimitive(value)) this.FHasDisplayLanguages = true;
|
|
425
440
|
}
|
|
426
441
|
|
|
427
442
|
if (name === 'designation') {
|
|
@@ -561,6 +576,17 @@ e
|
|
|
561
576
|
if (this.hasDesignations) {
|
|
562
577
|
s = s + this.FDesignations.join(',') + '|';
|
|
563
578
|
}
|
|
579
|
+
if (this.supplements && this.supplements.size > 0) {
|
|
580
|
+
// useSupplement changes the expansion result (and a bad supplement must
|
|
581
|
+
// error), so it must be part of the cache key. Sort for determinism.
|
|
582
|
+
s = s + '$' + [...this.supplements].sort().join(',') + '|';
|
|
583
|
+
}
|
|
584
|
+
// Further result-affecting parameters that were previously omitted from the
|
|
585
|
+
// key: the text filter (changes which codes expand), limited/incomplete
|
|
586
|
+
// expansion handling, whether abstract codes are included, and diagnostics.
|
|
587
|
+
// filter is free text, so JSON.stringify it to avoid delimiter collisions.
|
|
588
|
+
s = s + 'f:' + JSON.stringify(this.filter || '') + '|' +
|
|
589
|
+
b(this.limitedExpansion) + b(this.incompleteOK) + b(this.abstractOk) + b(this.diagnostics);
|
|
564
590
|
for (let t of this.FVersionRules) {
|
|
565
591
|
s = s + t.asString() + '|';
|
|
566
592
|
}
|
|
@@ -623,9 +649,11 @@ e
|
|
|
623
649
|
|
|
624
650
|
if (other.FHTTPLanguages) {
|
|
625
651
|
this.FHTTPLanguages = other.FHTTPLanguages;
|
|
652
|
+
this.FHasHTTPLanguages = this.FHasHTTPLanguages || other.FHasHTTPLanguages;
|
|
626
653
|
}
|
|
627
654
|
if (other.FDisplayLanguages) {
|
|
628
655
|
this.FDisplayLanguages = other.FDisplayLanguages;
|
|
656
|
+
this.FHasDisplayLanguages = this.FHasDisplayLanguages || other.FHasDisplayLanguages;
|
|
629
657
|
}
|
|
630
658
|
}
|
|
631
659
|
|
package/tx/provider.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const { CodeSystem } = require("./library/codesystem");
|
|
2
2
|
const {VersionUtilities} = require("../library/version-utilities");
|
|
3
3
|
const { FhirCodeSystemProvider} = require("./cs/cs-cs");
|
|
4
|
-
const {OperationContext
|
|
4
|
+
const {OperationContext} = require("./operation-context");
|
|
5
|
+
const {TerminologyError} = require("./library/errors");
|
|
5
6
|
const {validateParameter, validateOptionalParameter, validateArrayParameter} = require("../library/utilities");
|
|
6
7
|
const path = require("path");
|
|
7
8
|
const {PackageContentLoader} = require("../library/package-manager");
|
|
@@ -498,14 +499,16 @@ class Provider {
|
|
|
498
499
|
deleteCodeSystem(cs) {
|
|
499
500
|
this.codeSystems.delete(cs.vurl);
|
|
500
501
|
this.codeSystems.delete(cs.url);
|
|
502
|
+
// If other versions of the SAME url survive, re-point the unversioned [url]
|
|
503
|
+
// entry at the most-recent surviving version. Otherwise leave it deleted.
|
|
501
504
|
let existing = null;
|
|
502
505
|
for (let t of this.codeSystems.values()) {
|
|
503
|
-
if (!existing || t.isMoreRecent(existing)) {
|
|
506
|
+
if (t.url === cs.url && (!existing || t.isMoreRecent(existing))) {
|
|
504
507
|
existing = t;
|
|
505
508
|
}
|
|
506
509
|
}
|
|
507
510
|
if (existing) {
|
|
508
|
-
this.codeSystems.set(
|
|
511
|
+
this.codeSystems.set(existing.url, existing);
|
|
509
512
|
}
|
|
510
513
|
}
|
|
511
514
|
|
package/tx/tx-html.js
CHANGED
|
@@ -19,6 +19,7 @@ const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml"
|
|
|
19
19
|
const {ParametersXML} = require("./xml/parameters-xml");
|
|
20
20
|
const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
|
|
21
21
|
const {debugLog} = require("./operation-context");
|
|
22
|
+
const {InvalidError} = require("./library/errors");
|
|
22
23
|
|
|
23
24
|
const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
|
|
24
25
|
|
|
@@ -311,6 +312,12 @@ class TxHtmlRenderer {
|
|
|
311
312
|
return await this.buildHomePage(req);
|
|
312
313
|
} else {
|
|
313
314
|
try {
|
|
315
|
+
if (json === null || json === undefined || typeof json !== 'object' || Array.isArray(json)) {
|
|
316
|
+
throw new InvalidError(`Cannot render: expected a FHIR resource object but got ${json === null ? 'null' : (Array.isArray(json) ? 'an array' : typeof json)}`);
|
|
317
|
+
}
|
|
318
|
+
if (json.resourceType === undefined || json.resourceType === null || typeof json.resourceType !== 'string' || json.resourceType === '') {
|
|
319
|
+
throw new InvalidError(`Cannot render: resource has no resourceType (got ${json.resourceType === undefined ? 'undefined' : JSON.stringify(json.resourceType)})`);
|
|
320
|
+
}
|
|
314
321
|
const _fmt = req?.query?._format || req?.query?.format || req?.body?._format;
|
|
315
322
|
const op = req ? req.path.includes("$") : false;
|
|
316
323
|
const resourceType = json.resourceType;
|
|
@@ -464,6 +471,33 @@ class TxHtmlRenderer {
|
|
|
464
471
|
if (param.valueCode !== undefined) {
|
|
465
472
|
return `<code>${escape(param.valueCode)}</code>`;
|
|
466
473
|
}
|
|
474
|
+
if (param.valueId !== undefined) {
|
|
475
|
+
return escape(String(param.valueId));
|
|
476
|
+
}
|
|
477
|
+
if (param.valueOid !== undefined) {
|
|
478
|
+
return escape(String(param.valueOid));
|
|
479
|
+
}
|
|
480
|
+
if (param.valueUuid !== undefined) {
|
|
481
|
+
return escape(String(param.valueUuid));
|
|
482
|
+
}
|
|
483
|
+
if (param.valueMarkdown !== undefined) {
|
|
484
|
+
// Render markdown to HTML the same way the rest of the server does, using
|
|
485
|
+
// commonmark in safe mode (raw HTML in the markdown is escaped, so this is
|
|
486
|
+
// XSS-safe).
|
|
487
|
+
const commonmark = require('commonmark');
|
|
488
|
+
const reader = new commonmark.Parser();
|
|
489
|
+
const writer = new commonmark.HtmlRenderer({ safe: true });
|
|
490
|
+
return writer.render(reader.parse(String(param.valueMarkdown)));
|
|
491
|
+
}
|
|
492
|
+
if (param.valueInteger64 !== undefined) {
|
|
493
|
+
return escape(String(param.valueInteger64));
|
|
494
|
+
}
|
|
495
|
+
if (param.valuePositiveInt !== undefined) {
|
|
496
|
+
return escape(String(param.valuePositiveInt));
|
|
497
|
+
}
|
|
498
|
+
if (param.valueUnsignedInt !== undefined) {
|
|
499
|
+
return escape(String(param.valueUnsignedInt));
|
|
500
|
+
}
|
|
467
501
|
if (param.valueDate !== undefined) {
|
|
468
502
|
return escape(param.valueDate);
|
|
469
503
|
}
|