fhirsmith 0.3.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 +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
package/tx/tx-html.js
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TX HTML Rendering Module
|
|
3
|
+
//
|
|
4
|
+
// Renders FHIR resources as HTML for browser clients
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const htmlServer = require('../library/html-server');
|
|
9
|
+
const Logger = require('../library/logger');
|
|
10
|
+
const packageJson = require("../package.json");
|
|
11
|
+
|
|
12
|
+
const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
|
|
13
|
+
|
|
14
|
+
const TEMPLATE_PATH = path.join(__dirname, 'html', 'tx-template.html');
|
|
15
|
+
|
|
16
|
+
// Search parameters for the search form
|
|
17
|
+
const SEARCH_PARAMS = [
|
|
18
|
+
{ name: 'url', type: 'text', label: 'URL' },
|
|
19
|
+
{ name: 'version', type: 'text', label: 'Version' },
|
|
20
|
+
{ name: 'name', type: 'text', label: 'Name' },
|
|
21
|
+
{ name: 'title', type: 'text', label: 'Title' },
|
|
22
|
+
{ name: 'status', type: 'select', label: 'Status', options: ['', 'draft', 'active', 'retired', 'unknown'] },
|
|
23
|
+
{ name: 'publisher', type: 'text', label: 'Publisher' },
|
|
24
|
+
{ name: 'description', type: 'text', label: 'Description' },
|
|
25
|
+
{ name: 'identifier', type: 'text', label: 'Identifier' },
|
|
26
|
+
{ name: 'jurisdiction', type: 'text', label: 'Jurisdiction' },
|
|
27
|
+
{ name: 'date', type: 'text', label: 'Date' }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const CODESYSTEM_PARAMS = [
|
|
31
|
+
...SEARCH_PARAMS,
|
|
32
|
+
{ name: 'content-mode', type: 'select', label: 'Content Mode', options: ['', 'not-present', 'example', 'fragment', 'complete', 'supplement'] },
|
|
33
|
+
{ name: 'supplements', type: 'text', label: 'Supplements' },
|
|
34
|
+
{ name: 'system', type: 'text', label: 'System' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const SORT_OPTIONS = ['', 'id', 'url', 'version', 'date', 'name', 'vurl'];
|
|
38
|
+
|
|
39
|
+
const ELEMENT_OPTIONS = ['id', 'url', 'version', 'name', 'title', 'status', 'date', 'publisher', 'description'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load the TX HTML template
|
|
43
|
+
*/
|
|
44
|
+
function loadTemplate() {
|
|
45
|
+
try {
|
|
46
|
+
const templateLoaded = htmlServer.loadTemplate('tx', TEMPLATE_PATH);
|
|
47
|
+
if (!templateLoaded) {
|
|
48
|
+
txHtmlLog.error('Failed to load TX HTML template');
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
txHtmlLog.error(`Failed to load TX HTML template: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TxHtmlRenderer {
|
|
57
|
+
renderer;
|
|
58
|
+
liquid;
|
|
59
|
+
|
|
60
|
+
constructor(renderer, liquid) {
|
|
61
|
+
this.renderer = renderer;
|
|
62
|
+
this.liquid = liquid;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Escape HTML special characters
|
|
67
|
+
*/
|
|
68
|
+
escapeHtml(text) {
|
|
69
|
+
if (text === null || text === undefined) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
if (typeof text !== 'string') {
|
|
73
|
+
return String(text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const map = {
|
|
77
|
+
'&': '&',
|
|
78
|
+
'<': '<',
|
|
79
|
+
'>': '>',
|
|
80
|
+
'"': '"',
|
|
81
|
+
"'": '''
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a page with the TX template
|
|
89
|
+
*/
|
|
90
|
+
renderPage(title, content, endpoint, startTime) {
|
|
91
|
+
const options = {
|
|
92
|
+
version: packageJson.version,
|
|
93
|
+
endpointpath: endpoint.path,
|
|
94
|
+
fhirversion: endpoint.fhirVersion,
|
|
95
|
+
ms: Date.now() - startTime
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return htmlServer.renderPage('tx', title, content, options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if request accepts HTML
|
|
103
|
+
*/
|
|
104
|
+
acceptsHtml(req) {
|
|
105
|
+
const accept = req.headers.accept || '';
|
|
106
|
+
return accept.includes('text/html');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build page title from JSON response
|
|
111
|
+
*/
|
|
112
|
+
buildTitle(json, req) {
|
|
113
|
+
if (req.path == "/") {
|
|
114
|
+
return "Server Home";
|
|
115
|
+
} else {
|
|
116
|
+
const resourceType = json.resourceType || 'Response';
|
|
117
|
+
|
|
118
|
+
if (resourceType === 'Bundle' && json.type === 'searchset') {
|
|
119
|
+
// Extract the resource type being searched from self link or entries
|
|
120
|
+
const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
|
|
121
|
+
const typeMatch = selfLink.match(/\/(CodeSystem|ValueSet|ConceptMap)\?/);
|
|
122
|
+
if (typeMatch) {
|
|
123
|
+
return `Search: ${typeMatch[1]}`;
|
|
124
|
+
}
|
|
125
|
+
const firstEntry = json.entry?.[0]?.resource;
|
|
126
|
+
const searchedType = firstEntry?.resourceType || 'Resources';
|
|
127
|
+
return `Search: ${searchedType}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (resourceType === 'OperationOutcome') {
|
|
131
|
+
const severity = json.issue?.[0]?.severity || 'info';
|
|
132
|
+
return `${severity.charAt(0).toUpperCase() + severity.slice(1)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (json.id) {
|
|
136
|
+
return `${resourceType}/${json.id}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (json.name) {
|
|
140
|
+
return `${resourceType}: ${json.name}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return resourceType;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// eslint-disable-next-line no-unused-vars
|
|
148
|
+
async buildSearchForm(req, mode, params) {
|
|
149
|
+
const html = await this.liquid.renderFile('search-form', { baseUrl: this.escapeHtml(req.baseUrl) });
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async buildHomePage(req) {
|
|
154
|
+
const provider = req.txProvider;
|
|
155
|
+
|
|
156
|
+
let html = '';
|
|
157
|
+
|
|
158
|
+
// ===== Summary Section =====
|
|
159
|
+
|
|
160
|
+
// Calculate uptime
|
|
161
|
+
const uptimeMs = Date.now() - provider.startTime;
|
|
162
|
+
const uptimeSeconds = Math.floor(uptimeMs / 1000);
|
|
163
|
+
const uptimeDays = Math.floor(uptimeSeconds / 86400);
|
|
164
|
+
const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
|
|
165
|
+
const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
|
|
166
|
+
const uptimeSecs = uptimeSeconds % 60;
|
|
167
|
+
let uptimeStr = '';
|
|
168
|
+
if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
|
|
169
|
+
if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
|
|
170
|
+
if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
|
|
171
|
+
uptimeStr += `${uptimeSecs}s`;
|
|
172
|
+
|
|
173
|
+
// Memory usage
|
|
174
|
+
const memUsage = process.memoryUsage();
|
|
175
|
+
const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
|
|
176
|
+
const heapTotalMB = (memUsage.heapTotal / 1024 / 1024).toFixed(2);
|
|
177
|
+
const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
|
|
178
|
+
|
|
179
|
+
html += '<table class="grid">';
|
|
180
|
+
html += '<tr>';
|
|
181
|
+
html += `<td><strong>FHIR Version:</strong> ${this.escapeHtml(provider.getFhirVersion())}</td>`;
|
|
182
|
+
html += `<td><strong>Uptime:</strong> ${this.escapeHtml(uptimeStr)}</td>`;
|
|
183
|
+
html += `<td><strong>Request Count:</strong> ${provider.requestCount}</td>`;
|
|
184
|
+
html += '</tr>';
|
|
185
|
+
html += '<tr>';
|
|
186
|
+
html += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
|
|
187
|
+
html += `<td><strong>Heap Total:</strong> ${heapTotalMB} MB</td>`;
|
|
188
|
+
html += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
|
|
189
|
+
html += '</tr>';
|
|
190
|
+
|
|
191
|
+
// Count unique code systems
|
|
192
|
+
const uniqueFactorySystems = new Set();
|
|
193
|
+
for (const factory of provider.codeSystemFactories.values()) {
|
|
194
|
+
uniqueFactorySystems.add(factory.system());
|
|
195
|
+
}
|
|
196
|
+
const uniqueCodeSystems = new Set();
|
|
197
|
+
for (const cs of provider.codeSystems.values()) {
|
|
198
|
+
uniqueCodeSystems.add(cs.url);
|
|
199
|
+
}
|
|
200
|
+
html += '<tr>';
|
|
201
|
+
html += `<td><strong>CodeSystem #:</strong> ${new Set([...uniqueFactorySystems, ...uniqueCodeSystems]).size}</td>`;
|
|
202
|
+
|
|
203
|
+
// Count value sets
|
|
204
|
+
let totalValueSets = 0;
|
|
205
|
+
for (const vsp of provider.valueSetProviders) {
|
|
206
|
+
totalValueSets += vsp.vsCount();
|
|
207
|
+
}
|
|
208
|
+
html += `<td><strong>ValueSet #:</strong> ${totalValueSets || 'Unknown'}</td>`;
|
|
209
|
+
|
|
210
|
+
let totalConceptMaps = 0;
|
|
211
|
+
for (const cmp of provider.conceptMapProviders) {
|
|
212
|
+
totalConceptMaps += cmp.cmCount();
|
|
213
|
+
}
|
|
214
|
+
html += `<td><strong>ConceptMap #:</strong> ${totalConceptMaps || 'Unknown'}</td>`;
|
|
215
|
+
html += '</tr>';
|
|
216
|
+
html += '</table>';
|
|
217
|
+
|
|
218
|
+
html += '<hr/>';
|
|
219
|
+
html += await this.buildSearchForm(req);
|
|
220
|
+
|
|
221
|
+
// ===== Packages and Factories Section =====
|
|
222
|
+
html += '<hr/><h3>Content Sources & Code System Factories</h3>';
|
|
223
|
+
|
|
224
|
+
// List content sources
|
|
225
|
+
html += '<h6>Content Sources</h6>';
|
|
226
|
+
if (provider.contentSources && provider.contentSources.length > 0) {
|
|
227
|
+
const sorted = [...provider.contentSources].sort();
|
|
228
|
+
html += '<ul>';
|
|
229
|
+
for (const source of sorted) {
|
|
230
|
+
html += `<li>${this.escapeHtml(source)}</li>`;
|
|
231
|
+
}
|
|
232
|
+
html += '</ul>';
|
|
233
|
+
} else {
|
|
234
|
+
html += '<p><em>No content sources available</em></p>';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Code System Factories table
|
|
238
|
+
// Code System Factories table
|
|
239
|
+
html += '<h6 class="mt-4">External CodeSystems</h6>';
|
|
240
|
+
html += '<table class="grid">';
|
|
241
|
+
html += '<thead><tr><th>Name</th><th>URI</th><th>Version</th><th>Use Count</th></tr></thead>';
|
|
242
|
+
html += '<tbody>';
|
|
243
|
+
|
|
244
|
+
// Deduplicate factories and sort by system URL
|
|
245
|
+
const seenFactories = new Set();
|
|
246
|
+
const uniqueFactories = [];
|
|
247
|
+
for (const factory of provider.codeSystemFactories.values()) {
|
|
248
|
+
const key = factory.system() + '|' + (factory.version() || '');
|
|
249
|
+
if (!seenFactories.has(key)) {
|
|
250
|
+
seenFactories.add(key);
|
|
251
|
+
uniqueFactories.push(factory);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
uniqueFactories.sort((a, b) => a.name().localeCompare(b.name()));
|
|
255
|
+
|
|
256
|
+
for (const factory of uniqueFactories) {
|
|
257
|
+
html += '<tr>';
|
|
258
|
+
html += `<td>${this.escapeHtml(factory.name())}</td>`;
|
|
259
|
+
html += `<td>${this.escapeHtml(factory.system())}</td>`;
|
|
260
|
+
html += `<td>${this.escapeHtml(factory.version() || '-')}</td>`;
|
|
261
|
+
html += `<td>${factory.useCount ? factory.useCount() : '-'}</td>`;
|
|
262
|
+
html += '</tr>';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
html += '</tbody></table>';
|
|
266
|
+
html += '</div></div>';
|
|
267
|
+
|
|
268
|
+
return html;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Main render - determines what to render based on resource type
|
|
273
|
+
*/
|
|
274
|
+
async render(json, req, inBundle = false) {
|
|
275
|
+
if (req && req.path == "/") {
|
|
276
|
+
return await this.buildHomePage(req);
|
|
277
|
+
} else {
|
|
278
|
+
try {
|
|
279
|
+
const resourceType = json.resourceType;
|
|
280
|
+
|
|
281
|
+
switch (resourceType) {
|
|
282
|
+
case 'Parameters':
|
|
283
|
+
return await this.renderParameters(json);
|
|
284
|
+
case 'CodeSystem':
|
|
285
|
+
return await this.renderCodeSystem(json, inBundle);
|
|
286
|
+
case 'ValueSet':
|
|
287
|
+
return await this.renderValueSet(json, inBundle);
|
|
288
|
+
case 'ConceptMap':
|
|
289
|
+
return await this.renderConceptMap(json, inBundle);
|
|
290
|
+
case 'CapabilityStatement':
|
|
291
|
+
return await this.renderCapabilityStatement(json, inBundle);
|
|
292
|
+
case 'TerminologyCapabilities':
|
|
293
|
+
return await this.renderTerminologyCapabilities(json, inBundle);
|
|
294
|
+
case 'Bundle':
|
|
295
|
+
return await this.renderBundle(json, req, inBundle);
|
|
296
|
+
case 'OperationOutcome':
|
|
297
|
+
return await this.renderOperationOutcome(json, req);
|
|
298
|
+
case 'Operations':
|
|
299
|
+
return await this.renderOperationsForm(json, req);
|
|
300
|
+
default:
|
|
301
|
+
return await this.renderGeneric(json, inBundle);
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(error);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Render Parameters resource
|
|
312
|
+
*/
|
|
313
|
+
async renderParameters(json) {
|
|
314
|
+
let html = '<table class="table grid">';
|
|
315
|
+
html += '<thead><tr><th>Name</th><th>Value</th></tr></thead>';
|
|
316
|
+
html += '<tbody>';
|
|
317
|
+
|
|
318
|
+
if (json.parameter && Array.isArray(json.parameter)) {
|
|
319
|
+
for (const param of json.parameter) {
|
|
320
|
+
html += await this.renderParameter(param);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
html += '</tbody></table>';
|
|
325
|
+
|
|
326
|
+
// Collapsible JSON source
|
|
327
|
+
const resourceId = this.generateResourceId();
|
|
328
|
+
html += '<div class="json-source">';
|
|
329
|
+
html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleJsonSource('${resourceId}')">`;
|
|
330
|
+
html += 'Show JSON Source</button>';
|
|
331
|
+
html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
|
|
332
|
+
html += `<pre>${this.escapeHtml(JSON.stringify(json, null, 2))}</pre>`;
|
|
333
|
+
html += '</div>';
|
|
334
|
+
html += '</div>';
|
|
335
|
+
|
|
336
|
+
return html;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Render a single parameter row
|
|
341
|
+
*/
|
|
342
|
+
async renderParameter(param) {
|
|
343
|
+
let html = '<tr>';
|
|
344
|
+
html += `<td>${this.escapeHtml(param.name || '')}</td>`;
|
|
345
|
+
html += '<td>';
|
|
346
|
+
html += await this.renderParameterValue(param);
|
|
347
|
+
html += '</td>';
|
|
348
|
+
html += '</tr>';
|
|
349
|
+
return html;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Render the value portion of a parameter
|
|
354
|
+
*/
|
|
355
|
+
async renderParameterValue(param) {
|
|
356
|
+
// Check for parts (nested parameters)
|
|
357
|
+
if (param.part && Array.isArray(param.part)) {
|
|
358
|
+
let html = '<ul>';
|
|
359
|
+
for (const part of param.part) {
|
|
360
|
+
html += '<li>';
|
|
361
|
+
html += `<strong>${this.escapeHtml(part.name || '')}:</strong> `;
|
|
362
|
+
html += await this.renderParameterValue(part);
|
|
363
|
+
html += '</li>';
|
|
364
|
+
}
|
|
365
|
+
html += '</ul>';
|
|
366
|
+
return html;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check for resource
|
|
370
|
+
if (param.resource) {
|
|
371
|
+
return await this.render(param.resource, null, true);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check for complex datatypes
|
|
375
|
+
if (param.valueCoding) {
|
|
376
|
+
return this.renderCoding(param.valueCoding);
|
|
377
|
+
}
|
|
378
|
+
if (param.valueCodeableConcept) {
|
|
379
|
+
return this.renderCodeableConcept(param.valueCodeableConcept);
|
|
380
|
+
}
|
|
381
|
+
if (param.valueQuantity) {
|
|
382
|
+
return this.renderQuantity(param.valueQuantity);
|
|
383
|
+
}
|
|
384
|
+
if (param.valueAttachment) {
|
|
385
|
+
return this.renderAttachment(param.valueAttachment);
|
|
386
|
+
}
|
|
387
|
+
if (param.valueIdentifier) {
|
|
388
|
+
return this.renderIdentifier(param.valueIdentifier);
|
|
389
|
+
}
|
|
390
|
+
if (param.valuePeriod) {
|
|
391
|
+
return this.renderPeriod(param.valuePeriod);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Primitive types
|
|
395
|
+
if (param.valueString !== undefined) {
|
|
396
|
+
return this.escapeHtml(param.valueString);
|
|
397
|
+
}
|
|
398
|
+
if (param.valueBoolean !== undefined) {
|
|
399
|
+
return param.valueBoolean ? 'true' : 'false';
|
|
400
|
+
}
|
|
401
|
+
if (param.valueInteger !== undefined) {
|
|
402
|
+
return this.escapeHtml(String(param.valueInteger));
|
|
403
|
+
}
|
|
404
|
+
if (param.valueDecimal !== undefined) {
|
|
405
|
+
return this.escapeHtml(String(param.valueDecimal));
|
|
406
|
+
}
|
|
407
|
+
if (param.valueUri !== undefined) {
|
|
408
|
+
return this.escapeHtml(param.valueUri);
|
|
409
|
+
}
|
|
410
|
+
if (param.valueUrl !== undefined) {
|
|
411
|
+
return this.escapeHtml(param.valueUrl);
|
|
412
|
+
}
|
|
413
|
+
if (param.valueCanonical !== undefined) {
|
|
414
|
+
return this.escapeHtml(param.valueCanonical);
|
|
415
|
+
}
|
|
416
|
+
if (param.valueCode !== undefined) {
|
|
417
|
+
return `<code>${this.escapeHtml(param.valueCode)}</code>`;
|
|
418
|
+
}
|
|
419
|
+
if (param.valueDate !== undefined) {
|
|
420
|
+
return this.escapeHtml(param.valueDate);
|
|
421
|
+
}
|
|
422
|
+
if (param.valueDateTime !== undefined) {
|
|
423
|
+
return this.escapeHtml(param.valueDateTime);
|
|
424
|
+
}
|
|
425
|
+
if (param.valueTime !== undefined) {
|
|
426
|
+
return this.escapeHtml(param.valueTime);
|
|
427
|
+
}
|
|
428
|
+
if (param.valueInstant !== undefined) {
|
|
429
|
+
return this.escapeHtml(param.valueInstant);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return '<em>(empty)</em>';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Render Coding datatype
|
|
437
|
+
*/
|
|
438
|
+
async renderCoding(coding) {
|
|
439
|
+
if (!coding) return '';
|
|
440
|
+
|
|
441
|
+
let parts = [];
|
|
442
|
+
if (coding.system) {
|
|
443
|
+
parts.push(this.escapeHtml(coding.system));
|
|
444
|
+
}
|
|
445
|
+
if (coding.code) {
|
|
446
|
+
parts.push(`<code>${this.escapeHtml(coding.code)}</code>`);
|
|
447
|
+
}
|
|
448
|
+
if (coding.display) {
|
|
449
|
+
parts.push(`"${this.escapeHtml(coding.display)}"`);
|
|
450
|
+
}
|
|
451
|
+
if (coding.version) {
|
|
452
|
+
parts.push(`(version: ${this.escapeHtml(coding.version)})`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return parts.join(' | ') || '<em>(empty coding)</em>';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Render CodeableConcept datatype
|
|
460
|
+
*/
|
|
461
|
+
async renderCodeableConcept(cc) {
|
|
462
|
+
if (!cc) return '';
|
|
463
|
+
|
|
464
|
+
let html = '';
|
|
465
|
+
|
|
466
|
+
if (cc.text) {
|
|
467
|
+
html += `<strong>${this.escapeHtml(cc.text)}</strong>`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (cc.coding && Array.isArray(cc.coding) && cc.coding.length > 0) {
|
|
471
|
+
if (cc.text) html += '<br/>';
|
|
472
|
+
html += '<ul style="margin: 0; padding-left: 20px;">';
|
|
473
|
+
for (const coding of cc.coding) {
|
|
474
|
+
html += `<li>${this.renderCoding(coding)}</li>`;
|
|
475
|
+
}
|
|
476
|
+
html += '</ul>';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return html || '<em>(empty CodeableConcept)</em>';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Render Quantity datatype
|
|
484
|
+
*/
|
|
485
|
+
async renderQuantity(qty) {
|
|
486
|
+
if (!qty) return '';
|
|
487
|
+
|
|
488
|
+
let html = '';
|
|
489
|
+
|
|
490
|
+
if (qty.comparator) {
|
|
491
|
+
html += this.escapeHtml(qty.comparator) + ' ';
|
|
492
|
+
}
|
|
493
|
+
if (qty.value !== undefined) {
|
|
494
|
+
html += this.escapeHtml(String(qty.value));
|
|
495
|
+
}
|
|
496
|
+
if (qty.unit) {
|
|
497
|
+
html += ' ' + this.escapeHtml(qty.unit);
|
|
498
|
+
} else if (qty.code) {
|
|
499
|
+
html += ' ' + this.escapeHtml(qty.code);
|
|
500
|
+
}
|
|
501
|
+
if (qty.system) {
|
|
502
|
+
html += ` <small>(${this.escapeHtml(qty.system)})</small>`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return html || '<em>(empty Quantity)</em>';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Render Attachment datatype
|
|
510
|
+
*/
|
|
511
|
+
async renderAttachment(att) {
|
|
512
|
+
if (!att) return '';
|
|
513
|
+
|
|
514
|
+
let html = '';
|
|
515
|
+
|
|
516
|
+
if (att.title) {
|
|
517
|
+
html += `<strong>${this.escapeHtml(att.title)}</strong><br/>`;
|
|
518
|
+
}
|
|
519
|
+
if (att.contentType) {
|
|
520
|
+
html += `Content-Type: ${this.escapeHtml(att.contentType)}<br/>`;
|
|
521
|
+
}
|
|
522
|
+
if (att.url) {
|
|
523
|
+
html += `URL: <a href="${this.escapeHtml(att.url)}">${this.escapeHtml(att.url)}</a><br/>`;
|
|
524
|
+
}
|
|
525
|
+
if (att.size !== undefined) {
|
|
526
|
+
html += `Size: ${this.escapeHtml(String(att.size))} bytes<br/>`;
|
|
527
|
+
}
|
|
528
|
+
if (att.language) {
|
|
529
|
+
html += `Language: ${this.escapeHtml(att.language)}<br/>`;
|
|
530
|
+
}
|
|
531
|
+
if (att.data) {
|
|
532
|
+
html += `<small>(base64 data present, ${att.data.length} chars)</small>`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return html || '<em>(empty Attachment)</em>';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Render Identifier datatype
|
|
540
|
+
*/
|
|
541
|
+
async renderIdentifier(id) {
|
|
542
|
+
if (!id) return '';
|
|
543
|
+
|
|
544
|
+
let parts = [];
|
|
545
|
+
|
|
546
|
+
if (id.use) {
|
|
547
|
+
parts.push(`[${this.escapeHtml(id.use)}]`);
|
|
548
|
+
}
|
|
549
|
+
if (id.type && id.type.text) {
|
|
550
|
+
parts.push(this.escapeHtml(id.type.text));
|
|
551
|
+
}
|
|
552
|
+
if (id.system) {
|
|
553
|
+
parts.push(this.escapeHtml(id.system));
|
|
554
|
+
}
|
|
555
|
+
if (id.value) {
|
|
556
|
+
parts.push(`<strong>${this.escapeHtml(id.value)}</strong>`);
|
|
557
|
+
}
|
|
558
|
+
if (id.period) {
|
|
559
|
+
parts.push(this.renderPeriod(id.period));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return parts.join(' | ') || '<em>(empty Identifier)</em>';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Render Period datatype
|
|
567
|
+
*/
|
|
568
|
+
async renderPeriod(period) {
|
|
569
|
+
if (!period) return '';
|
|
570
|
+
|
|
571
|
+
let html = '';
|
|
572
|
+
|
|
573
|
+
if (period.start && period.end) {
|
|
574
|
+
html = `${this.escapeHtml(period.start)} to ${this.escapeHtml(period.end)}`;
|
|
575
|
+
} else if (period.start) {
|
|
576
|
+
html = `from ${this.escapeHtml(period.start)}`;
|
|
577
|
+
} else if (period.end) {
|
|
578
|
+
html = `until ${this.escapeHtml(period.end)}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return html || '<em>(empty Period)</em>';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Render CodeSystem resource
|
|
586
|
+
*/
|
|
587
|
+
async renderCodeSystem(json, inBundle) {
|
|
588
|
+
let html = await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
|
|
589
|
+
|
|
590
|
+
if (!inBundle) {
|
|
591
|
+
html += await this.liquid.renderFile('codesystem-operations', {
|
|
592
|
+
opsId: this.generateResourceId(),
|
|
593
|
+
url: this.escapeHtml(json.url || '')
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return html;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Render ValueSet resource
|
|
602
|
+
*/
|
|
603
|
+
async renderValueSet(json, inBundle) {
|
|
604
|
+
let html = await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
|
|
605
|
+
|
|
606
|
+
if (!inBundle) {
|
|
607
|
+
html += await this.liquid.renderFile('valueset-operations', {
|
|
608
|
+
opsId: this.generateResourceId(),
|
|
609
|
+
vcSystemId: this.generateResourceId(),
|
|
610
|
+
inferSystemId: this.generateResourceId(),
|
|
611
|
+
url: this.escapeHtml(json.url || '')
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return html;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Render ConceptMap resource
|
|
620
|
+
*/
|
|
621
|
+
// eslint-disable-next-line no-unused-vars
|
|
622
|
+
async renderConceptMap(json, inBundle) {
|
|
623
|
+
return this.renderResourceWithNarrative(json);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Render CapabilityStatement resource
|
|
628
|
+
*/
|
|
629
|
+
// eslint-disable-next-line no-unused-vars
|
|
630
|
+
async renderCapabilityStatement(json, inBundle) {
|
|
631
|
+
return await this.renderResourceWithNarrative(json, await this.renderer.renderCapabilityStatement(json));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// eslint-disable-next-line no-unused-vars
|
|
635
|
+
async renderTerminologyCapabilities(json, inBundle) {
|
|
636
|
+
return await this.renderResourceWithNarrative(json, await this.renderer.renderTerminologyCapabilities(json));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Render OperationOutcome resource
|
|
641
|
+
*/
|
|
642
|
+
async renderOperationOutcome(json) {
|
|
643
|
+
let html = '<div class="operation-outcome">';
|
|
644
|
+
html += `<h4>OperationOutcome</h4>`;
|
|
645
|
+
|
|
646
|
+
if (json.issue && Array.isArray(json.issue)) {
|
|
647
|
+
for (const issue of json.issue) {
|
|
648
|
+
html += '<div class="alert ';
|
|
649
|
+
|
|
650
|
+
// Determine alert style based on this issue's severity
|
|
651
|
+
const severity = issue.severity || 'information';
|
|
652
|
+
switch (severity) {
|
|
653
|
+
case 'error':
|
|
654
|
+
case 'fatal':
|
|
655
|
+
html += 'alert-danger';
|
|
656
|
+
break;
|
|
657
|
+
case 'warning':
|
|
658
|
+
html += 'alert-warning';
|
|
659
|
+
break;
|
|
660
|
+
case 'information':
|
|
661
|
+
html += 'alert-info';
|
|
662
|
+
break;
|
|
663
|
+
default:
|
|
664
|
+
html += 'alert-secondary';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
html += '">';
|
|
668
|
+
html += `<strong>${this.escapeHtml(issue.severity || 'unknown')}:</strong> `;
|
|
669
|
+
html += `[${this.escapeHtml(issue.code || 'unknown')}] `;
|
|
670
|
+
html += this.escapeHtml(issue.diagnostics || issue.details?.text || 'No details');
|
|
671
|
+
html += '</div>';
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
html += '</div>';
|
|
676
|
+
return html;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Render Bundle resource
|
|
681
|
+
*/
|
|
682
|
+
async renderBundle(json, req) {
|
|
683
|
+
if (json.type === 'searchset') {
|
|
684
|
+
return await this.renderSearchBundle(json, req);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Generic bundle rendering
|
|
688
|
+
return await this.renderGenericBundle(json, req);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Render a search result Bundle
|
|
693
|
+
*/
|
|
694
|
+
async renderSearchBundle(json, req) {
|
|
695
|
+
|
|
696
|
+
// Check if there are any actual search parameters (not just pagination/control params)
|
|
697
|
+
const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
|
|
698
|
+
const hasSearchParams = this.checkForSearchParams(selfLink);
|
|
699
|
+
|
|
700
|
+
// If no search params provided, show the search form
|
|
701
|
+
if (!hasSearchParams) {
|
|
702
|
+
return this.renderSearchForm(json, req);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Check if _elements was specified (look in self link)
|
|
706
|
+
const elementsMatch = selfLink.match(/[?&]_elements=([^&]*)/);
|
|
707
|
+
const elements = elementsMatch ? decodeURIComponent(elementsMatch[1]).split(',').map(e => e.trim()) : null;
|
|
708
|
+
|
|
709
|
+
if (elements && elements.length > 0) {
|
|
710
|
+
return this.renderSearchTable(json, elements, req);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Default: render as summary with individual resources
|
|
714
|
+
return await this.renderSearchSummary(json, req);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Check if URL has any actual search parameters (not just _offset, _count, _elements, _sort)
|
|
719
|
+
*/
|
|
720
|
+
checkForSearchParams(url) {
|
|
721
|
+
try {
|
|
722
|
+
const urlObj = new URL(url);
|
|
723
|
+
const controlParams = ['_offset', '_count', '_sort'];
|
|
724
|
+
|
|
725
|
+
for (const [key, value] of urlObj.searchParams) {
|
|
726
|
+
if (!controlParams.includes(key) && value) {
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
} catch {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Render search form (when no search params provided)
|
|
738
|
+
*/
|
|
739
|
+
async renderSearchForm(json, req) {
|
|
740
|
+
const resourceType = this.getSearchResourceType(json);
|
|
741
|
+
const params = resourceType === 'CodeSystem' ? CODESYSTEM_PARAMS : SEARCH_PARAMS;
|
|
742
|
+
|
|
743
|
+
let html = '<div class="alert alert-info">Enter search criteria:</div>';
|
|
744
|
+
html += `<form method="get" action="${this.escapeHtml(req.baseUrl)}/${this.escapeHtml(resourceType)}">`;
|
|
745
|
+
html += '<div class="row">';
|
|
746
|
+
|
|
747
|
+
// Build form fields
|
|
748
|
+
for (const param of params) {
|
|
749
|
+
html += '<div class="col-md-4 mb-3">';
|
|
750
|
+
html += `<label for="${param.name}" class="form-label">${this.escapeHtml(param.label)}</label>`;
|
|
751
|
+
|
|
752
|
+
if (param.type === 'select') {
|
|
753
|
+
html += `<select name="${param.name}" id="${param.name}" class="form-select">`;
|
|
754
|
+
for (const opt of param.options) {
|
|
755
|
+
html += `<option value="${this.escapeHtml(opt)}">${this.escapeHtml(opt || '(any)')}</option>`;
|
|
756
|
+
}
|
|
757
|
+
html += '</select>';
|
|
758
|
+
} else {
|
|
759
|
+
html += `<input type="text" name="${param.name}" id="${param.name}"/>`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
html += '</div>';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
html += '</div>';
|
|
766
|
+
|
|
767
|
+
// Sort dropdown
|
|
768
|
+
html += '<div class="row">';
|
|
769
|
+
html += '<div class="col-md-4 mb-3">';
|
|
770
|
+
html += '<label for="_sort" class="form-label">Sort By</label>';
|
|
771
|
+
html += '<select name="_sort" id="_sort" class="form-select">';
|
|
772
|
+
for (const opt of SORT_OPTIONS) {
|
|
773
|
+
html += `<option value="${this.escapeHtml(opt)}">${this.escapeHtml(opt || '(default)')}</option>`;
|
|
774
|
+
}
|
|
775
|
+
html += '</select>';
|
|
776
|
+
html += '</div>';
|
|
777
|
+
html += '</div>';
|
|
778
|
+
|
|
779
|
+
// Elements checkboxes
|
|
780
|
+
html += '<div class="mb-3">';
|
|
781
|
+
html += '<label class="form-label">Elements to include:</label><br/>';
|
|
782
|
+
for (const elem of ELEMENT_OPTIONS) {
|
|
783
|
+
html += `<div class="form-check form-check-inline">`;
|
|
784
|
+
html += `<input type="checkbox" name="_elements" value="${this.escapeHtml(elem)}" id="elem_${elem}" class="form-check-input"/>`;
|
|
785
|
+
html += `<label for="elem_${elem}" class="form-check-label">${this.escapeHtml(elem)}</label>`;
|
|
786
|
+
html += '</div>';
|
|
787
|
+
}
|
|
788
|
+
html += '</div>';
|
|
789
|
+
|
|
790
|
+
html += '<button type="submit" class="btn btn-primary">Search</button>';
|
|
791
|
+
html += '</form>';
|
|
792
|
+
|
|
793
|
+
return html;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Get resource type from search bundle (from self link or first entry)
|
|
798
|
+
*/
|
|
799
|
+
getSearchResourceType(json) {
|
|
800
|
+
// Try to get from self link first
|
|
801
|
+
const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
|
|
802
|
+
const typeMatch = selfLink.match(/\/(CodeSystem|ValueSet|ConceptMap)\?/);
|
|
803
|
+
if (typeMatch) {
|
|
804
|
+
return typeMatch[1];
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Fall back to first entry
|
|
808
|
+
const firstEntry = json.entry?.[0]?.resource;
|
|
809
|
+
return firstEntry?.resourceType || 'Resource';
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Render search results as a table (when _elements is specified)
|
|
814
|
+
*/
|
|
815
|
+
async renderSearchTable(json, elements, req) {
|
|
816
|
+
const entries = json.entry || [];
|
|
817
|
+
const total = json.total || 0;
|
|
818
|
+
|
|
819
|
+
let html = `<p>Found ${total} result(s)</p>`;
|
|
820
|
+
|
|
821
|
+
// Pagination links
|
|
822
|
+
html += this.renderPaginationLinks(json);
|
|
823
|
+
|
|
824
|
+
// Build table
|
|
825
|
+
html += '<table class="table table-striped grid">';
|
|
826
|
+
html += '<thead><tr>';
|
|
827
|
+
html += '<th>ID</th>';
|
|
828
|
+
for (const elem of elements) {
|
|
829
|
+
if (elem !== 'id') {
|
|
830
|
+
html += `<th>${this.escapeHtml(elem)}</th>`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
html += '</tr></thead>';
|
|
834
|
+
html += '<tbody>';
|
|
835
|
+
|
|
836
|
+
for (const entry of entries) {
|
|
837
|
+
const resource = entry.resource;
|
|
838
|
+
if (!resource) continue;
|
|
839
|
+
|
|
840
|
+
html += '<tr>';
|
|
841
|
+
|
|
842
|
+
// ID column with link
|
|
843
|
+
const id = resource.id || '';
|
|
844
|
+
const resourceType = resource.resourceType || '';
|
|
845
|
+
html += `<td><a href="${this.escapeHtml(req.baseUrl)}/${this.escapeHtml(resourceType)}/${this.escapeHtml(id)}">${this.escapeHtml(id)}</a></td>`;
|
|
846
|
+
|
|
847
|
+
// Other element columns
|
|
848
|
+
for (const elem of elements) {
|
|
849
|
+
if (elem !== 'id') {
|
|
850
|
+
const value = resource[elem];
|
|
851
|
+
html += `<td>${this.escapeHtml(this.formatValue(value))}</td>`;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
html += '</tr>';
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
html += '</tbody></table>';
|
|
859
|
+
|
|
860
|
+
// Pagination links again at bottom
|
|
861
|
+
html += this.renderPaginationLinks(json);
|
|
862
|
+
|
|
863
|
+
return html;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Render search results as summary with individual resources
|
|
868
|
+
*/
|
|
869
|
+
async renderSearchSummary(json, req) {
|
|
870
|
+
const entries = json.entry || [];
|
|
871
|
+
const total = json.total || 0;
|
|
872
|
+
|
|
873
|
+
let html = `<p>Found ${total} result(s)</p>`;
|
|
874
|
+
|
|
875
|
+
// Pagination links
|
|
876
|
+
html += this.renderPaginationLinks(json);
|
|
877
|
+
|
|
878
|
+
// Bundle summary
|
|
879
|
+
html += '<div class="card mb-3">';
|
|
880
|
+
html += '<div class="card-header">Bundle Summary</div>';
|
|
881
|
+
html += '<div class="card-body">';
|
|
882
|
+
html += `<p><strong>Type:</strong> ${this.escapeHtml(json.type)}</p>`;
|
|
883
|
+
html += `<p><strong>Total:</strong> ${total}</p>`;
|
|
884
|
+
html += '</div>';
|
|
885
|
+
html += '</div>';
|
|
886
|
+
|
|
887
|
+
// Each entry
|
|
888
|
+
for (const entry of entries) {
|
|
889
|
+
html += '<hr/>';
|
|
890
|
+
|
|
891
|
+
if (entry.resource) {
|
|
892
|
+
const resource = entry.resource;
|
|
893
|
+
html += `<h4>${this.escapeHtml(resource.resourceType)}/${this.escapeHtml(resource.id || 'unknown')}</h4>`;
|
|
894
|
+
|
|
895
|
+
if (entry.fullUrl) {
|
|
896
|
+
html += `<p><small><a href="${this.escapeHtml(entry.fullUrl)}">${this.escapeHtml(entry.fullUrl)}</a></small></p>`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Render the resource
|
|
900
|
+
html += await this.render(resource, req, true);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Pagination links again at bottom
|
|
905
|
+
html += this.renderPaginationLinks(json);
|
|
906
|
+
|
|
907
|
+
return html;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Render pagination links
|
|
912
|
+
*/
|
|
913
|
+
renderPaginationLinks(json) {
|
|
914
|
+
const links = json.link || [];
|
|
915
|
+
if (links.length === 0) return '';
|
|
916
|
+
|
|
917
|
+
let html = '<nav><ul class="pagination">';
|
|
918
|
+
|
|
919
|
+
const linkOrder = ['first', 'previous', 'self', 'next', 'last'];
|
|
920
|
+
|
|
921
|
+
for (const rel of linkOrder) {
|
|
922
|
+
const link = links.find(l => l.relation === rel);
|
|
923
|
+
if (link) {
|
|
924
|
+
const isDisabled = rel === 'self';
|
|
925
|
+
const label = rel.charAt(0).toUpperCase() + rel.slice(1);
|
|
926
|
+
|
|
927
|
+
if (isDisabled) {
|
|
928
|
+
html += `<li class="page-item active"><span class="page-link">${this.escapeHtml(label)}</span></li>`;
|
|
929
|
+
} else {
|
|
930
|
+
html += `<li class="page-item"><a class="page-link" href="${this.escapeHtml(link.url)}">${this.escapeHtml(label)}</a></li>`;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
html += '</ul></nav>';
|
|
936
|
+
return html;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Render a generic bundle (non-search)
|
|
941
|
+
*/
|
|
942
|
+
async renderGenericBundle(json, req) {
|
|
943
|
+
let html = '<div class="card mb-3">';
|
|
944
|
+
html += '<div class="card-header">Bundle</div>';
|
|
945
|
+
html += '<div class="card-body">';
|
|
946
|
+
html += `<p><strong>Type:</strong> ${this.escapeHtml(json.type)}</p>`;
|
|
947
|
+
html += `<p><strong>Total:</strong> ${json.total || 'N/A'}</p>`;
|
|
948
|
+
html += '</div>';
|
|
949
|
+
html += '</div>';
|
|
950
|
+
|
|
951
|
+
// Links
|
|
952
|
+
if (json.link && json.link.length > 0) {
|
|
953
|
+
html += '<h4>Links</h4>';
|
|
954
|
+
html += '<ul>';
|
|
955
|
+
for (const link of json.link) {
|
|
956
|
+
html += `<li><strong>${this.escapeHtml(link.relation)}:</strong> <a href="${this.escapeHtml(link.url)}">${this.escapeHtml(link.url)}</a></li>`;
|
|
957
|
+
}
|
|
958
|
+
html += '</ul>';
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Entries
|
|
962
|
+
if (json.entry && json.entry.length > 0) {
|
|
963
|
+
for (const entry of json.entry) {
|
|
964
|
+
html += '<hr/>';
|
|
965
|
+
if (entry.resource) {
|
|
966
|
+
html += await this.render(entry.resource, req, true);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return html;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Render generic resource (fallback)
|
|
976
|
+
*/
|
|
977
|
+
async renderGeneric(json, inBundle) {
|
|
978
|
+
return this.renderResourceWithNarrative(json, inBundle);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Format a value for display
|
|
983
|
+
*/
|
|
984
|
+
formatValue(value) {
|
|
985
|
+
if (value === null || value === undefined) {
|
|
986
|
+
return '';
|
|
987
|
+
}
|
|
988
|
+
if (typeof value === 'object') {
|
|
989
|
+
return JSON.stringify(value);
|
|
990
|
+
}
|
|
991
|
+
return String(value);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Generate a unique ID for collapsible sections
|
|
996
|
+
*/
|
|
997
|
+
let
|
|
998
|
+
resourceIdCounter = 0;
|
|
999
|
+
|
|
1000
|
+
generateResourceId() {
|
|
1001
|
+
return 'resource_' + (++this.resourceIdCounter);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Render resource with text/div narrative and collapsible JSON source
|
|
1007
|
+
*/
|
|
1008
|
+
async renderResourceWithNarrative(json, rendered) {
|
|
1009
|
+
const resourceId = this.generateResourceId();
|
|
1010
|
+
|
|
1011
|
+
let html = "";
|
|
1012
|
+
|
|
1013
|
+
// Show text/div narrative if present
|
|
1014
|
+
if (rendered) {
|
|
1015
|
+
html += '<div class="narrative">';
|
|
1016
|
+
html += rendered; // Already HTML, render as-is
|
|
1017
|
+
html += '</div>';
|
|
1018
|
+
} else {
|
|
1019
|
+
html += '<div class="narrative">(No Narrative)</div>';
|
|
1020
|
+
}
|
|
1021
|
+
if (json.text && json.text.div) {
|
|
1022
|
+
// Collapsible JSON source
|
|
1023
|
+
html += '<div class="xhtml">';
|
|
1024
|
+
html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
|
|
1025
|
+
html += 'Show Original Narrative</button>';
|
|
1026
|
+
html += `<div id="${resourceId}x" class="original-narrative" style="display: none; margin-top: 10px;">`;
|
|
1027
|
+
|
|
1028
|
+
html += '<div class="narrative">';
|
|
1029
|
+
html += json.text.div; // Already HTML, render as-is
|
|
1030
|
+
html += '</div>';
|
|
1031
|
+
}
|
|
1032
|
+
html += '</div>';
|
|
1033
|
+
html += '</div>';
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
// Collapsible JSON source
|
|
1037
|
+
html += '<div class="json-source">';
|
|
1038
|
+
html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleJsonSource('${resourceId}')">`;
|
|
1039
|
+
html += 'Show JSON Source</button>';
|
|
1040
|
+
html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
|
|
1041
|
+
html += `<pre>${this.escapeHtml(JSON.stringify(json, null, 2))}</pre>`;
|
|
1042
|
+
html += '</div>';
|
|
1043
|
+
html += '</div>';
|
|
1044
|
+
|
|
1045
|
+
return html;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// eslint-disable-next-line no-unused-vars
|
|
1049
|
+
async renderOperationsForm(json, req) {
|
|
1050
|
+
const vcSystemId = this.generateResourceId();
|
|
1051
|
+
const inferSystemId = this.generateResourceId();
|
|
1052
|
+
|
|
1053
|
+
return await this.liquid.renderFile('operations-form', {
|
|
1054
|
+
vcSystemId,
|
|
1055
|
+
inferSystemId,
|
|
1056
|
+
valueSetsJson: JSON.stringify(json.valueSets || [])
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
module.exports = {
|
|
1062
|
+
TxHtmlRenderer, loadTemplate
|
|
1063
|
+
};
|