fhirsmith 0.9.6 → 0.9.7
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 +20 -0
- package/library/folder-content-loader.js +91 -0
- package/npmprojector/npmprojector.js +2 -6
- package/package.json +1 -1
- package/publisher/publisher.js +101 -9
- package/registry/registry.js +6 -6
- package/server.js +6 -2
- package/translations/Messages.properties +1 -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/library/canonical-resource.js +4 -2
- 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.js +65 -21
- package/tx/operation-context.js +13 -23
- package/tx/params.js +36 -8
- package/tx/provider.js +6 -3
- package/tx/tx-html.js +7 -0
- package/tx/tx.js +12 -13
- package/tx/vs/vs-vsac.js +157 -9
- package/tx/workers/expand.js +100 -96
- package/tx/workers/lookup.js +6 -0
- 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 +1 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ 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
8
|
|
|
9
|
+
## [v0.9.7] - 2026-06-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Support for tx modele to load resources directly
|
|
14
|
+
- Missing overwork protection processing ECL
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fix bug validating secondary displays across languages
|
|
19
|
+
- Fix CORS issue (double headers)
|
|
20
|
+
- Fix czech code in snomed import
|
|
21
|
+
- Many minor validation fixes
|
|
22
|
+
- ECL processing bugs
|
|
23
|
+
- Publishing: better logging, and fix timeout & restart error
|
|
24
|
+
|
|
25
|
+
### Tx Conformance Statement
|
|
26
|
+
|
|
27
|
+
FHIRsmith passed all 2503 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.10)
|
|
28
|
+
|
|
9
29
|
## [v0.9.6] - 2026-05-21
|
|
10
30
|
|
|
11
31
|
### Added
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { PackageContentLoader } = require('./package-manager');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A content loader that scans a folder for JSON FHIR terminology resources
|
|
7
|
+
* (CodeSystem, ValueSet, ConceptMap). It synthesizes the package layout that
|
|
8
|
+
* PackageContentLoader expects so the existing PackageValueSetProvider /
|
|
9
|
+
* PackageConceptMapProvider machinery can be reused without modification.
|
|
10
|
+
*
|
|
11
|
+
* The user's source folder is treated as read-only. Any SQLite caches built
|
|
12
|
+
* by the Package*Provider classes are written to the cacheFolder passed in,
|
|
13
|
+
* not to the source folder.
|
|
14
|
+
*/
|
|
15
|
+
class FolderContentLoader extends PackageContentLoader {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} sourceFolder - Folder to scan for JSON resources
|
|
18
|
+
* @param {string} cacheFolder - Folder where Package*Provider DBs may live
|
|
19
|
+
*/
|
|
20
|
+
constructor(sourceFolder, cacheFolder) {
|
|
21
|
+
super(cacheFolder);
|
|
22
|
+
this.sourceFolder = sourceFolder;
|
|
23
|
+
// Resources are read directly from the source folder rather than from
|
|
24
|
+
// <packageFolder>/package as PackageContentLoader assumes.
|
|
25
|
+
this.packageSubfolder = sourceFolder;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Replaces the standard initialize: there is no package.json or .index.json
|
|
30
|
+
* to read, so we synthesize an index from the JSON files in the folder.
|
|
31
|
+
*/
|
|
32
|
+
async initialize() {
|
|
33
|
+
if (this.loaded) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const stat = await fs.stat(this.sourceFolder);
|
|
39
|
+
if (!stat.isDirectory()) {
|
|
40
|
+
throw new Error(`Folder source path is not a directory: ${this.sourceFolder}`);
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === 'ENOENT') {
|
|
44
|
+
throw new Error(`Folder source path does not exist: ${this.sourceFolder}`);
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Synthesize package metadata so id()/version()/pid() return something usable.
|
|
50
|
+
this.package = {
|
|
51
|
+
name: 'folder.' + path.basename(this.sourceFolder),
|
|
52
|
+
version: '0.0.0',
|
|
53
|
+
fhirVersions: ['5.0.0']
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const files = [];
|
|
57
|
+
const entries = await fs.readdir(this.sourceFolder, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.isFile()) continue;
|
|
60
|
+
if (!entry.name.toLowerCase().endsWith('.json')) continue;
|
|
61
|
+
|
|
62
|
+
const fullPath = path.join(this.sourceFolder, entry.name);
|
|
63
|
+
let json;
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
66
|
+
json = JSON.parse(content);
|
|
67
|
+
} catch {
|
|
68
|
+
// Skip unreadable / non-JSON files rather than failing the whole load.
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!json || typeof json !== 'object') continue;
|
|
73
|
+
const rt = json.resourceType;
|
|
74
|
+
if (rt !== 'CodeSystem' && rt !== 'ValueSet' && rt !== 'ConceptMap') continue;
|
|
75
|
+
|
|
76
|
+
files.push({
|
|
77
|
+
filename: entry.name,
|
|
78
|
+
resourceType: rt,
|
|
79
|
+
id: json.id,
|
|
80
|
+
url: json.url,
|
|
81
|
+
version: json.version
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.index = { files, 'index-version': 2 };
|
|
86
|
+
this.buildIndexes();
|
|
87
|
+
this.loaded = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { FolderContentLoader };
|
|
@@ -127,12 +127,8 @@ class NpmProjectorModule {
|
|
|
127
127
|
* Set up Express routes
|
|
128
128
|
*/
|
|
129
129
|
setupRoutes() {
|
|
130
|
-
// CORS
|
|
131
|
-
|
|
132
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
133
|
-
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
134
|
-
next();
|
|
135
|
-
});
|
|
130
|
+
// CORS is handled once at the app level (server.js, from config.server.cors).
|
|
131
|
+
// Do not set Access-Control-* headers here.
|
|
136
132
|
|
|
137
133
|
// Root - module info
|
|
138
134
|
this.router.get('/', (req, res) => {
|
package/package.json
CHANGED
package/publisher/publisher.js
CHANGED
|
@@ -17,6 +17,7 @@ class PublisherModule {
|
|
|
17
17
|
this.logger = null;
|
|
18
18
|
this.taskProcessor = null;
|
|
19
19
|
this.isProcessing = false;
|
|
20
|
+
this.activeTaskIds = new Set();
|
|
20
21
|
this.shutdownRequested = false;
|
|
21
22
|
this.stats = stats;
|
|
22
23
|
}
|
|
@@ -308,13 +309,19 @@ class PublisherModule {
|
|
|
308
309
|
if (this.shutdownRequested) return;
|
|
309
310
|
|
|
310
311
|
if (this.isProcessing) {
|
|
312
|
+
// A publication can legitimately run for longer than an hour (go-publish on a
|
|
313
|
+
// large IG), so never reset isProcessing while a task is in flight — doing so
|
|
314
|
+
// lets the poller start a second, concurrent run of the same task, which then
|
|
315
|
+
// fails on 'ig-registry already exists' and stomps the real run's status.
|
|
316
|
+
// Recovery from a genuinely hung IG Publisher is handled by the
|
|
317
|
+
// igPublisherTimeoutMinutes kill in runIgPublisher/runPublisherGoPublish.
|
|
311
318
|
const stuckMs = this.isProcessingStarted ? Date.now() - this.isProcessingStarted : 0;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
|
|
316
|
-
return;
|
|
319
|
+
const warnAfterMs = ((this.config.igPublisherTimeoutMinutes || 60) + 30) * 60 * 1000;
|
|
320
|
+
if (stuckMs > warnAfterMs) {
|
|
321
|
+
this.logger.warn('Task processor still busy after ' + Math.round(stuckMs / 60000) + ' min (tasks: ' +
|
|
322
|
+
Array.from(this.activeTaskIds).join(', ') + ') — possible hang, not starting new work');
|
|
317
323
|
}
|
|
324
|
+
return;
|
|
318
325
|
}
|
|
319
326
|
|
|
320
327
|
await this.processNextTask();
|
|
@@ -325,10 +332,14 @@ class PublisherModule {
|
|
|
325
332
|
this.isProcessing = true;
|
|
326
333
|
this.isProcessingStarted = Date.now();
|
|
327
334
|
|
|
335
|
+
let claimedTaskId = null;
|
|
328
336
|
try {
|
|
329
337
|
// Look for queued tasks first (draft builds)
|
|
330
338
|
let task = await this.getNextQueuedTask();
|
|
331
339
|
if (task) {
|
|
340
|
+
if (this.activeTaskIds.has(task.id)) return; // already being processed
|
|
341
|
+
claimedTaskId = task.id;
|
|
342
|
+
this.activeTaskIds.add(task.id);
|
|
332
343
|
this.stats.task('Publisher', 'Building ' + task.npm_package_id + '#' + task.version);
|
|
333
344
|
await this.processDraftBuild(task);
|
|
334
345
|
this.stats.taskDone('Publisher', 'Built ' + task.npm_package_id + '#' + task.version);
|
|
@@ -338,6 +349,9 @@ class PublisherModule {
|
|
|
338
349
|
// Then look for approved tasks (publishing)
|
|
339
350
|
task = await this.getNextApprovedTask();
|
|
340
351
|
if (task) {
|
|
352
|
+
if (this.activeTaskIds.has(task.id)) return; // already being processed
|
|
353
|
+
claimedTaskId = task.id;
|
|
354
|
+
this.activeTaskIds.add(task.id);
|
|
341
355
|
this.stats.task('Publisher', 'Publishing ' + task.npm_package_id + '#' + task.version);
|
|
342
356
|
await this.processPublication(task);
|
|
343
357
|
this.stats.taskDone('Publisher', 'Published ' + task.npm_package_id + '#' + task.version);
|
|
@@ -349,6 +363,9 @@ class PublisherModule {
|
|
|
349
363
|
this.logger.error('Error in task processor:', error);
|
|
350
364
|
this.stats.taskError('Publisher', 'Error: ' + error.message);
|
|
351
365
|
} finally {
|
|
366
|
+
if (claimedTaskId !== null) {
|
|
367
|
+
this.activeTaskIds.delete(claimedTaskId);
|
|
368
|
+
}
|
|
352
369
|
this.isProcessing = false;
|
|
353
370
|
}
|
|
354
371
|
}
|
|
@@ -390,6 +407,9 @@ class PublisherModule {
|
|
|
390
407
|
fields.push('waiting_approval_at = CURRENT_TIMESTAMP');
|
|
391
408
|
} else if (status === 'complete') {
|
|
392
409
|
fields.push('completed_at = CURRENT_TIMESTAMP');
|
|
410
|
+
// A successful completion invalidates any earlier failure (e.g. from a
|
|
411
|
+
// superseded duplicate run) — don't let a stale message outlive success.
|
|
412
|
+
fields.push('failure_reason = NULL');
|
|
393
413
|
} else if (status === 'failed') {
|
|
394
414
|
fields.push('failed_at = CURRENT_TIMESTAMP');
|
|
395
415
|
}
|
|
@@ -414,6 +434,28 @@ class PublisherModule {
|
|
|
414
434
|
});
|
|
415
435
|
}
|
|
416
436
|
|
|
437
|
+
// Update arbitrary task fields WITHOUT touching status. Use this from long-running
|
|
438
|
+
// work (e.g. saving the announcement mid-publication) — writing back the in-memory
|
|
439
|
+
// task.status can resurrect a stale status and cause the poller to re-run the task.
|
|
440
|
+
async updateTaskFields(taskId, fields) {
|
|
441
|
+
const keys = Object.keys(fields);
|
|
442
|
+
if (keys.length === 0) return;
|
|
443
|
+
const assignments = keys.map(key => key + ' = ?');
|
|
444
|
+
const values = keys.map(key => fields[key]);
|
|
445
|
+
values.push(taskId);
|
|
446
|
+
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
this.db.run(
|
|
449
|
+
'UPDATE tasks SET ' + assignments.join(', ') + ' WHERE id = ?',
|
|
450
|
+
values,
|
|
451
|
+
(err) => {
|
|
452
|
+
if (err) reject(err);
|
|
453
|
+
else resolve();
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
417
459
|
async logTaskMessage(taskId, level, message) {
|
|
418
460
|
return new Promise((resolve) => {
|
|
419
461
|
this.db.run(
|
|
@@ -622,14 +664,38 @@ class PublisherModule {
|
|
|
622
664
|
// Create log file stream
|
|
623
665
|
const logStream = fs.createWriteStream(logFile);
|
|
624
666
|
|
|
667
|
+
const buildStart = Date.now();
|
|
668
|
+
let lastDataAt = Date.now();
|
|
669
|
+
|
|
625
670
|
java.stdout.on('data', (data) => {
|
|
671
|
+
lastDataAt = Date.now();
|
|
626
672
|
logStream.write(data);
|
|
627
673
|
});
|
|
628
674
|
|
|
629
675
|
java.stderr.on('data', (data) => {
|
|
676
|
+
lastDataAt = Date.now();
|
|
630
677
|
logStream.write(data);
|
|
631
678
|
});
|
|
632
679
|
|
|
680
|
+
// Heartbeat: emit a status line every 60s regardless of stdout activity,
|
|
681
|
+
// so silent phases of the Publisher (e.g. "Validating Resources") still
|
|
682
|
+
// surface a signal-of-life in the task log.
|
|
683
|
+
const heartbeat = setInterval(async () => {
|
|
684
|
+
const elapsedMs = Date.now() - buildStart;
|
|
685
|
+
const sinceDataMs = Date.now() - lastDataAt;
|
|
686
|
+
let logKb = 0;
|
|
687
|
+
try { logKb = Math.round(fs.statSync(logFile).size / 1024); } catch (_) {}
|
|
688
|
+
const elapsedMin = Math.floor(elapsedMs / 60000);
|
|
689
|
+
const elapsedSec = Math.floor(elapsedMs / 1000) % 60;
|
|
690
|
+
const idleSec = Math.floor(sinceDataMs / 1000);
|
|
691
|
+
await this.logTaskMessage(
|
|
692
|
+
taskId,
|
|
693
|
+
'info',
|
|
694
|
+
'IG Publisher heartbeat: elapsed ' + elapsedMin + 'm' + elapsedSec + 's, ' +
|
|
695
|
+
'log ' + logKb + ' KB, last output ' + idleSec + 's ago'
|
|
696
|
+
);
|
|
697
|
+
}, 60 * 1000);
|
|
698
|
+
|
|
633
699
|
java.on('close', async (code) => {
|
|
634
700
|
logStream.end();
|
|
635
701
|
|
|
@@ -660,6 +726,7 @@ class PublisherModule {
|
|
|
660
726
|
|
|
661
727
|
java.on('close', () => {
|
|
662
728
|
clearTimeout(timeout);
|
|
729
|
+
clearInterval(heartbeat);
|
|
663
730
|
});
|
|
664
731
|
});
|
|
665
732
|
}
|
|
@@ -789,7 +856,7 @@ class PublisherModule {
|
|
|
789
856
|
if (fs.existsSync(announcementPath)) {
|
|
790
857
|
try {
|
|
791
858
|
const announcement = fs.readFileSync(announcementPath, 'utf8');
|
|
792
|
-
await this.
|
|
859
|
+
await this.updateTaskFields(task.id, { announcement: announcement });
|
|
793
860
|
await this.logTaskMessage(task.id, 'info', 'Announcement text saved (' + announcement.length + ' chars)');
|
|
794
861
|
} catch (err) {
|
|
795
862
|
await this.logTaskMessage(task.id, 'warn', 'Failed to read announcement file: ' + err.message);
|
|
@@ -830,14 +897,37 @@ class PublisherModule {
|
|
|
830
897
|
|
|
831
898
|
const logStream = fs.createWriteStream(logFile);
|
|
832
899
|
|
|
900
|
+
const buildStart = Date.now();
|
|
901
|
+
let lastDataAt = Date.now();
|
|
902
|
+
|
|
833
903
|
java.stdout.on('data', (data) => {
|
|
904
|
+
lastDataAt = Date.now();
|
|
834
905
|
logStream.write(data);
|
|
835
906
|
});
|
|
836
907
|
|
|
837
908
|
java.stderr.on('data', (data) => {
|
|
909
|
+
lastDataAt = Date.now();
|
|
838
910
|
logStream.write(data);
|
|
839
911
|
});
|
|
840
912
|
|
|
913
|
+
// Heartbeat: emit a status line every 60s regardless of stdout activity,
|
|
914
|
+
// so silent phases of the Publisher still surface a signal-of-life.
|
|
915
|
+
const heartbeat = setInterval(async () => {
|
|
916
|
+
const elapsedMs = Date.now() - buildStart;
|
|
917
|
+
const sinceDataMs = Date.now() - lastDataAt;
|
|
918
|
+
let logKb = 0;
|
|
919
|
+
try { logKb = Math.round(fs.statSync(logFile).size / 1024); } catch (_) {}
|
|
920
|
+
const elapsedMin = Math.floor(elapsedMs / 60000);
|
|
921
|
+
const elapsedSec = Math.floor(elapsedMs / 1000) % 60;
|
|
922
|
+
const idleSec = Math.floor(sinceDataMs / 1000);
|
|
923
|
+
await this.logTaskMessage(
|
|
924
|
+
taskId,
|
|
925
|
+
'info',
|
|
926
|
+
'IG Publisher go-publish heartbeat: elapsed ' + elapsedMin + 'm' + elapsedSec + 's, ' +
|
|
927
|
+
'log ' + logKb + ' KB, last output ' + idleSec + 's ago'
|
|
928
|
+
);
|
|
929
|
+
}, 60 * 1000);
|
|
930
|
+
|
|
841
931
|
java.on('close', async (code) => {
|
|
842
932
|
logStream.end();
|
|
843
933
|
if (code === 0) {
|
|
@@ -856,16 +946,18 @@ class PublisherModule {
|
|
|
856
946
|
reject(error);
|
|
857
947
|
});
|
|
858
948
|
|
|
859
|
-
// Timeout
|
|
949
|
+
// Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes)
|
|
950
|
+
const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60;
|
|
860
951
|
const timeout = setTimeout(async () => {
|
|
861
952
|
java.kill();
|
|
862
953
|
logStream.end();
|
|
863
|
-
await this.logTaskMessage(taskId, 'error', 'IG Publisher go-publish timed out after
|
|
954
|
+
await this.logTaskMessage(taskId, 'error', 'IG Publisher go-publish timed out after ' + timeoutMinutes + ' minutes');
|
|
864
955
|
reject(new Error('IG Publisher go-publish timed out'));
|
|
865
|
-
},
|
|
956
|
+
}, timeoutMinutes * 60 * 1000);
|
|
866
957
|
|
|
867
958
|
java.on('close', () => {
|
|
868
959
|
clearTimeout(timeout);
|
|
960
|
+
clearInterval(heartbeat);
|
|
869
961
|
});
|
|
870
962
|
});
|
|
871
963
|
}
|
package/registry/registry.js
CHANGED
|
@@ -758,7 +758,7 @@ class RegistryModule {
|
|
|
758
758
|
case '11000267109': edition = 'KR'; break;
|
|
759
759
|
case '900000001000122104': edition = 'ES-ES'; break;
|
|
760
760
|
case '2011000195101': edition = 'CH'; break;
|
|
761
|
-
case '11000279109': edition = '
|
|
761
|
+
case '11000279109': edition = 'CZ'; break;
|
|
762
762
|
case '999000021000000109': edition = 'UK+Clinical'; break;
|
|
763
763
|
case '5631000179106': edition = 'UY'; break;
|
|
764
764
|
case '21000325107': edition = 'CL'; break;
|
|
@@ -1233,7 +1233,7 @@ class RegistryModule {
|
|
|
1233
1233
|
// FHIR Version field
|
|
1234
1234
|
html += '<p>';
|
|
1235
1235
|
html += '<label for="fhirVersion" class="form-label fw-bold">FHIR Version <span class="text-danger">*</span></label>';
|
|
1236
|
-
html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
|
|
1236
|
+
html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
|
|
1237
1237
|
value="${escape(fhirVersion)}" required>`;
|
|
1238
1238
|
html += '</p>';
|
|
1239
1239
|
html += '<p class="text-muted small">Examples: R4, 4.0.1, 5.0.0, etc.</p>';
|
|
@@ -1241,7 +1241,7 @@ class RegistryModule {
|
|
|
1241
1241
|
html += '<div class="alert alert-info">Either Code System URL or Value Set URL must be provided:</div>';
|
|
1242
1242
|
html += '<p>';
|
|
1243
1243
|
html += '<label for="url" class="form-label fw-bold">Code System URL</label>';
|
|
1244
|
-
html += `<input type="url" class="form-control" id="url" name="url"
|
|
1244
|
+
html += `<input type="url" class="form-control" id="url" name="url"
|
|
1245
1245
|
value="${escape(url)}">`;
|
|
1246
1246
|
html += '</p>';
|
|
1247
1247
|
html += '<p class="text-muted small">Example: http://loinc.org</p>';
|
|
@@ -1249,7 +1249,7 @@ class RegistryModule {
|
|
|
1249
1249
|
// ValueSet URL field - now vertical
|
|
1250
1250
|
html += '<p>';
|
|
1251
1251
|
html += '<label for="valueSet" class="form-label fw-bold">Value Set URL</label>';
|
|
1252
|
-
html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
|
|
1252
|
+
html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
|
|
1253
1253
|
value="${escape(valueSet)}">`;
|
|
1254
1254
|
html += '</p>';
|
|
1255
1255
|
html += '<p class="text-muted small">Example: http://hl7.org/fhir/ValueSet/observation-codes</p>';
|
|
@@ -1257,7 +1257,7 @@ class RegistryModule {
|
|
|
1257
1257
|
|
|
1258
1258
|
// Authoritative Only checkbox
|
|
1259
1259
|
html += '<p>';
|
|
1260
|
-
html += `<input type="checkbox" class="form-check-input" id="authoritativeOnly"
|
|
1260
|
+
html += `<input type="checkbox" class="form-check-input" id="authoritativeOnly"
|
|
1261
1261
|
name="authoritativeOnly" value="true" ${authoritativeOnly ? 'checked' : ''}>`;
|
|
1262
1262
|
html += '<label class="form-check-label" for="authoritativeOnly"> Show only authoritative servers</label>';
|
|
1263
1263
|
html += '</p>';
|
|
@@ -1276,7 +1276,7 @@ class RegistryModule {
|
|
|
1276
1276
|
document.querySelector('form').addEventListener('submit', function(e) {
|
|
1277
1277
|
const url = document.getElementById('url').value.trim();
|
|
1278
1278
|
const valueSet = document.getElementById('valueSet').value.trim();
|
|
1279
|
-
|
|
1279
|
+
|
|
1280
1280
|
if (!url && !valueSet) {
|
|
1281
1281
|
e.preventDefault();
|
|
1282
1282
|
alert('You must provide either a Code System URL or a Value Set URL');
|
package/server.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
const express = require('express');
|
|
10
|
-
|
|
10
|
+
const cors = require('cors');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const os = require('os');
|
|
@@ -82,7 +82,11 @@ app.use((req, res, next) => {
|
|
|
82
82
|
next();
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
// app
|
|
85
|
+
// Single, app-level CORS policy for all modules, driven by config.server.cors.
|
|
86
|
+
// This also handles OPTIONS preflight, so modules must NOT set their own
|
|
87
|
+
// Access-Control-* headers (doing so stacks a second CORS layer and produces
|
|
88
|
+
// duplicate, conflicting headers that browsers reject).
|
|
89
|
+
app.use(cors(config.server.cors));
|
|
86
90
|
|
|
87
91
|
// Module instances
|
|
88
92
|
const modules = {};
|
|
@@ -544,7 +544,7 @@ NO_VALID_DISPLAY_FOUND_LANG_NONE_other = ''{5}'' is the default display; the cod
|
|
|
544
544
|
NO_VALID_DISPLAY_FOUND_LANG_SOME_one = ''{5}'' is the default display; no valid Display Names found for {1}#{2} in the language {4}
|
|
545
545
|
NO_VALID_DISPLAY_FOUND_LANG_SOME_other = ''{5}'' is the default display; no valid Display Names found for {1}#{2} in the language {4}
|
|
546
546
|
NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_ERR = Wrong Display Name ''{0}'' for {1}#{2}. There are no valid display names found for language(s) ''{3}''. Default display is ''{4}''
|
|
547
|
-
NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK = There are no valid display names found for the code {1}#{2} for language(s) ''{3}''. The display is ''{
|
|
547
|
+
NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK = There are no valid display names found for the code {1}#{2} for language(s) ''{3}''. The display is ''{0}'' which is a valid display for the default language
|
|
548
548
|
NO_VALID_DISPLAY_FOUND_one = No valid Display Names found for {1}#{2} in the language {4}
|
|
549
549
|
NO_VALID_DISPLAY_FOUND_other = No valid Display Names found for {1}#{2} in the languages {4}
|
|
550
550
|
No_validator_configured = No validator configured
|
package/tx/cs/cs-cs.js
CHANGED
|
@@ -1140,6 +1140,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1140
1140
|
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1141
1141
|
|
|
1142
1142
|
for (const concept of allConcepts) {
|
|
1143
|
+
this.opContext.deadCheck('cs:searchFilter');
|
|
1143
1144
|
const rating = this._calculateSearchRating(concept, searchTerm);
|
|
1144
1145
|
if (rating > 0) {
|
|
1145
1146
|
results.add(concept, rating);
|
|
@@ -1285,6 +1286,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1285
1286
|
|
|
1286
1287
|
const allCodes = this.codeSystem.getAllCodes();
|
|
1287
1288
|
for (const code of allCodes) {
|
|
1289
|
+
this.opContext.deadCheck('cs:conceptFilter:is-not-a');
|
|
1288
1290
|
if (!excludeSet.has(code)) {
|
|
1289
1291
|
const concept = this.codeSystem.getConceptByCode(code);
|
|
1290
1292
|
if (concept) {
|
|
@@ -1316,6 +1318,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1316
1318
|
const regex = regexUtilities.compile('^' + value + '$');
|
|
1317
1319
|
const allCodes = this.codeSystem.getAllCodes();
|
|
1318
1320
|
for (const code of allCodes) {
|
|
1321
|
+
this.opContext.deadCheck('cs:conceptFilter:regex');
|
|
1319
1322
|
if (regex.test(code)) {
|
|
1320
1323
|
const concept = this.codeSystem.getConceptByCode(code);
|
|
1321
1324
|
if (concept) {
|
|
@@ -1349,6 +1352,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1349
1352
|
}
|
|
1350
1353
|
const descendants = this.codeSystem.getDescendants(ancestorCode);
|
|
1351
1354
|
for (const code of descendants) {
|
|
1355
|
+
this.opContext.deadCheck('cs:addDescendants');
|
|
1352
1356
|
if (code !== ancestorCode) {
|
|
1353
1357
|
const concept = this.codeSystem.getConceptByCode(code);
|
|
1354
1358
|
if (concept) {
|
|
@@ -1370,6 +1374,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1370
1374
|
if (concept) {
|
|
1371
1375
|
const descendants = this.codeSystem.getChildren(parentCode);
|
|
1372
1376
|
for (const code of descendants) {
|
|
1377
|
+
this.opContext.deadCheck('cs:addChildren');
|
|
1373
1378
|
if (code !== parentCode) { // should not be
|
|
1374
1379
|
const concept = this.codeSystem.getConceptByCode(code);
|
|
1375
1380
|
if (concept) {
|
|
@@ -1393,6 +1398,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1393
1398
|
|
|
1394
1399
|
const allCodes = this.codeSystem.getAllCodes();
|
|
1395
1400
|
for (const code of allCodes) {
|
|
1401
|
+
this.opContext.deadCheck('cs:childExistsFilter');
|
|
1396
1402
|
const hasChildren = this.codeSystem.getChildren(code).length > 0;
|
|
1397
1403
|
if (hasChildren === wantChildren) {
|
|
1398
1404
|
const concept = this.codeSystem.getConceptByCode(code);
|
|
@@ -1419,6 +1425,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1419
1425
|
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1420
1426
|
|
|
1421
1427
|
for (const concept of allConcepts) {
|
|
1428
|
+
this.opContext.deadCheck('cs:propertyFilter');
|
|
1422
1429
|
if (this._conceptMatchesPropertyFilter(concept, propertyDef, op, value)) {
|
|
1423
1430
|
results.add(concept, 0);
|
|
1424
1431
|
}
|
|
@@ -1498,6 +1505,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1498
1505
|
const allConcepts = this.codeSystem.getAllConcepts();
|
|
1499
1506
|
|
|
1500
1507
|
for (const concept of allConcepts) {
|
|
1508
|
+
this.opContext.deadCheck('cs:knownPropertyFilter');
|
|
1501
1509
|
let matches = false;
|
|
1502
1510
|
|
|
1503
1511
|
if (prop === 'notSelectable') {
|
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -947,6 +947,7 @@ class LoincServices extends BaseCSServices {
|
|
|
947
947
|
reject(err);
|
|
948
948
|
} else {
|
|
949
949
|
for (const row of rows) {
|
|
950
|
+
if (this.opContext) this.opContext.deadCheck('loinc:findRegexMatches');
|
|
950
951
|
if (regex.test(row[valueColumn])) {
|
|
951
952
|
matchingKeys.push(row[keyColumn]);
|
|
952
953
|
}
|
|
@@ -5,7 +5,8 @@ const { AbstractCodeSystemProvider } = require('./cs-provider-api');
|
|
|
5
5
|
*/
|
|
6
6
|
class ListCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
7
7
|
/**
|
|
8
|
-
* {
|
|
8
|
+
* {CodeSystem[]} The preloaded FHIR code systems in this list. This is an
|
|
9
|
+
* array — append with .push(), not Map-style .set().
|
|
9
10
|
*/
|
|
10
11
|
codeSystems = [];
|
|
11
12
|
|