fhirsmith 0.10.0 → 0.11.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 +48 -1
- package/changelog-template.md +22 -0
- package/library/logger.js +8 -1
- package/library/package-manager.js +40 -2
- package/npmprojector/watcher.js +1 -1
- package/package.json +3 -2
- package/packages/packages.js +28 -15
- package/publisher/publisher.js +8 -8
- package/registry/api.js +92 -31
- package/registry/crawler.js +15 -1
- package/registry/model.js +158 -8
- package/registry/readme.md +5 -13
- package/registry/registry.js +87 -46
- package/registry/test-runner.js +1 -1
- package/server.js +8 -1
- package/token/token.js +1 -1
- package/translations/Messages.properties +3 -1
- package/translations/rendering-phrases.properties +1227 -1218
- package/translations/rendering-phrases_ar.properties +1097 -1091
- package/translations/rendering-phrases_fr.properties +626 -624
- package/translations/rendering-phrases_nl.properties +976 -970
- package/translations/rendering-phrases_pt.properties +1026 -1020
- package/translations/rendering-phrases_ru.properties +1100 -1094
- package/tsconfig.eslint.json +9 -0
- package/tx/cs/cs-api.js +57 -4
- package/tx/cs/cs-cs.js +98 -25
- package/tx/cs/cs-hgvs.js +4 -0
- package/tx/cs/cs-lang.js +8 -0
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-snomed.js +530 -75
- package/tx/cs/cs-uri.js +4 -0
- package/tx/data/snomed-subset.txt +40 -3
- package/tx/importers/import-sct.module.js +18 -0
- package/tx/library/designations.js +3 -5
- package/tx/library/renderer.js +235 -38
- package/tx/library.js +5 -1
- package/tx/operation-context.js +40 -3
- package/tx/provider.js +41 -12
- package/tx/sct/ecl.js +30 -11
- package/tx/sct/expressions.js +28 -1
- package/tx/sct/structures.js +5 -5
- package/tx/tx-html.js +24 -15
- package/tx/tx.fhir.org.yml +1 -4
- package/tx/vs/vs-database.js +41 -8
- package/tx/vs/vs-package.js +55 -0
- package/tx/vs/vs-vsac.js +4 -0
- package/tx/workers/batch-validate.js +125 -18
- package/tx/workers/cache-control.js +31 -2
- package/tx/workers/expand.js +50 -32
- package/tx/workers/metadata.js +27 -6
- package/tx/workers/search.js +7 -1
- package/tx/workers/validate.js +30 -18
- package/tx/workers/worker.js +30 -7
- package/tx/xversion/xv-valueset.js +5 -2
- package/xig/xig.js +3 -3
package/registry/model.js
CHANGED
|
@@ -86,6 +86,8 @@ class ServerInformation {
|
|
|
86
86
|
this.authCSList = []; // Authoritative code systems (with wildcards)
|
|
87
87
|
this.authVSList = []; // Authoritative value sets (with wildcards)
|
|
88
88
|
this.usageList = []; // Usage tags
|
|
89
|
+
this.languages = {}; // Language specific authoritative claims: BCP-47 tag -> list of CS masks
|
|
90
|
+
this.exclusions = []; // CodeSystem/ValueSet masks hidden from the ecosystem entirely
|
|
89
91
|
this.versions = []; // Array of ServerVersionInformation
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -100,6 +102,11 @@ class ServerInformation {
|
|
|
100
102
|
this.authCSList = [...source.authCSList];
|
|
101
103
|
this.authVSList = [...source.authVSList];
|
|
102
104
|
this.usageList = [...source.usageList];
|
|
105
|
+
this.languages = {};
|
|
106
|
+
Object.keys(source.languages || {}).forEach(tag => {
|
|
107
|
+
this.languages[tag] = [...source.languages[tag]];
|
|
108
|
+
});
|
|
109
|
+
this.exclusions = [...(source.exclusions || [])];
|
|
103
110
|
|
|
104
111
|
source.versions.forEach(sourceVersion => {
|
|
105
112
|
const existing = this.getVersion(sourceVersion.version);
|
|
@@ -116,11 +123,80 @@ class ServerInformation {
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
isAuthCS(codeSystem) {
|
|
119
|
-
return this.
|
|
126
|
+
return !this.isExcludedTarget(codeSystem) &&
|
|
127
|
+
this.authCSList.some(mask => this._passesMask(mask, codeSystem));
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
isAuthVS(valueSet) {
|
|
123
|
-
return this.
|
|
131
|
+
return !this.isExcludedTarget(valueSet) &&
|
|
132
|
+
this.authVSList.some(mask => this._passesMask(mask, valueSet));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* True if the code system / value set is hidden from the ecosystem for this server
|
|
137
|
+
* (matches an entry in exclusions). Excluded content is hidden entirely: the server
|
|
138
|
+
* is never offered for it, neither as authoritative nor as a candidate, even if an
|
|
139
|
+
* authoritative mask (or language specific claim) would otherwise match it.
|
|
140
|
+
* Masks are tested against both the full canonical (url|version) and the base url.
|
|
141
|
+
*/
|
|
142
|
+
isExcludedTarget(value) {
|
|
143
|
+
return (this.exclusions || []).some(mask => this._passesMaskEx(mask, value, true));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Match this server's authoritative claims for a code system, taking language specific
|
|
148
|
+
* claims (the languages property: BCP-47 tag -> mask list) into account.
|
|
149
|
+
*
|
|
150
|
+
* Language specific claims are only visible when the request specifies a language. A
|
|
151
|
+
* claimed tag is inherently a mask: it covers a request language that is equal to it or
|
|
152
|
+
* more specific ('de' covers 'de' and 'de-AT'; 'de-AT' does not cover plain 'de'). The
|
|
153
|
+
* best match is on the earliest (heaviest) request language, then the most specific
|
|
154
|
+
* claimed tag. Claims in the authoritative list apply irrespective of language, but rank
|
|
155
|
+
* after all language specific matches (score = MAX_SAFE_INTEGER).
|
|
156
|
+
*
|
|
157
|
+
* @param {string} codeSystem - canonical url or url|version
|
|
158
|
+
* @param {Array|null} requestLangs - parsed Accept-Language list ([{tag, q}] in descending
|
|
159
|
+
* weight order, per ServerRegistryUtilities.parseAcceptLanguage), or null for no language
|
|
160
|
+
* @param {boolean} maskBase - also test masks against the base URL (before '|'), matching
|
|
161
|
+
* the semantics of hasMatchingCodeSystem as used for the discovery rows
|
|
162
|
+
* @returns {Object} {isAuth: boolean, scoped: boolean, tag?: string, score: number}
|
|
163
|
+
* score: lower is better - earlier request languages and more specific tags win
|
|
164
|
+
*/
|
|
165
|
+
matchAuthCS(codeSystem, requestLangs = null, maskBase = false) {
|
|
166
|
+
// excluded content is hidden entirely - no claim of any kind can match it
|
|
167
|
+
if (this.isExcludedTarget(codeSystem)) {
|
|
168
|
+
return { isAuth: false, scoped: false, score: Number.MAX_SAFE_INTEGER };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const masksMatch = (masks) => masks.some(mask => this._passesMaskEx(mask, codeSystem, maskBase));
|
|
172
|
+
|
|
173
|
+
if (requestLangs && requestLangs.length > 0 && this.languages) {
|
|
174
|
+
let best = null;
|
|
175
|
+
for (let i = 0; i < requestLangs.length; i++) {
|
|
176
|
+
for (const tag of Object.keys(this.languages)) {
|
|
177
|
+
if (!ServerRegistryUtilities.languageTagCovers(tag, requestLangs[i].tag)) continue;
|
|
178
|
+
if (!masksMatch(this.languages[tag] || [])) continue;
|
|
179
|
+
// earlier request language (higher weight) dominates; then longer (more specific) tag
|
|
180
|
+
const score = i * 1000 + (256 - tag.length);
|
|
181
|
+
if (!best || score < best.score) {
|
|
182
|
+
best = { isAuth: true, scoped: true, tag, score };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (best) return best; // don't consider later (lighter) request languages
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (masksMatch(this.authCSList)) {
|
|
189
|
+
return { isAuth: true, scoped: false, score: Number.MAX_SAFE_INTEGER };
|
|
190
|
+
}
|
|
191
|
+
return { isAuth: false, scoped: false, score: Number.MAX_SAFE_INTEGER };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_passesMaskEx(mask, value, includeBase) {
|
|
195
|
+
if (this._passesMask(mask, value)) return true;
|
|
196
|
+
if (includeBase && value.includes('|')) {
|
|
197
|
+
return this._passesMask(mask, value.substring(0, value.indexOf('|')));
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
124
200
|
}
|
|
125
201
|
|
|
126
202
|
_passesMask(mask, value) {
|
|
@@ -132,11 +208,34 @@ class ServerInformation {
|
|
|
132
208
|
|
|
133
209
|
getDescription() {
|
|
134
210
|
let result = '';
|
|
135
|
-
|
|
211
|
+
|
|
136
212
|
if (this.usageList.length > 0) {
|
|
137
213
|
result = `Usage Tags: ${this.usageList.join(', ')}`;
|
|
138
214
|
}
|
|
139
|
-
|
|
215
|
+
|
|
216
|
+
const langTags = Object.keys(this.languages || {});
|
|
217
|
+
if (langTags.length > 0) {
|
|
218
|
+
if (result) result += '. ';
|
|
219
|
+
result += 'Authoritative for particular languages: <ul>';
|
|
220
|
+
langTags.forEach(tag => {
|
|
221
|
+
(this.languages[tag] || []).forEach(mask => {
|
|
222
|
+
const escaped = escape(mask).replace(/\*/g, '<b>*</b>');
|
|
223
|
+
result += `<li>${escape(tag)}: ${escaped}</li>`;
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
result += '</ul>';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (this.exclusions && this.exclusions.length > 0) {
|
|
230
|
+
if (result) result += '. ';
|
|
231
|
+
result += 'Excluded (hidden from the ecosystem): <ul>';
|
|
232
|
+
this.exclusions.forEach(mask => {
|
|
233
|
+
const escaped = escape(mask).replace(/\*/g, '<b>*</b>');
|
|
234
|
+
result += `<li>${escaped}</li>`;
|
|
235
|
+
});
|
|
236
|
+
result += '</ul>';
|
|
237
|
+
}
|
|
238
|
+
|
|
140
239
|
if (this.authCSList.length > 0) {
|
|
141
240
|
if (result) result += '. ';
|
|
142
241
|
result += 'Authoritative for the following CodeSystems: <ul>';
|
|
@@ -169,6 +268,8 @@ class ServerInformation {
|
|
|
169
268
|
authoritative: this.authCSList.join(','),
|
|
170
269
|
'authoritative-valuesets': this.authVSList.join(','),
|
|
171
270
|
usageList: this.usageList,
|
|
271
|
+
languages: this.languages,
|
|
272
|
+
exclusions: this.exclusions.join(','),
|
|
172
273
|
versions: this.versions.map(v => v.toJSON())
|
|
173
274
|
};
|
|
174
275
|
}
|
|
@@ -179,13 +280,20 @@ class ServerInformation {
|
|
|
179
280
|
instance.name = json.name || '';
|
|
180
281
|
instance.address = json.address || '';
|
|
181
282
|
instance.accessInfo = json['access-info'] || '';
|
|
182
|
-
instance.authCSList = json.authoritative
|
|
283
|
+
instance.authCSList = json.authoritative
|
|
183
284
|
? json.authoritative.split(',').filter(s => s)
|
|
184
285
|
: [];
|
|
185
|
-
instance.authVSList = json['authoritative-valuesets']
|
|
286
|
+
instance.authVSList = json['authoritative-valuesets']
|
|
186
287
|
? json['authoritative-valuesets'].split(',').filter(s => s)
|
|
187
288
|
: [];
|
|
188
289
|
instance.usageList = json.usageList || [];
|
|
290
|
+
// languages is an object (tag -> mask list); tolerate legacy/absent data
|
|
291
|
+
instance.languages = (json.languages && typeof json.languages === 'object' && !Array.isArray(json.languages))
|
|
292
|
+
? json.languages
|
|
293
|
+
: {};
|
|
294
|
+
instance.exclusions = json.exclusions
|
|
295
|
+
? json.exclusions.split(',').filter(s => s)
|
|
296
|
+
: [];
|
|
189
297
|
instance.versions = (json.versions || []).map(v => ServerVersionInformation.fromJSON(v));
|
|
190
298
|
return instance;
|
|
191
299
|
}
|
|
@@ -327,6 +435,7 @@ class ServerRow {
|
|
|
327
435
|
this.systems = 0; // count of code systems
|
|
328
436
|
this.sets = 0; // count of value sets
|
|
329
437
|
this.authoritative = false;
|
|
438
|
+
this.languages = []; // language tag(s) of the language specific claim matched (empty = none)
|
|
330
439
|
}
|
|
331
440
|
|
|
332
441
|
toJSON() {
|
|
@@ -357,7 +466,10 @@ class ServerRow {
|
|
|
357
466
|
json['authoritative-valuesets'] = this.authVSList;
|
|
358
467
|
}
|
|
359
468
|
|
|
360
|
-
|
|
469
|
+
if (this.languages.length > 0) {
|
|
470
|
+
json.languages = this.languages;
|
|
471
|
+
}
|
|
472
|
+
|
|
361
473
|
return json;
|
|
362
474
|
}
|
|
363
475
|
|
|
@@ -378,12 +490,13 @@ class ServerRow {
|
|
|
378
490
|
instance.authCSList = json.authoritative || [];
|
|
379
491
|
instance.authVSList = json['authoritative-valuesets'] || [];
|
|
380
492
|
instance.security = json['security'] || '';
|
|
493
|
+
instance.languages = json.languages || [];
|
|
381
494
|
|
|
382
495
|
return instance;
|
|
383
496
|
}
|
|
384
497
|
}
|
|
385
498
|
|
|
386
|
-
// Utility functions
|
|
499
|
+
// Utility functions for registry matching and row building
|
|
387
500
|
class ServerRegistryUtilities {
|
|
388
501
|
static passesMask(mask, value) {
|
|
389
502
|
if (mask.endsWith('*')) {
|
|
@@ -392,6 +505,43 @@ class ServerRegistryUtilities {
|
|
|
392
505
|
return value === mask;
|
|
393
506
|
}
|
|
394
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Parse an Accept-Language style value ('de', 'de-AT, de;q=0.9, en;q=0.1') into
|
|
510
|
+
* an ordered list of {tag, q}, highest weight first. Wildcards ('*') and zero-weight
|
|
511
|
+
* entries are dropped - they don't constrain routing. Returns null when nothing usable.
|
|
512
|
+
*/
|
|
513
|
+
static parseAcceptLanguage(value) {
|
|
514
|
+
if (!value || typeof value !== 'string') return null;
|
|
515
|
+
const items = [];
|
|
516
|
+
for (const part of value.split(',')) {
|
|
517
|
+
const pieces = part.trim().split(';').map(p => p.trim());
|
|
518
|
+
const tag = pieces[0];
|
|
519
|
+
if (!tag || tag === '*') continue;
|
|
520
|
+
let q = 1.0;
|
|
521
|
+
for (let i = 1; i < pieces.length; i++) {
|
|
522
|
+
const m = /^q\s*=\s*([0-9.]+)$/i.exec(pieces[i]);
|
|
523
|
+
if (m) q = parseFloat(m[1]);
|
|
524
|
+
}
|
|
525
|
+
if (!(q > 0)) continue;
|
|
526
|
+
items.push({ tag, q });
|
|
527
|
+
}
|
|
528
|
+
if (items.length === 0) return null;
|
|
529
|
+
items.sort((a, b) => b.q - a.q); // stable: preserves order among equal weights
|
|
530
|
+
return items;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* BCP-47 tags are hierarchical, so a tag is inherently a mask: a scoped tag covers a
|
|
535
|
+
* request language that is equal to it, or more specific (on '-' boundaries).
|
|
536
|
+
* languageTagCovers('de', 'de-AT') = true; ('de', 'dent') = false; ('de-AT', 'de') = false.
|
|
537
|
+
*/
|
|
538
|
+
static languageTagCovers(scopedTag, requestTag) {
|
|
539
|
+
if (!scopedTag || !requestTag) return false;
|
|
540
|
+
const s = scopedTag.toLowerCase();
|
|
541
|
+
const r = requestTag.toLowerCase();
|
|
542
|
+
return r === s || r.startsWith(s + '-');
|
|
543
|
+
}
|
|
544
|
+
|
|
395
545
|
static hasMatchingCodeSystem(cs, list, supportMask, content, noVersionIndependentMatching = false) {
|
|
396
546
|
if (!cs || list.length === 0) return false;
|
|
397
547
|
|
package/registry/readme.md
CHANGED
|
@@ -178,6 +178,10 @@ Servers can be marked as authoritative for specific code systems or value sets.
|
|
|
178
178
|
- Authoritative servers are always ranked first
|
|
179
179
|
- Wildcards are supported (e.g., `http://loinc.org*`)
|
|
180
180
|
- Non-authoritative servers are still returned but ranked lower
|
|
181
|
+
- Language specific claims (`languages`: BCP-47 tag -> mask list) make a server authoritative for
|
|
182
|
+
requests in that language only; matched entries rank ahead of language independent claims
|
|
183
|
+
- `exclusions` hides matching code systems/value sets from the ecosystem entirely (never
|
|
184
|
+
authoritative, never a candidate)
|
|
181
185
|
|
|
182
186
|
## Testing
|
|
183
187
|
|
|
@@ -205,18 +209,6 @@ Start development server with auto-reload:
|
|
|
205
209
|
npm run dev
|
|
206
210
|
```
|
|
207
211
|
|
|
208
|
-
## Migration from Pascal
|
|
209
|
-
|
|
210
|
-
This module is a JavaScript port of a Pascal implementation. Key differences:
|
|
211
|
-
|
|
212
|
-
| Pascal | JavaScript |
|
|
213
|
-
|--------|------------|
|
|
214
|
-
| TStringList | Array |
|
|
215
|
-
| TFslList<T> | Array |
|
|
216
|
-
| set of TServerSecurity | Set |
|
|
217
|
-
| TFslDateTime | Date |
|
|
218
|
-
| class methods | static methods |
|
|
219
|
-
|
|
220
212
|
## Security Models
|
|
221
213
|
|
|
222
214
|
The crawler detects the following security models:
|
|
@@ -230,4 +222,4 @@ The crawler detects the following security models:
|
|
|
230
222
|
|
|
231
223
|
## License
|
|
232
224
|
|
|
233
|
-
BSD-3-Clause
|
|
225
|
+
BSD-3-Clause
|
package/registry/registry.js
CHANGED
|
@@ -116,13 +116,13 @@ class RegistryModule {
|
|
|
116
116
|
|
|
117
117
|
// Run initial crawl after a short delay
|
|
118
118
|
setTimeout(() => {
|
|
119
|
-
this.performCrawl();
|
|
119
|
+
this.performCrawl().catch((err) => this.logger.error('Registry crawl failed: ' + err.message));
|
|
120
120
|
}, 5000);
|
|
121
121
|
|
|
122
122
|
// Set up periodic crawling
|
|
123
123
|
this.stats.addTask("TxRegistry", `${intervalMinutes} min`);
|
|
124
124
|
this.crawlInterval = setInterval(() => {
|
|
125
|
-
this.performCrawl();
|
|
125
|
+
this.performCrawl().catch((err) => this.logger.error('Registry crawl failed: ' + err.message));
|
|
126
126
|
}, intervalMs);
|
|
127
127
|
|
|
128
128
|
this.logger.info(`Started periodic crawl every ${intervalMinutes} minutes`);
|
|
@@ -299,7 +299,7 @@ class RegistryModule {
|
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
301
|
* Get status text about crawling
|
|
302
|
-
*
|
|
302
|
+
* Describes the crawl schedule state
|
|
303
303
|
*/
|
|
304
304
|
getStatusText() {
|
|
305
305
|
if (this.crawlInProgress) {
|
|
@@ -331,7 +331,7 @@ class RegistryModule {
|
|
|
331
331
|
|
|
332
332
|
/**
|
|
333
333
|
* Format a time period in milliseconds to a human-readable string
|
|
334
|
-
|
|
334
|
+
|
|
335
335
|
*/
|
|
336
336
|
describePeriod(milliseconds) {
|
|
337
337
|
const seconds = Math.floor(milliseconds / 1000);
|
|
@@ -552,35 +552,43 @@ class RegistryModule {
|
|
|
552
552
|
|
|
553
553
|
if (!data || !data.registries) return [];
|
|
554
554
|
|
|
555
|
-
// Gather all authoritative code systems
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
});
|
|
565
|
-
}
|
|
555
|
+
// Gather all authoritative code systems - both the language independent claims
|
|
556
|
+
// (authoritative) and the language specific claims (languages: tag -> mask list)
|
|
557
|
+
const addClaim = (server, csMask, languageTag) => {
|
|
558
|
+
if (!authCSMap.has(csMask)) {
|
|
559
|
+
authCSMap.set(csMask, {
|
|
560
|
+
mask: csMask,
|
|
561
|
+
servers: new Map()
|
|
562
|
+
});
|
|
563
|
+
}
|
|
566
564
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
565
|
+
// Key by server code + language tag, so a server's language independent and
|
|
566
|
+
// language specific claims for the same mask show as separate rows
|
|
567
|
+
const csEntry = authCSMap.get(csMask);
|
|
568
|
+
const key = `${server.code}|${languageTag || ''}`;
|
|
569
|
+
if (!csEntry.servers.has(key)) {
|
|
570
|
+
csEntry.servers.set(key, {
|
|
571
|
+
name: server.name,
|
|
572
|
+
url: server.address,
|
|
573
|
+
languages: languageTag ? [languageTag] : [],
|
|
574
|
+
versions: new Set()
|
|
575
|
+
});
|
|
576
|
+
}
|
|
576
577
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
578
|
+
// Add version info for this server
|
|
579
|
+
const serverEntry = csEntry.servers.get(key);
|
|
580
|
+
server.versions.forEach(version => {
|
|
581
|
+
if (!version.error) {
|
|
582
|
+
serverEntry.versions.add(version.version);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
data.registries.forEach(registry => {
|
|
588
|
+
registry.servers.forEach(server => {
|
|
589
|
+
server.authCSList.forEach(csMask => addClaim(server, csMask, null));
|
|
590
|
+
Object.keys(server.languages || {}).forEach(tag => {
|
|
591
|
+
(server.languages[tag] || []).forEach(csMask => addClaim(server, csMask, tag));
|
|
584
592
|
});
|
|
585
593
|
});
|
|
586
594
|
});
|
|
@@ -626,18 +634,19 @@ class RegistryModule {
|
|
|
626
634
|
});
|
|
627
635
|
}
|
|
628
636
|
|
|
629
|
-
// Add server info
|
|
637
|
+
// Add server info (value set claims have no language dimension at this time)
|
|
630
638
|
const vsEntry = authVSMap.get(vsMask);
|
|
631
|
-
if (!vsEntry.servers.has(server.
|
|
632
|
-
vsEntry.servers.set(server.
|
|
639
|
+
if (!vsEntry.servers.has(server.code)) {
|
|
640
|
+
vsEntry.servers.set(server.code, {
|
|
633
641
|
name: server.name,
|
|
634
642
|
url: server.address,
|
|
643
|
+
languages: [],
|
|
635
644
|
versions: new Set()
|
|
636
645
|
});
|
|
637
646
|
}
|
|
638
647
|
|
|
639
648
|
// Add version info for this server
|
|
640
|
-
const serverEntry = vsEntry.servers.get(server.
|
|
649
|
+
const serverEntry = vsEntry.servers.get(server.code);
|
|
641
650
|
server.versions.forEach(version => {
|
|
642
651
|
if (!version.error) {
|
|
643
652
|
serverEntry.versions.add(version.version);
|
|
@@ -788,6 +797,19 @@ class RegistryModule {
|
|
|
788
797
|
return text.replace(/\*/g, '<strong class="text-primary">*</strong>');
|
|
789
798
|
}
|
|
790
799
|
|
|
800
|
+
/**
|
|
801
|
+
* Render the language tag(s) of a language specific claim (empty string when the
|
|
802
|
+
* claim is language independent)
|
|
803
|
+
* @param {Object} serverEntry - entry with a languages array
|
|
804
|
+
* @returns {string} HTML fragment
|
|
805
|
+
*/
|
|
806
|
+
_renderLanguageScope(serverEntry) {
|
|
807
|
+
if (!serverEntry.languages || serverEntry.languages.length === 0) {
|
|
808
|
+
return '';
|
|
809
|
+
}
|
|
810
|
+
return ` <span class="badge badge-info" title="This claim applies to requests in these languages only">${escape(serverEntry.languages.join(', '))}</span>`;
|
|
811
|
+
}
|
|
812
|
+
|
|
791
813
|
/**
|
|
792
814
|
* Render HTML table for authoritative code systems
|
|
793
815
|
* @returns {string} HTML string
|
|
@@ -821,7 +843,7 @@ class RegistryModule {
|
|
|
821
843
|
const rowspan = cs.servers.length;
|
|
822
844
|
html += '<tr>';
|
|
823
845
|
html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(formattedMask))}</td>`;
|
|
824
|
-
html += `<td><a href="${escape(cs.servers[0].url)}" target="_blank">${escape(cs.servers[0].url)}</a
|
|
846
|
+
html += `<td><a href="${escape(cs.servers[0].url)}" target="_blank">${escape(cs.servers[0].url)}</a>${this._renderLanguageScope(cs.servers[0])}</td>`;
|
|
825
847
|
|
|
826
848
|
// Format versions as R3/R4/R5
|
|
827
849
|
const formattedVersions = cs.servers[0].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -831,7 +853,7 @@ class RegistryModule {
|
|
|
831
853
|
// Additional rows for this code system (if any)
|
|
832
854
|
for (let i = 1; i < cs.servers.length; i++) {
|
|
833
855
|
html += '<tr>';
|
|
834
|
-
html += `<td><a href="${escape(cs.servers[i].url)}" target="_blank">${escape(cs.servers[i].url)}</a
|
|
856
|
+
html += `<td><a href="${escape(cs.servers[i].url)}" target="_blank">${escape(cs.servers[i].url)}</a>${this._renderLanguageScope(cs.servers[i])}</td>`;
|
|
835
857
|
|
|
836
858
|
// Format versions as R3/R4/R5
|
|
837
859
|
const formattedVersions = cs.servers[i].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -877,7 +899,7 @@ class RegistryModule {
|
|
|
877
899
|
const rowspan = vs.servers.length;
|
|
878
900
|
html += '<tr>';
|
|
879
901
|
html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(vs.mask))}</td>`;
|
|
880
|
-
html += `<td><a href="${escape(vs.servers[0].url)}" target="_blank">${escape(vs.servers[0].url)}</a
|
|
902
|
+
html += `<td><a href="${escape(vs.servers[0].url)}" target="_blank">${escape(vs.servers[0].url)}</a>${this._renderLanguageScope(vs.servers[0])}</td>`;
|
|
881
903
|
|
|
882
904
|
// Format versions as R3/R4/R5
|
|
883
905
|
const formattedVersions = vs.servers[0].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -887,7 +909,7 @@ class RegistryModule {
|
|
|
887
909
|
// Additional rows for this value set (if any)
|
|
888
910
|
for (let i = 1; i < vs.servers.length; i++) {
|
|
889
911
|
html += '<tr>';
|
|
890
|
-
html += `<td><a href="${escape(vs.servers[i].url)}" target="_blank">${escape(vs.servers[i].url)}</a
|
|
912
|
+
html += `<td><a href="${escape(vs.servers[i].url)}" target="_blank">${escape(vs.servers[i].url)}</a>${this._renderLanguageScope(vs.servers[i])}</td>`;
|
|
891
913
|
|
|
892
914
|
// Format versions as R3/R4/R5
|
|
893
915
|
const formattedVersions = vs.servers[i].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -976,7 +998,7 @@ class RegistryModule {
|
|
|
976
998
|
|
|
977
999
|
try {
|
|
978
1000
|
const params = this._normalizeQueryParams(req.query);
|
|
979
|
-
const {fhirVersion, valueSet, usage, version} = params;
|
|
1001
|
+
const {fhirVersion, valueSet, usage, version, language} = params;
|
|
980
1002
|
let {url} = params;
|
|
981
1003
|
|
|
982
1004
|
// If a version was supplied separately, fold it into the code system
|
|
@@ -1050,16 +1072,16 @@ class RegistryModule {
|
|
|
1050
1072
|
|
|
1051
1073
|
if (valueSet) {
|
|
1052
1074
|
// Value set resolve
|
|
1053
|
-
const resolveResult = this.api.resolveValueSet(fhirVersion, valueSet, authoritativeOnly, usage);
|
|
1075
|
+
const resolveResult = this.api.resolveValueSet(fhirVersion, valueSet, authoritativeOnly, usage, language);
|
|
1054
1076
|
result = resolveResult.result;
|
|
1055
1077
|
matches = resolveResult.matches;
|
|
1056
|
-
this.logger.info(`Resolved ValueSet ${valueSet} for FHIR ${fhirVersion} (usage=${usage}): ${matches}`);
|
|
1078
|
+
this.logger.info(`Resolved ValueSet ${valueSet} for FHIR ${fhirVersion} (usage=${usage}, language=${language || 'none'}): ${matches}`);
|
|
1057
1079
|
} else {
|
|
1058
1080
|
// Code system resolve
|
|
1059
|
-
const resolveResult = this.api.resolveCodeSystem(fhirVersion, url, authoritativeOnly, usage);
|
|
1081
|
+
const resolveResult = this.api.resolveCodeSystem(fhirVersion, url, authoritativeOnly, usage, language);
|
|
1060
1082
|
result = resolveResult.result;
|
|
1061
1083
|
matches = resolveResult.matches;
|
|
1062
|
-
this.logger.info(`Resolved CodeSystem ${url} for FHIR ${fhirVersion} (usage=${usage}): ${matches}`);
|
|
1084
|
+
this.logger.info(`Resolved CodeSystem ${url} for FHIR ${fhirVersion} (usage=${usage}, language=${language || 'none'}): ${matches}`);
|
|
1063
1085
|
}
|
|
1064
1086
|
|
|
1065
1087
|
// If only authoritative servers are requested, filter results
|
|
@@ -1076,7 +1098,7 @@ class RegistryModule {
|
|
|
1076
1098
|
htmlServer.loadTemplate('registry', templatePath);
|
|
1077
1099
|
}
|
|
1078
1100
|
|
|
1079
|
-
const content = this.buildResolveResultContent(result, fhirVersion, url || valueSet, usage);
|
|
1101
|
+
const content = this.buildResolveResultContent(result, fhirVersion, url || valueSet, usage, language);
|
|
1080
1102
|
const stats = this.api.getStatistics();
|
|
1081
1103
|
stats.processingTime = Date.now() - startTime;
|
|
1082
1104
|
|
|
@@ -1104,7 +1126,7 @@ class RegistryModule {
|
|
|
1104
1126
|
}
|
|
1105
1127
|
}
|
|
1106
1128
|
|
|
1107
|
-
buildResolveResultContent(result, fhirVersion, resourceUrl, usage) {
|
|
1129
|
+
buildResolveResultContent(result, fhirVersion, resourceUrl, usage, language) {
|
|
1108
1130
|
let html = '';
|
|
1109
1131
|
|
|
1110
1132
|
// Query information section
|
|
@@ -1119,6 +1141,9 @@ class RegistryModule {
|
|
|
1119
1141
|
if (usage) {
|
|
1120
1142
|
html += `<p><strong>Usage:</strong> ${escape(usage)}</p>`;
|
|
1121
1143
|
}
|
|
1144
|
+
if (language) {
|
|
1145
|
+
html += `<p><strong>Language:</strong> ${escape(language)}</p>`;
|
|
1146
|
+
}
|
|
1122
1147
|
html += '</div>';
|
|
1123
1148
|
html += '</div>';
|
|
1124
1149
|
|
|
@@ -1136,6 +1161,7 @@ class RegistryModule {
|
|
|
1136
1161
|
html += '<th>Server Name</th>';
|
|
1137
1162
|
html += '<th>URL</th>';
|
|
1138
1163
|
html += '<th>Security</th>';
|
|
1164
|
+
html += '<th>Languages</th>';
|
|
1139
1165
|
html += '<th>Access Info</th>';
|
|
1140
1166
|
html += '</tr>';
|
|
1141
1167
|
html += '</thead>';
|
|
@@ -1146,6 +1172,7 @@ class RegistryModule {
|
|
|
1146
1172
|
html += `<td>${escape(server['server-name'])}</td>`;
|
|
1147
1173
|
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1148
1174
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1175
|
+
html += `<td>${server.languages ? escape(server.languages.join(', ')) : ''}</td>`;
|
|
1149
1176
|
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
1150
1177
|
html += '</tr>';
|
|
1151
1178
|
});
|
|
@@ -1173,6 +1200,7 @@ class RegistryModule {
|
|
|
1173
1200
|
html += '<th>Server Name</th>';
|
|
1174
1201
|
html += '<th>URL</th>';
|
|
1175
1202
|
html += '<th>Security</th>';
|
|
1203
|
+
html += '<th>Languages</th>';
|
|
1176
1204
|
html += '<th>Access Info</th>';
|
|
1177
1205
|
html += '<th>Content</th>';
|
|
1178
1206
|
html += '</tr>';
|
|
@@ -1184,6 +1212,11 @@ class RegistryModule {
|
|
|
1184
1212
|
html += `<td>${escape(server['server-name'])}</td>`;
|
|
1185
1213
|
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1186
1214
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1215
|
+
if (server['language-support'] === 'unknown') {
|
|
1216
|
+
html += '<td><span class="text-muted" title="This server hosts the code system, but the ecosystem does not know whether it can supply the requested language">unknown</span></td>';
|
|
1217
|
+
} else {
|
|
1218
|
+
html += `<td>${server.languages ? escape(server.languages.join(', ')) : ''}</td>`;
|
|
1219
|
+
}
|
|
1187
1220
|
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
1188
1221
|
html += `<td>${server.content ? escape(server.content) : ''}</td>`;
|
|
1189
1222
|
html += '</tr>';
|
|
@@ -1229,6 +1262,7 @@ class RegistryModule {
|
|
|
1229
1262
|
const url = queryParams.url || '';
|
|
1230
1263
|
const version = queryParams.version || '';
|
|
1231
1264
|
const valueSet = queryParams.valueSet || '';
|
|
1265
|
+
const language = queryParams.language || '';
|
|
1232
1266
|
const authoritativeOnly = queryParams.authoritativeOnly === 'true';
|
|
1233
1267
|
|
|
1234
1268
|
let html = '';
|
|
@@ -1271,6 +1305,13 @@ class RegistryModule {
|
|
|
1271
1305
|
html += '</p>';
|
|
1272
1306
|
html += '<p class="text-muted small">Example: http://hl7.org/fhir/ValueSet/observation-codes</p>';
|
|
1273
1307
|
|
|
1308
|
+
// Language field (optional)
|
|
1309
|
+
html += '<p>';
|
|
1310
|
+
html += '<label for="language" class="form-label fw-bold">Language</label>';
|
|
1311
|
+
html += `<input type="text" class="form-control" id="language" name="language"
|
|
1312
|
+
value="${escape(language)}">`;
|
|
1313
|
+
html += '</p>';
|
|
1314
|
+
html += '<p class="text-muted small">Optional. The language of the request being routed, in Accept-Language syntax. Examples: de or de-AT, de;q=0.9, en;q=0.1</p>';
|
|
1274
1315
|
|
|
1275
1316
|
// Authoritative Only checkbox
|
|
1276
1317
|
html += '<p>';
|
package/registry/test-runner.js
CHANGED
|
@@ -187,7 +187,7 @@ function runTests() {
|
|
|
187
187
|
if (rows2.length > 0) {
|
|
188
188
|
console.log(` Versions: ${rows2.map(r => `${r.version} (auth=${r.authoritative})`).join(', ')}`);
|
|
189
189
|
}
|
|
190
|
-
console.log(` Test ${rows2.length === 1 && rows2[0].version === '4.0.1' ? '✓ PASSED (strict)' : rows2.length === 2 ? '✓ PASSED (
|
|
190
|
+
console.log(` Test ${rows2.length === 1 && rows2[0].version === '4.0.1' ? '✓ PASSED (strict)' : rows2.length === 2 ? '✓ PASSED (lenient)' : '✗ FAILED'}\n`);
|
|
191
191
|
|
|
192
192
|
// Test 3: should rank servers without errors higher
|
|
193
193
|
console.log('Test 3: should not include servers with errors even if authoritative');
|
package/server.js
CHANGED
|
@@ -68,6 +68,10 @@ htmlServer.setSponsorMessage(config.sponsorMessage ? config.sponsorMessage : '')
|
|
|
68
68
|
|
|
69
69
|
const app = express();
|
|
70
70
|
|
|
71
|
+
// Behind nginx (or any reverse proxy): honor X-Forwarded-* so req.protocol and
|
|
72
|
+
// req.hostname reflect what the client actually requested, not the loopback hop.
|
|
73
|
+
app.set('trust proxy', config.server.trustProxy ?? true);
|
|
74
|
+
|
|
71
75
|
const PORT = process.env.PORT || config.server.port || 3000;
|
|
72
76
|
|
|
73
77
|
// Middleware
|
|
@@ -860,4 +864,7 @@ async function serveFhirsmithHome(req, res) {
|
|
|
860
864
|
}
|
|
861
865
|
|
|
862
866
|
// Start the server
|
|
863
|
-
startServer()
|
|
867
|
+
startServer().catch((err) => {
|
|
868
|
+
serverLog.error('Fatal error during startup:', err);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
});
|
package/token/token.js
CHANGED
|
@@ -528,7 +528,7 @@ class TokenModule {
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
if (userId) {
|
|
531
|
-
this.logSecurityEvent(userId, 'logout', req.ip, req.get('User-Agent'), {});
|
|
531
|
+
void this.logSecurityEvent(userId, 'logout', req.ip, req.get('User-Agent'), {}); // fire-and-forget audit; errors handled internally
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
res.redirect('/token/login');
|
|
@@ -158,6 +158,7 @@ CODESYSTEM_SHAREABLE_MISSING_HL7 = Code systems published by HL7 SHALL conform t
|
|
|
158
158
|
CODESYSTEM_SUPP_NO_DISPLAY = This display (''{0}'') differs from that defined by the base code system (''{1}''). Both displays claim to be the ''primary designation'' for the same language (''{2}''), and the correct interpretation of this is undefined
|
|
159
159
|
CODESYSTEM_THO_CHECK = Most code systems defined in HL7 IGs will need to move to THO later during the process. Consider giving this code system a THO URL now (See https://confluence.hl7.org/display/TSMG/Terminology+Play+Book, and/or talk to TSMG)
|
|
160
160
|
CODESYSTEM_TOO_COSTLY_TIME = The operation ''{1}'' took too long to process (>{0}sec) (at {2})
|
|
161
|
+
CODESYSTEM_NOT_ENUMERABLE = The code system ''{0}'' cannot be expanded because its codes cannot be iterated or enumerated in any meaningful sense
|
|
161
162
|
Coding_has_no_system__cannot_validate = Coding has no system. A code with no system has no defined meaning, and it cannot be validated. A system should be provided
|
|
162
163
|
CONCEPTMAP_GROUP_SOURCE_CODE_INVALID = The source code ''{0}'' is not valid in the code system {1}
|
|
163
164
|
CONCEPTMAP_GROUP_SOURCE_CODE_INVALID_VS = The source code ''{0}'' is not valid in the value set {1}
|
|
@@ -1492,7 +1493,8 @@ RESOURCE_INTERNAL_USE_ONLY = This {0} comes from the package {1} which has been
|
|
|
1492
1493
|
TYPE_SPECIFIC_CHECKS_DT_SID_INCORRECT = The URL {0} is not a correct URL to use (in spite of being in the standard, and/or accepted in the past)
|
|
1493
1494
|
INACTIVE_DISPLAY_FOUND_one = ''{1}'' is no longer considered a correct display for code ''{2}'' (status = {4}). The correct display is ''{3}''
|
|
1494
1495
|
INACTIVE_DISPLAY_FOUND_other = ''{1}'' is no longer considered a correct display for code ''{2}'' (status = {4}). The correct display is one of {3}
|
|
1495
|
-
INACTIVE_CONCEPT_FOUND =
|
|
1496
|
+
INACTIVE_CONCEPT_FOUND = The concept ''{1}'' has a status of {0} and its use should be reviewed
|
|
1497
|
+
INACTIVE_CONCEPT_FOUND_ADD = The concept ''{1}'' has a status of {0} and {2} and its use should be reviewed
|
|
1496
1498
|
DEPRECATED_CONCEPT_FOUND = The concept ''{1}'' is deprecated and its use should be reviewed
|
|
1497
1499
|
CONCEPT_DEPRECATED_IN_VALUESET = The presence of the concept ''{1}'' in the system ''{0}'' in the value set {3} is marked with a status of {2} and its use should be reviewed
|
|
1498
1500
|
SYSTEM_VERSION_MULTIPLE_OVERRIDE = Multiple version overrides found for system {0}: ''{1}'', ''{2}'');
|