fhirsmith 0.5.6 → 0.7.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 +38 -0
- package/README.md +2 -0
- package/configurations/projector.json +21 -0
- package/configurations/readme.md +5 -0
- package/library/html-server.js +2 -1
- package/library/package-manager.js +37 -34
- package/library/utilities.js +10 -1
- package/library/version-utilities.js +85 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +144 -52
- package/packages/packages.js +15 -7
- package/publisher/publisher.js +15 -3
- package/registry/api.js +173 -191
- package/registry/crawler.js +100 -65
- package/registry/model.js +14 -8
- package/registry/registry.js +5 -0
- package/root-template.html +1 -0
- package/server.js +113 -45
- package/tx/README.md +4 -4
- package/tx/cs/cs-api.js +18 -1
- package/tx/cs/cs-base.js +1 -0
- package/tx/cs/cs-loinc.js +5 -2
- package/tx/cs/cs-provider-api.js +25 -1
- package/tx/cs/cs-provider-list.js +2 -2
- package/tx/cs/cs-rxnorm.js +9 -2
- package/tx/cs/cs-snomed.js +17 -2
- package/tx/html/codesystem-operations.liquid +17 -24
- package/tx/html/valueset-operations.liquid +46 -52
- package/tx/library/canonical-resource.js +6 -1
- package/tx/library/codesystem.js +6 -1
- package/tx/library/renderer.js +81 -7
- package/tx/library.js +145 -13
- package/tx/ocl/README.md +236 -0
- package/tx/ocl/cache/cache-paths.cjs +32 -0
- package/tx/ocl/cache/cache-paths.js +2 -0
- package/tx/ocl/cache/cache-utils.cjs +43 -0
- package/tx/ocl/cache/cache-utils.js +2 -0
- package/tx/ocl/cm-ocl.cjs +531 -0
- package/tx/ocl/cm-ocl.js +1 -105
- package/tx/ocl/cs-ocl.cjs +1779 -0
- package/tx/ocl/cs-ocl.js +1 -38
- package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
- package/tx/ocl/fingerprint/fingerprint.js +2 -0
- package/tx/ocl/http/client.cjs +31 -0
- package/tx/ocl/http/client.js +2 -0
- package/tx/ocl/http/pagination.cjs +98 -0
- package/tx/ocl/http/pagination.js +2 -0
- package/tx/ocl/jobs/background-queue.cjs +200 -0
- package/tx/ocl/jobs/background-queue.js +2 -0
- package/tx/ocl/mappers/concept-mapper.cjs +66 -0
- package/tx/ocl/mappers/concept-mapper.js +2 -0
- package/tx/ocl/model/concept-filter-context.cjs +51 -0
- package/tx/ocl/model/concept-filter-context.js +2 -0
- package/tx/ocl/shared/constants.cjs +15 -0
- package/tx/ocl/shared/constants.js +2 -0
- package/tx/ocl/shared/patches.cjs +224 -0
- package/tx/ocl/shared/patches.js +2 -0
- package/tx/ocl/vs-ocl.cjs +1848 -0
- package/tx/ocl/vs-ocl.js +1 -104
- package/tx/operation-context.js +8 -1
- package/tx/params.js +24 -3
- package/tx/provider.js +51 -2
- package/tx/sct/expressions.js +20 -9
- package/tx/tx-html.js +144 -51
- package/tx/tx.js +10 -2
- package/tx/vs/vs-vsac.js +4 -3
- package/tx/workers/batch-validate.js +3 -2
- package/tx/workers/batch.js +3 -2
- package/tx/workers/expand.js +125 -18
- package/tx/workers/lookup.js +5 -4
- package/tx/workers/read.js +2 -1
- package/tx/workers/related.js +3 -2
- package/tx/workers/search.js +6 -8
- package/tx/workers/subsumes.js +3 -2
- package/tx/workers/translate.js +4 -3
- package/tx/workers/validate.js +132 -40
- package/tx/workers/worker.js +1 -7
- package/tx/xversion/xv-terminologyCapabilities.js +1 -1
package/tx/ocl/vs-ocl.js
CHANGED
|
@@ -1,105 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Abstract base class for value set providers
|
|
3
|
-
* Defines the interface that all value set providers must implement
|
|
4
|
-
*/
|
|
5
|
-
// eslint-disable-next-line no-unused-vars
|
|
6
|
-
class OCLValueSetProvider {
|
|
7
|
-
/**
|
|
8
|
-
* {int} Unique number assigned to this provider
|
|
9
|
-
*/
|
|
10
|
-
spaceId;
|
|
1
|
+
module.exports = require('./vs-ocl.cjs');
|
|
11
2
|
|
|
12
|
-
/**
|
|
13
|
-
* ensure that the ids on the value sets are unique, if they are
|
|
14
|
-
* in the global namespace
|
|
15
|
-
*
|
|
16
|
-
* @param {Set<String>} ids
|
|
17
|
-
*/
|
|
18
|
-
// eslint-disable-next-line no-unused-vars
|
|
19
|
-
assignIds(ids) {
|
|
20
|
-
throw new Error('assignIds must be implemented by AbstractValueSetProvider subclass');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Fetches a specific value set by URL and version
|
|
25
|
-
* @param {string} url - The URL/identifier of the value set
|
|
26
|
-
* @param {string} version - The version of the value set
|
|
27
|
-
* @returns {Promise<ValueSet>} The requested value set
|
|
28
|
-
* @throws {Error} Must be implemented by subclasses
|
|
29
|
-
*/
|
|
30
|
-
// eslint-disable-next-line no-unused-vars
|
|
31
|
-
async fetchValueSet(url, version) {
|
|
32
|
-
throw new Error('fetchValueSet must be implemented by subclass');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Fetches a specific value set by id. ValueSet providers must enforce that value set ids are unique
|
|
37
|
-
* either globally (as enforced by assignIds) or in their space
|
|
38
|
-
*
|
|
39
|
-
* @param {string} id - The id of the value set
|
|
40
|
-
* @returns {Promise<ValueSet>} The requested value set
|
|
41
|
-
* @throws {Error} Must be implemented by subclasses
|
|
42
|
-
*/
|
|
43
|
-
// eslint-disable-next-line no-unused-vars
|
|
44
|
-
async fetchValueSetById(id) {
|
|
45
|
-
throw new Error('fetchValueSetById must be implemented by subclass');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Searches for value sets based on provided criteria
|
|
50
|
-
* @param {Array<{name: string, value: string}>} searchParams - List of name/value pairs for search criteria
|
|
51
|
-
* @returns {Promise<Array<ValueSet>>} List of matching value sets
|
|
52
|
-
* @throws {Error} Must be implemented by subclasses
|
|
53
|
-
*/
|
|
54
|
-
// eslint-disable-next-line no-unused-vars
|
|
55
|
-
async searchValueSets(searchParams, elements = null) {
|
|
56
|
-
throw new Error('searchValueSets must be implemented by subclass');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
*
|
|
61
|
-
* @returns {number} total number of value sets
|
|
62
|
-
*/
|
|
63
|
-
vsCount() {
|
|
64
|
-
return 0;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Validates search parameters
|
|
69
|
-
* @param {Array<{name: string, value: string}>} searchParams - Search parameters to validate
|
|
70
|
-
* @protected
|
|
71
|
-
*/
|
|
72
|
-
_validateSearchParams(searchParams) {
|
|
73
|
-
if (!Array.isArray(searchParams)) {
|
|
74
|
-
throw new Error('Search parameters must be an array');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
for (const param of searchParams) {
|
|
78
|
-
if (!param || typeof param !== 'object') {
|
|
79
|
-
throw new Error('Each search parameter must be an object');
|
|
80
|
-
}
|
|
81
|
-
if (typeof param.name !== 'string' || typeof param.value !== 'string') {
|
|
82
|
-
throw new Error('Search parameter must have string name and value properties');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validates URL and version parameters
|
|
89
|
-
* @param {string} url - URL to validate
|
|
90
|
-
* @param {string} version - Version to validate
|
|
91
|
-
* @protected
|
|
92
|
-
*/
|
|
93
|
-
_validateFetchParams(url, version) {
|
|
94
|
-
if (typeof url !== 'string' || !url.trim()) {
|
|
95
|
-
throw new Error('URL must be a non-empty string');
|
|
96
|
-
}
|
|
97
|
-
if (version != null && typeof version !== 'string') {
|
|
98
|
-
throw new Error('Version must be a string');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
module.exports = {
|
|
104
|
-
AbstractValueSetProvider
|
|
105
|
-
};
|
package/tx/operation-context.js
CHANGED
|
@@ -19,6 +19,12 @@ function isDebugging() {
|
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function debugLog(error, message) {
|
|
23
|
+
if (isDebugging()) {
|
|
24
|
+
console.log(error, message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
|
|
23
29
|
class TimeTracker {
|
|
24
30
|
constructor() {
|
|
@@ -584,5 +590,6 @@ module.exports = {
|
|
|
584
590
|
TimeTracker,
|
|
585
591
|
ResourceCache,
|
|
586
592
|
ExpansionCache,
|
|
587
|
-
isDebugging
|
|
593
|
+
isDebugging,
|
|
594
|
+
debugLog
|
|
588
595
|
};
|
package/tx/params.js
CHANGED
|
@@ -66,6 +66,7 @@ class TxParameters {
|
|
|
66
66
|
this.FDisplayWarning = false;
|
|
67
67
|
this.FMembershipOnly = false;
|
|
68
68
|
this.FDiagnostics = false;
|
|
69
|
+
this.FVersionsMatch = false;
|
|
69
70
|
|
|
70
71
|
this.hasActiveOnly = false;
|
|
71
72
|
this.hasExcludeNested = false;
|
|
@@ -77,6 +78,7 @@ class TxParameters {
|
|
|
77
78
|
this.hasDefaultToLatestVersion = false;
|
|
78
79
|
this.hasDisplayWarning = false;
|
|
79
80
|
this.hasMembershipOnly = false;
|
|
81
|
+
this.hasVersionsMatch = false;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
readParams(params) {
|
|
@@ -199,6 +201,10 @@ class TxParameters {
|
|
|
199
201
|
if (getValuePrimitive(p) == true) this.membershipOnly = true;
|
|
200
202
|
break;
|
|
201
203
|
}
|
|
204
|
+
case 'versionsMatch' : {
|
|
205
|
+
if (getValuePrimitive(p) == true) this.FVersionsMatch = true;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
202
208
|
case 'profile' : {
|
|
203
209
|
let value = p.resource;
|
|
204
210
|
if (value && (value.resourceType === 'Parameters' || value.resourceType === 'ExpansionProfile')) {
|
|
@@ -387,6 +393,15 @@ class TxParameters {
|
|
|
387
393
|
this.hasMembershipOnly = true;
|
|
388
394
|
}
|
|
389
395
|
|
|
396
|
+
get versionsMatch() {
|
|
397
|
+
return this.FVersionsMatch;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
set versionsMatch(value) {
|
|
401
|
+
this.FVersionsMatch = value;
|
|
402
|
+
this.hasVersionsMatch = true;
|
|
403
|
+
}
|
|
404
|
+
e
|
|
390
405
|
get versionRules() {
|
|
391
406
|
return this.FVersionRules;
|
|
392
407
|
}
|
|
@@ -412,6 +427,10 @@ class TxParameters {
|
|
|
412
427
|
if (name === 'designation') {
|
|
413
428
|
this.designations.push(getValuePrimitive(value));
|
|
414
429
|
}
|
|
430
|
+
|
|
431
|
+
if (name === 'versionsMatch') {
|
|
432
|
+
this.versionsMatch = getValuePrimitive(value) === 'true';
|
|
433
|
+
}
|
|
415
434
|
}
|
|
416
435
|
}
|
|
417
436
|
|
|
@@ -502,6 +521,7 @@ class TxParameters {
|
|
|
502
521
|
b('include-designations', this.FIncludeDesignations);
|
|
503
522
|
b('include-definition', this.FIncludeDefinition);
|
|
504
523
|
b('membership-only', this.FMembershipOnly);
|
|
524
|
+
b('versions-match', this.FVersionsMatch);
|
|
505
525
|
b('default-to-latest', this.FDefaultToLatestVersion);
|
|
506
526
|
b('display-warning', this.FDisplayWarning);
|
|
507
527
|
|
|
@@ -526,11 +546,11 @@ class TxParameters {
|
|
|
526
546
|
};
|
|
527
547
|
|
|
528
548
|
let s = '|'+this.count+'|'+this.limit+'|'+this.offset+
|
|
529
|
-
this.FUid + '|' + b(this.FMembershipOnly) + '|' + this.FProperties.join(',') + '|' +
|
|
549
|
+
this.FUid + '|' + b(this.FMembershipOnly) + '|' + b(this.FVersionsMatch)+'|' + this.FProperties.join(',') + '|' +
|
|
530
550
|
b(this.FActiveOnly) + b(this.FDisplayWarning) + b(this.FExcludeNested) + b(this.FGenerateNarrative) + b(this.FExcludeNotForUI) + b(this.FExcludePostCoordinated) +
|
|
531
551
|
b(this.FIncludeDesignations) + b(this.FIncludeDefinition) + b(this.hasActiveOnly) + b(this.hasExcludeNested) + b(this.hasGenerateNarrative) +
|
|
532
552
|
b(this.hasExcludeNotForUI) + b(this.hasExcludePostCoordinated) + b(this.hasIncludeDesignations) + this.sort+'|'+
|
|
533
|
-
b(this.hasIncludeDefinition) + b(this.hasDefaultToLatestVersion) + b(this.hasDisplayWarning) + b(this.hasExcludeNotForUI) + b(this.hasMembershipOnly) + b(this.FDefaultToLatestVersion);
|
|
553
|
+
b(this.hasIncludeDefinition) + b(this.hasDefaultToLatestVersion) + b(this.hasDisplayWarning) + b(this.hasExcludeNotForUI) + b(this.hasMembershipOnly) + b(this.hasVersionsMatch) + b(this.FDefaultToLatestVersion);
|
|
534
554
|
|
|
535
555
|
if (this.hasHTTPLanguages) {
|
|
536
556
|
s = s + this.FHTTPLanguages.asString(true) + '|';
|
|
@@ -577,6 +597,7 @@ class TxParameters {
|
|
|
577
597
|
this.FIncludeDefinition = other.FIncludeDefinition;
|
|
578
598
|
this.FUid = other.FUid;
|
|
579
599
|
this.FMembershipOnly = other.FMembershipOnly;
|
|
600
|
+
this.FVersionsMatch = other.FVersionsMatch;
|
|
580
601
|
this.FDefaultToLatestVersion = other.FDefaultToLatestVersion;
|
|
581
602
|
this.FDisplayWarning = other.FDisplayWarning;
|
|
582
603
|
this.FDiagnostics = other.FDiagnostics;
|
|
@@ -588,7 +609,7 @@ class TxParameters {
|
|
|
588
609
|
this.hasIncludeDesignations = other.hasIncludeDesignations;
|
|
589
610
|
this.hasIncludeDefinition = other.hasIncludeDefinition;
|
|
590
611
|
this.hasDefaultToLatestVersion = other.hasDefaultToLatestVersion;
|
|
591
|
-
this.
|
|
612
|
+
this.hasVersionsMatch = other.hasVersionsMatch;
|
|
592
613
|
this.hasDisplayWarning = other.hasDisplayWarning;
|
|
593
614
|
this.sort = other.sort;
|
|
594
615
|
|
package/tx/provider.js
CHANGED
|
@@ -23,6 +23,7 @@ const {PackageConceptMapProvider} = require("./cm/cm-package");
|
|
|
23
23
|
class Provider {
|
|
24
24
|
i18n;
|
|
25
25
|
fhirVersion;
|
|
26
|
+
context;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* {Map<String, CodeSystemFactoryProvider>} A list of code system factories that contains all the preloaded native code systems
|
|
@@ -34,6 +35,11 @@ class Provider {
|
|
|
34
35
|
*/
|
|
35
36
|
codeSystems;
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* {List<AbstractCodeSystemProvider>} code system providers, for maintaing the code system list
|
|
40
|
+
*/
|
|
41
|
+
codeSystemProviders
|
|
42
|
+
|
|
37
43
|
/**
|
|
38
44
|
* {List<AbstractValueSetProvider>} A list of value set providers that know how to provide value sets by request
|
|
39
45
|
*/
|
|
@@ -153,6 +159,7 @@ class Provider {
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
getCodeSystemById(opContext, id) {
|
|
162
|
+
|
|
156
163
|
// Search through codeSystems map for matching id
|
|
157
164
|
for (const cs of this.codeSystems.values()) {
|
|
158
165
|
if (opContext) opContext.deadCheck('getCodeSystemById');
|
|
@@ -318,9 +325,10 @@ class Provider {
|
|
|
318
325
|
factory = this.codeSystemFactories.get(vurlMM);
|
|
319
326
|
}
|
|
320
327
|
if (factory != null) {
|
|
328
|
+
let vdesc = version == null ? "" : factory.describeVersion(version);
|
|
321
329
|
return {
|
|
322
|
-
link: this.path+"/CodeSystem/"+factory.id(),
|
|
323
|
-
description: factory.
|
|
330
|
+
link: this.path+"/CodeSystem/x-"+factory.id(),
|
|
331
|
+
description: factory.nameBase()+' '+vdesc
|
|
324
332
|
};
|
|
325
333
|
}
|
|
326
334
|
let cs = this.codeSystems.get(vurl);
|
|
@@ -423,6 +431,47 @@ class Provider {
|
|
|
423
431
|
}
|
|
424
432
|
}
|
|
425
433
|
return false;
|
|
434
|
+
}x
|
|
435
|
+
|
|
436
|
+
async updateCodeSystemList() {
|
|
437
|
+
for (let csp of this.codeSystemProviders) {
|
|
438
|
+
let changes = await csp.getCodeSystemChanges(this.fhirVersion, this.context);
|
|
439
|
+
if (changes) {
|
|
440
|
+
for (let cs of changes.added || []) {
|
|
441
|
+
this.addCodeSystem(cs);
|
|
442
|
+
}
|
|
443
|
+
for (let cs of changes.changed || []) {
|
|
444
|
+
this.addCodeSystem(cs);
|
|
445
|
+
}
|
|
446
|
+
for (let cs of changes.deleted || []) {
|
|
447
|
+
this.deleteCodeSystem(cs);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
addCodeSystem(cs) {
|
|
454
|
+
const existing = this.codeSystems.get(cs.url);
|
|
455
|
+
if (!existing || cs.isMoreRecent(existing)) {
|
|
456
|
+
this.codeSystems.set(cs.url, cs);
|
|
457
|
+
}
|
|
458
|
+
if (cs.version) {
|
|
459
|
+
this.codeSystems.set(cs.vurl, cs);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
deleteCodeSystem(cs) {
|
|
464
|
+
this.codeSystems.delete(cs.vurl);
|
|
465
|
+
this.codeSystems.delete(cs.url);
|
|
466
|
+
let existing = null;
|
|
467
|
+
for (let t of this.codeSystems.values()) {
|
|
468
|
+
if (!existing || t.isMoreRecent(existing)) {
|
|
469
|
+
existing = t;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (existing) {
|
|
473
|
+
this.codeSystems.set(cs.url, cs);
|
|
474
|
+
}
|
|
426
475
|
}
|
|
427
476
|
|
|
428
477
|
}
|
package/tx/sct/expressions.js
CHANGED
|
@@ -1469,7 +1469,7 @@ class SnomedExpressionServices {
|
|
|
1469
1469
|
/**
|
|
1470
1470
|
* Validate concept reference
|
|
1471
1471
|
*/
|
|
1472
|
-
checkConcept(concept,
|
|
1472
|
+
checkConcept(concept, limits) {
|
|
1473
1473
|
if (concept.code) {
|
|
1474
1474
|
const conceptId = BigInt(concept.code);
|
|
1475
1475
|
const result = this.concepts.findConcept(conceptId);
|
|
@@ -1480,13 +1480,24 @@ class SnomedExpressionServices {
|
|
|
1480
1480
|
throw new Error(`Concept ${concept.code} not found`);
|
|
1481
1481
|
}
|
|
1482
1482
|
}
|
|
1483
|
-
if (
|
|
1484
|
-
// if a limit is specified, then the concept has to be a specialization of
|
|
1485
|
-
let
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1483
|
+
if (limits && concept.reference) {
|
|
1484
|
+
// if a limit is specified, then the concept has to be a specialization of one of them.
|
|
1485
|
+
let ok = false;
|
|
1486
|
+
for (const limit of limits) {
|
|
1487
|
+
let parentRef = this.concepts.findConcept(limit);
|
|
1488
|
+
let descendentsRef = this.concepts.getAllDesc(parentRef.index);
|
|
1489
|
+
const descendants = this.refs.getReferences(descendentsRef);
|
|
1490
|
+
if (descendants && descendants.includes(concept.reference)) {
|
|
1491
|
+
ok = true;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (!ok) {
|
|
1496
|
+
if (limits.length == 1) {
|
|
1497
|
+
throw new Error(`Concept ${concept.code} is not valid in this context (must be a ${limits[0]})`);
|
|
1498
|
+
} else {
|
|
1499
|
+
throw new Error(`Concept ${concept.code} is not valid in this context (must be a descendent of one of ${limits})`);
|
|
1500
|
+
}
|
|
1490
1501
|
}
|
|
1491
1502
|
}
|
|
1492
1503
|
|
|
@@ -1615,7 +1626,7 @@ class SnomedExpressionServices {
|
|
|
1615
1626
|
* Validate refinement
|
|
1616
1627
|
*/
|
|
1617
1628
|
checkRefinement(refinement) {
|
|
1618
|
-
this.checkConcept(refinement.name, '410662002');
|
|
1629
|
+
this.checkConcept(refinement.name, ['410662002', '106237007']);
|
|
1619
1630
|
this.checkExpression(refinement.value);
|
|
1620
1631
|
}
|
|
1621
1632
|
|
package/tx/tx-html.js
CHANGED
|
@@ -9,6 +9,15 @@ const htmlServer = require('../library/html-server');
|
|
|
9
9
|
const Logger = require('../library/logger');
|
|
10
10
|
const packageJson = require("../package.json");
|
|
11
11
|
const escape = require('escape-html');
|
|
12
|
+
const {ExpandWorker} = require("./workers/expand");
|
|
13
|
+
const ValueSet = require("./library/valueset");
|
|
14
|
+
const {CodeSystemXML} = require("./xml/codesystem-xml");
|
|
15
|
+
const {ValueSetXML} = require("./xml/valueset-xml");
|
|
16
|
+
const {BundleXML} = require("./xml/bundle-xml");
|
|
17
|
+
const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
|
|
18
|
+
const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
|
|
19
|
+
const {ParametersXML} = require("./xml/parameters-xml");
|
|
20
|
+
const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
|
|
12
21
|
|
|
13
22
|
const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
|
|
14
23
|
|
|
@@ -57,10 +66,16 @@ function loadTemplate() {
|
|
|
57
66
|
class TxHtmlRenderer {
|
|
58
67
|
renderer;
|
|
59
68
|
liquid;
|
|
69
|
+
languages;
|
|
70
|
+
i18n;
|
|
71
|
+
path;
|
|
60
72
|
|
|
61
|
-
constructor(renderer, liquid) {
|
|
73
|
+
constructor(renderer, liquid, languages, i18n, path) {
|
|
62
74
|
this.renderer = renderer;
|
|
63
75
|
this.liquid = liquid;
|
|
76
|
+
this.languages = languages;
|
|
77
|
+
this.i18n = i18n;
|
|
78
|
+
this.path = path;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
/**
|
|
@@ -85,7 +100,7 @@ class TxHtmlRenderer {
|
|
|
85
100
|
if (_fmt && typeof _fmt !== 'string') {
|
|
86
101
|
_fmt = null;
|
|
87
102
|
}
|
|
88
|
-
if (_fmt && _fmt == 'html') {
|
|
103
|
+
if (_fmt && (_fmt == 'html' || _fmt.startsWith('html/'))) {
|
|
89
104
|
return true;
|
|
90
105
|
}
|
|
91
106
|
if (!_fmt) {
|
|
@@ -106,6 +121,14 @@ class TxHtmlRenderer {
|
|
|
106
121
|
} else {
|
|
107
122
|
const resourceType = json.resourceType || 'Response';
|
|
108
123
|
|
|
124
|
+
let pfx = resourceType;
|
|
125
|
+
if (req.path.includes('$')) {
|
|
126
|
+
let s = req.path.substring(req.path.indexOf('$') + 1).replace(/[^a-zA-Z].*$/, '');
|
|
127
|
+
switch (s) {
|
|
128
|
+
case 'expand': pfx = "Expansion for "+resourceType;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
109
132
|
if (resourceType === 'Bundle' && json.type === 'searchset') {
|
|
110
133
|
// Extract the resource type being searched from self link or entries
|
|
111
134
|
const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
|
|
@@ -124,11 +147,11 @@ class TxHtmlRenderer {
|
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
if (json.id) {
|
|
127
|
-
return `${
|
|
150
|
+
return `${pfx} ${json.id}`;
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
if (json.name) {
|
|
131
|
-
return `${
|
|
154
|
+
return `${pfx} ${json.name}`;
|
|
132
155
|
}
|
|
133
156
|
|
|
134
157
|
return resourceType;
|
|
@@ -267,15 +290,27 @@ class TxHtmlRenderer {
|
|
|
267
290
|
return await this.buildHomePage(req);
|
|
268
291
|
} else {
|
|
269
292
|
try {
|
|
293
|
+
const _fmt = req?.query?._format || req?.query?.format || req?.body?._format;
|
|
294
|
+
const op = req ? req.path.includes("$") : false;
|
|
270
295
|
const resourceType = json.resourceType;
|
|
271
296
|
|
|
272
297
|
switch (resourceType) {
|
|
273
298
|
case 'Parameters':
|
|
274
299
|
return await this.renderParameters(json);
|
|
275
300
|
case 'CodeSystem':
|
|
276
|
-
return await this.renderCodeSystem(json, inBundle);
|
|
277
|
-
case 'ValueSet':
|
|
278
|
-
|
|
301
|
+
return await this.renderCodeSystem(json, inBundle, _fmt, op);
|
|
302
|
+
case 'ValueSet': {
|
|
303
|
+
let exp = undefined;
|
|
304
|
+
if (!inBundle && !op && (!_fmt || _fmt == 'html')) {
|
|
305
|
+
try {
|
|
306
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
307
|
+
exp = new ValueSet(await worker.handleInternalExpand(json, req));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
exp = error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return await this.renderValueSet(json, inBundle, _fmt, op, exp);
|
|
313
|
+
}
|
|
279
314
|
case 'ConceptMap':
|
|
280
315
|
return await this.renderConceptMap(json, inBundle);
|
|
281
316
|
case 'CapabilityStatement':
|
|
@@ -575,35 +610,88 @@ class TxHtmlRenderer {
|
|
|
575
610
|
/**
|
|
576
611
|
* Render CodeSystem resource
|
|
577
612
|
*/
|
|
578
|
-
async renderCodeSystem(json, inBundle) {
|
|
579
|
-
|
|
613
|
+
async renderCodeSystem(json, inBundle, _fmt) {
|
|
614
|
+
if (inBundle) {
|
|
615
|
+
return await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
|
|
616
|
+
} else {
|
|
617
|
+
let html = `<ul class="nav nav-tabs">`;
|
|
618
|
+
html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
|
|
619
|
+
html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
|
|
620
|
+
html += this.tab(_fmt && _fmt == 'html/xml', 'XML', json.resourceType, 'html/xml', json.id);
|
|
621
|
+
html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
|
|
622
|
+
html += this.tab(_fmt && _fmt == 'html/ops', 'LookUp / Subsumes', json.resourceType, 'html/ops', json.id);
|
|
623
|
+
html += `</ul>`;
|
|
624
|
+
|
|
625
|
+
if (!_fmt || _fmt == 'html') {
|
|
626
|
+
html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
|
|
627
|
+
} else if (_fmt == "html/json") {
|
|
628
|
+
html += await this.renderResourceJson(json);
|
|
629
|
+
} else if (_fmt == "html/xml") {
|
|
630
|
+
html += await this.renderResourceXml(json);
|
|
631
|
+
} else if (_fmt == "html/narrative") {
|
|
632
|
+
html += await this.renderResourceWithNarrative(json, json.text?.div);
|
|
633
|
+
} else if (_fmt == "html/ops") {
|
|
634
|
+
html += await this.liquid.renderFile('codesystem-operations', {
|
|
635
|
+
opsId: this.generateResourceId(),
|
|
636
|
+
vcSystemId: this.generateResourceId(),
|
|
637
|
+
inferSystemId: this.generateResourceId(),
|
|
638
|
+
url: escape(json.url || '')
|
|
639
|
+
});
|
|
640
|
+
}
|
|
580
641
|
|
|
581
|
-
if (!inBundle) {
|
|
582
|
-
html += await this.liquid.renderFile('codesystem-operations', {
|
|
583
|
-
opsId: this.generateResourceId(),
|
|
584
|
-
url: escape(json.url || '')
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
642
|
|
|
588
|
-
|
|
643
|
+
return html;
|
|
644
|
+
}
|
|
589
645
|
}
|
|
590
646
|
|
|
647
|
+
tab(b, name, rtype, type, id) {
|
|
648
|
+
if (b) {
|
|
649
|
+
return `<li class="active"><a href="#">${name}</a></li>`;
|
|
650
|
+
} else {
|
|
651
|
+
return `<li><a href="${this.path}/${rtype}/${id}?_format=${type}">${name}</a></li>`;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
591
654
|
/**
|
|
592
655
|
* Render ValueSet resource
|
|
593
656
|
*/
|
|
594
|
-
async renderValueSet(json, inBundle) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
html
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
657
|
+
async renderValueSet(json, inBundle, _fmt, op, exp) {
|
|
658
|
+
if (inBundle || op) {
|
|
659
|
+
return await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
|
|
660
|
+
} else {
|
|
661
|
+
let html = `<ul class="nav nav-tabs">`;
|
|
662
|
+
html += this.tab(!_fmt || _fmt == 'html', json.resourceType, json.resourceType, 'html', json.id);
|
|
663
|
+
html += this.tab(_fmt && _fmt == 'html/json', 'JSON', json.resourceType, 'html/json', json.id);
|
|
664
|
+
html += this.tab(_fmt && _fmt == 'html/xml', 'XML', json.resourceType, 'html/xml', json.id);
|
|
665
|
+
html += this.tab(_fmt && _fmt == 'html/narrative', 'Original Narrative', json.resourceType, 'html/narrative', json.id);
|
|
666
|
+
html += this.tab(_fmt && _fmt == 'html/ops', 'Expand / Validate', json.resourceType, 'html/ops', json.id);
|
|
667
|
+
html += `</ul>`;
|
|
668
|
+
|
|
669
|
+
if (!_fmt || _fmt == 'html') {
|
|
670
|
+
html += await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
|
|
671
|
+
if (exp) {
|
|
672
|
+
html += "<h2>Expansion</h2>";
|
|
673
|
+
if (exp instanceof ValueSet) {
|
|
674
|
+
html += await this.renderer.renderVSExpansion(exp.jsonObj, false)
|
|
675
|
+
} else {
|
|
676
|
+
html += `<p>Error: `+exp.message+`</p>`;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} else if (_fmt == "html/json") {
|
|
680
|
+
html += await this.renderResourceJson(json);
|
|
681
|
+
} else if (_fmt == "html/xml") {
|
|
682
|
+
html += await this.renderResourceXml(json);
|
|
683
|
+
} else if (_fmt == "html/narrative") {
|
|
684
|
+
html += await this.renderResourceWithNarrative(json, json.text?.div);
|
|
685
|
+
} else if (_fmt == "html/ops") {
|
|
686
|
+
html += await this.liquid.renderFile('valueset-operations', {
|
|
687
|
+
opsId: this.generateResourceId(),
|
|
688
|
+
vcSystemId: this.generateResourceId(),
|
|
689
|
+
inferSystemId: this.generateResourceId(),
|
|
690
|
+
url: escape(json.url || '')
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return html;
|
|
604
694
|
}
|
|
605
|
-
|
|
606
|
-
return html;
|
|
607
695
|
}
|
|
608
696
|
|
|
609
697
|
/**
|
|
@@ -1101,9 +1189,7 @@ class TxHtmlRenderer {
|
|
|
1101
1189
|
* Render resource with text/div narrative and collapsible JSON source
|
|
1102
1190
|
*/
|
|
1103
1191
|
async renderResourceWithNarrative(json, rendered) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
let html = "";
|
|
1192
|
+
let html = '';
|
|
1107
1193
|
|
|
1108
1194
|
// Show text/div narrative if present
|
|
1109
1195
|
if (rendered) {
|
|
@@ -1113,30 +1199,37 @@ class TxHtmlRenderer {
|
|
|
1113
1199
|
} else {
|
|
1114
1200
|
html += '<div class="narrative">(No Narrative)</div>';
|
|
1115
1201
|
}
|
|
1116
|
-
if (json.text && json.text.div) {
|
|
1117
|
-
// Collapsible JSON source
|
|
1118
|
-
html += '<div class="xhtml">';
|
|
1119
|
-
html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
|
|
1120
|
-
html += 'Show Original Narrative</button>';
|
|
1121
|
-
html += `<div id="${resourceId}x" class="original-narrative" style="display: none; margin-top: 10px;">`;
|
|
1122
|
-
|
|
1123
|
-
html += '<div class="narrative">';
|
|
1124
|
-
html += json.text.div; // Already HTML, render as-is
|
|
1125
|
-
html += '</div>';
|
|
1126
|
-
}
|
|
1127
|
-
html += '</div>';
|
|
1128
|
-
html += '</div>';
|
|
1129
1202
|
|
|
1203
|
+
return html;
|
|
1204
|
+
}
|
|
1130
1205
|
|
|
1131
|
-
|
|
1132
|
-
html
|
|
1133
|
-
html += `<
|
|
1134
|
-
html += 'Show JSON Source</button>';
|
|
1135
|
-
html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
|
|
1206
|
+
async renderResourceJson(json) {
|
|
1207
|
+
let html = "";
|
|
1208
|
+
html += `<div class="json-content" style="margin-top: 10px;">`;
|
|
1136
1209
|
html += `<pre>${escape(JSON.stringify(json, null, 2))}</pre>`;
|
|
1137
1210
|
html += '</div>';
|
|
1138
|
-
html
|
|
1211
|
+
return html;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
convertResourceToXml(res) {
|
|
1215
|
+
switch (res.resourceType) {
|
|
1216
|
+
case "CodeSystem" : return CodeSystemXML.toXml(res);
|
|
1217
|
+
case "ValueSet" : return ValueSetXML.toXml(res);
|
|
1218
|
+
case "Bundle" : return BundleXML.toXml(res, this.fhirVersion);
|
|
1219
|
+
case "CapabilityStatement" : return CapabilityStatementXML.toXml(res, "R5");
|
|
1220
|
+
case "TerminologyCapabilities" : return TerminologyCapabilitiesXML.toXml(res, "R5");
|
|
1221
|
+
case "Parameters": return ParametersXML.toXml(res, this.fhirVersion);
|
|
1222
|
+
case "OperationOutcome": return OperationOutcomeXML.toXml(res, this.fhirVersion);
|
|
1223
|
+
}
|
|
1224
|
+
throw new Error(`Resource type ${res.resourceType} not supported in XML`);
|
|
1225
|
+
}
|
|
1139
1226
|
|
|
1227
|
+
async renderResourceXml(json) {
|
|
1228
|
+
let xml = this.convertResourceToXml(json);
|
|
1229
|
+
let html = "";
|
|
1230
|
+
html += `<div class="xml-content" style="margin-top: 10px;">`;
|
|
1231
|
+
html += `<pre>${escape(xml)}</pre>`;
|
|
1232
|
+
html += '</div>';
|
|
1140
1233
|
return html;
|
|
1141
1234
|
}
|
|
1142
1235
|
|
|
@@ -1153,7 +1246,7 @@ class TxHtmlRenderer {
|
|
|
1153
1246
|
}
|
|
1154
1247
|
|
|
1155
1248
|
buildSourceOptions(provider) {
|
|
1156
|
-
let result = '';
|
|
1249
|
+
let result = '<option value=""></option>';
|
|
1157
1250
|
result += `<option value="internal">internal</option>`;
|
|
1158
1251
|
for (let sp of provider.listValueSetSourceCodes()) {
|
|
1159
1252
|
result += `<option value="${sp}">${sp}</option>`;
|
package/tx/tx.js
CHANGED
|
@@ -247,6 +247,14 @@ class TXModule {
|
|
|
247
247
|
if (this.stats) {
|
|
248
248
|
this.stats.addTask("Client Cache", "5 min");
|
|
249
249
|
}
|
|
250
|
+
this.timers.push(setInterval(async () => {
|
|
251
|
+
try {
|
|
252
|
+
await endpointInfo.provider.updateCodeSystemList();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.log.error(`Error updating CodeSystem list for ${endpointPath}: ${error.message}`);
|
|
255
|
+
}
|
|
256
|
+
}, 60 * 1000));
|
|
257
|
+
this.log.info(`CodeSystem list update scheduled for ${endpointPath}`);
|
|
250
258
|
this.timers.push(setInterval(() => {
|
|
251
259
|
endpointInfo.resourceCache.prune(cacheTimeoutMs);
|
|
252
260
|
}, pruneIntervalMs));
|
|
@@ -300,7 +308,7 @@ class TXModule {
|
|
|
300
308
|
// Wrap res.json to intercept and convert to HTML if browser requests it, and log the request
|
|
301
309
|
const originalJson = res.json.bind(res);
|
|
302
310
|
|
|
303
|
-
let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid);
|
|
311
|
+
let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid, this.languages, this.i18n, endpointInfo.path);
|
|
304
312
|
res.json = async (data) => {
|
|
305
313
|
try {
|
|
306
314
|
const duration = Date.now() - req.txStartTime;
|
|
@@ -897,7 +905,7 @@ class TXModule {
|
|
|
897
905
|
router.get('/problems.html', async (req, res) => {
|
|
898
906
|
const start = Date.now();
|
|
899
907
|
try {
|
|
900
|
-
let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid);
|
|
908
|
+
let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, req.txEndpoint.path);
|
|
901
909
|
const problemFinder = new ProblemFinder();
|
|
902
910
|
const content = await problemFinder.scanValueSets(req.txProvider);
|
|
903
911
|
const html = await txhtml.renderPage('Problems', '<h3>ValueSet dependencies on unknown CodeSystem/Versions</h3>'+content, req.txEndpoint, req.txStartTime);
|