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/registry/api.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
// Enhanced registry-api.js with resolver and HTML rendering functions
|
|
2
|
+
|
|
3
|
+
const { ServerRegistryUtilities } = require('./model');
|
|
4
|
+
|
|
5
|
+
class RegistryAPI {
|
|
6
|
+
constructor(crawler) {
|
|
7
|
+
this.crawler = crawler;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build rows for code system queries
|
|
12
|
+
* Matches the Pascal buildRowsCS functionality
|
|
13
|
+
*/
|
|
14
|
+
buildRowsForCodeSystem(params = {}) {
|
|
15
|
+
const {
|
|
16
|
+
registryCode = '',
|
|
17
|
+
serverCode = '',
|
|
18
|
+
version = '',
|
|
19
|
+
codeSystem = ''
|
|
20
|
+
} = params;
|
|
21
|
+
|
|
22
|
+
const rows = [];
|
|
23
|
+
const data = this.crawler.getData();
|
|
24
|
+
|
|
25
|
+
// Lock for thread safety during read
|
|
26
|
+
data.lock('buildRowsCS');
|
|
27
|
+
try {
|
|
28
|
+
data.registries.forEach(registry => {
|
|
29
|
+
if (registryCode && registry.code !== registryCode) return;
|
|
30
|
+
|
|
31
|
+
registry.servers.forEach(server => {
|
|
32
|
+
if (serverCode && server.code !== serverCode) return;
|
|
33
|
+
|
|
34
|
+
// Check if server is authoritative for this code system
|
|
35
|
+
const isAuth = codeSystem ? ServerRegistryUtilities.hasMatchingCodeSystem(
|
|
36
|
+
codeSystem,
|
|
37
|
+
server.authCSList,
|
|
38
|
+
true // support wildcards
|
|
39
|
+
) : false;
|
|
40
|
+
|
|
41
|
+
server.versions.forEach(versionInfo => {
|
|
42
|
+
if (version && !ServerRegistryUtilities.versionMatches(version, versionInfo.version)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Always skip servers with errors - they can't serve requests
|
|
47
|
+
if (versionInfo.error) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Include if:
|
|
52
|
+
// 1. Authoritative for the requested code system
|
|
53
|
+
// 2. No filter specified
|
|
54
|
+
// 3. Has the code system in its list
|
|
55
|
+
if (isAuth ||
|
|
56
|
+
!codeSystem ||
|
|
57
|
+
(codeSystem && ServerRegistryUtilities.hasMatchingCodeSystem(
|
|
58
|
+
codeSystem,
|
|
59
|
+
versionInfo.codeSystems,
|
|
60
|
+
false // no wildcards for actual content
|
|
61
|
+
))) {
|
|
62
|
+
const row = ServerRegistryUtilities.createRow(
|
|
63
|
+
registry,
|
|
64
|
+
server,
|
|
65
|
+
versionInfo,
|
|
66
|
+
isAuth
|
|
67
|
+
);
|
|
68
|
+
rows.push(row);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
} finally {
|
|
74
|
+
data.unlock();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this._sortAndRankRows(rows);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build rows for value set queries
|
|
82
|
+
* Matches the Pascal buildRowsVS functionality
|
|
83
|
+
*/
|
|
84
|
+
buildRowsForValueSet(params = {}) {
|
|
85
|
+
const {
|
|
86
|
+
registryCode = '',
|
|
87
|
+
serverCode = '',
|
|
88
|
+
version = '',
|
|
89
|
+
valueSet = ''
|
|
90
|
+
} = params;
|
|
91
|
+
|
|
92
|
+
const rows = [];
|
|
93
|
+
const data = this.crawler.getData();
|
|
94
|
+
|
|
95
|
+
data.lock('buildRowsVS');
|
|
96
|
+
try {
|
|
97
|
+
data.registries.forEach(registry => {
|
|
98
|
+
if (registryCode && registry.code !== registryCode) return;
|
|
99
|
+
|
|
100
|
+
registry.servers.forEach(server => {
|
|
101
|
+
if (serverCode && server.code !== serverCode) return;
|
|
102
|
+
|
|
103
|
+
// Check if server is authoritative for this value set
|
|
104
|
+
const isAuth = valueSet ? ServerRegistryUtilities.hasMatchingValueSet(
|
|
105
|
+
valueSet,
|
|
106
|
+
server.authVSList,
|
|
107
|
+
true // support wildcards
|
|
108
|
+
) : false;
|
|
109
|
+
|
|
110
|
+
server.versions.forEach(versionInfo => {
|
|
111
|
+
if (version && !ServerRegistryUtilities.versionMatches(version, versionInfo.version)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Always skip servers with errors - they can't serve requests
|
|
116
|
+
if (versionInfo.error) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Include if:
|
|
121
|
+
// 1. No filter specified
|
|
122
|
+
// 2. Authoritative for the value set (even via wildcard)
|
|
123
|
+
// 3. Has the value set in its list
|
|
124
|
+
let includeRow = false;
|
|
125
|
+
|
|
126
|
+
if (!valueSet) {
|
|
127
|
+
// No filter, include all working servers
|
|
128
|
+
includeRow = true;
|
|
129
|
+
} else {
|
|
130
|
+
// Check if actually has the value set
|
|
131
|
+
const hasValueSet = ServerRegistryUtilities.hasMatchingValueSet(
|
|
132
|
+
valueSet,
|
|
133
|
+
versionInfo.valueSets,
|
|
134
|
+
false // no wildcards for actual content
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Include if authoritative OR has the value set
|
|
138
|
+
// This matches the Pascal logic: if auth or hasMatchingValueSet
|
|
139
|
+
if (isAuth || hasValueSet) {
|
|
140
|
+
includeRow = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (includeRow) {
|
|
145
|
+
const row = ServerRegistryUtilities.createRow(
|
|
146
|
+
registry,
|
|
147
|
+
server,
|
|
148
|
+
versionInfo,
|
|
149
|
+
isAuth
|
|
150
|
+
);
|
|
151
|
+
rows.push(row);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
data.unlock();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this._sortAndRankRows(rows);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all available registries
|
|
165
|
+
*/
|
|
166
|
+
getRegistries() {
|
|
167
|
+
const data = this.crawler.getData();
|
|
168
|
+
return data.registries.map(r => ({
|
|
169
|
+
code: r.code,
|
|
170
|
+
name: r.name,
|
|
171
|
+
address: r.address,
|
|
172
|
+
authority: r.authority,
|
|
173
|
+
error: r.error,
|
|
174
|
+
serverCount: r.servers.length
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all servers for a registry
|
|
180
|
+
*/
|
|
181
|
+
getServers(registryCode) {
|
|
182
|
+
const data = this.crawler.getData();
|
|
183
|
+
const registry = data.getRegistry(registryCode);
|
|
184
|
+
|
|
185
|
+
if (!registry) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return registry.servers.map(s => ({
|
|
190
|
+
code: s.code,
|
|
191
|
+
name: s.name,
|
|
192
|
+
address: s.address,
|
|
193
|
+
description: s.getDescription(),
|
|
194
|
+
details: s.getDetails(),
|
|
195
|
+
versionCount: s.versions.length,
|
|
196
|
+
authCSCount: s.authCSList.length,
|
|
197
|
+
authVSCount: s.authVSList.length,
|
|
198
|
+
usageTags: s.usageList
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get server details
|
|
204
|
+
*/
|
|
205
|
+
getServerDetails(registryCode, serverCode) {
|
|
206
|
+
const data = this.crawler.getData();
|
|
207
|
+
const registry = data.getRegistry(registryCode);
|
|
208
|
+
|
|
209
|
+
if (!registry) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const server = registry.getServer(serverCode);
|
|
214
|
+
if (!server) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
...server.toJSON(),
|
|
220
|
+
versions: server.versions.map(v => ({
|
|
221
|
+
...v.toJSON(),
|
|
222
|
+
details: v.getDetails(),
|
|
223
|
+
csList: v.getCsListHtml(),
|
|
224
|
+
vsList: v.getVsListHtml()
|
|
225
|
+
}))
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get statistics about the registry
|
|
231
|
+
*/
|
|
232
|
+
getStatistics() {
|
|
233
|
+
const data = this.crawler.getData();
|
|
234
|
+
|
|
235
|
+
let totalServers = 0;
|
|
236
|
+
let totalVersions = 0;
|
|
237
|
+
let totalCodeSystems = new Set();
|
|
238
|
+
let totalValueSets = new Set();
|
|
239
|
+
let errorCount = 0;
|
|
240
|
+
let workingVersions = 0;
|
|
241
|
+
|
|
242
|
+
data.registries.forEach(registry => {
|
|
243
|
+
if (registry.error) errorCount++;
|
|
244
|
+
|
|
245
|
+
registry.servers.forEach(server => {
|
|
246
|
+
totalServers++;
|
|
247
|
+
|
|
248
|
+
server.versions.forEach(version => {
|
|
249
|
+
totalVersions++;
|
|
250
|
+
if (version.error) {
|
|
251
|
+
errorCount++;
|
|
252
|
+
} else {
|
|
253
|
+
workingVersions++;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
version.codeSystems.forEach(cs => totalCodeSystems.add(cs));
|
|
257
|
+
version.valueSets.forEach(vs => totalValueSets.add(vs));
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
lastRun: data.lastRun,
|
|
264
|
+
outcome: data.outcome,
|
|
265
|
+
registryCount: data.registries.length,
|
|
266
|
+
serverCount: totalServers,
|
|
267
|
+
versionCount: totalVersions,
|
|
268
|
+
workingVersions: workingVersions,
|
|
269
|
+
uniqueCodeSystems: totalCodeSystems.size,
|
|
270
|
+
uniqueValueSets: totalValueSets.size,
|
|
271
|
+
errorCount: errorCount
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Sort and rank rows based on various criteria
|
|
277
|
+
*/
|
|
278
|
+
_sortAndRankRows(rows) {
|
|
279
|
+
return rows.sort((a, b) => {
|
|
280
|
+
// 1. Authoritative servers first
|
|
281
|
+
if (a.authoritative !== b.authoritative) {
|
|
282
|
+
return a.authoritative ? -1 : 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. No errors before errors
|
|
286
|
+
const aHasError = a.error !== '';
|
|
287
|
+
const bHasError = b.error !== '';
|
|
288
|
+
if (aHasError !== bHasError) {
|
|
289
|
+
return aHasError ? 1 : -1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 3. More recent success first (smaller lastSuccess value)
|
|
293
|
+
if (a.lastSuccess !== b.lastSuccess) {
|
|
294
|
+
// If one has never succeeded, put it last
|
|
295
|
+
if (a.lastSuccess === 0) return 1;
|
|
296
|
+
if (b.lastSuccess === 0) return -1;
|
|
297
|
+
return a.lastSuccess - b.lastSuccess;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 4. More resources is better
|
|
301
|
+
const aResources = a.systems + a.sets;
|
|
302
|
+
const bResources = b.systems + b.sets;
|
|
303
|
+
if (aResources !== bResources) {
|
|
304
|
+
return bResources - aResources;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 5. Prefer newer versions
|
|
308
|
+
const versionCompare = this._compareVersions(b.version, a.version);
|
|
309
|
+
if (versionCompare !== 0) {
|
|
310
|
+
return versionCompare;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 6. Alphabetical by server name as tie-breaker
|
|
314
|
+
return a.serverName.localeCompare(b.serverName);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_normalizeFhirVersion(version) {
|
|
319
|
+
if (!version) return version;
|
|
320
|
+
|
|
321
|
+
// Convert R4 or r4 to 4.0, R5 or r5 to 5.0, etc.
|
|
322
|
+
const rMatch = /^[rR](\d+)$/.exec(version);
|
|
323
|
+
if (rMatch) {
|
|
324
|
+
return `${rMatch[1]}.0`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return version;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Compare semantic versions
|
|
332
|
+
*/
|
|
333
|
+
_compareVersions(v1, v2) {
|
|
334
|
+
const parts1 = v1.split('.').map(p => parseInt(p) || 0);
|
|
335
|
+
const parts2 = v2.split('.').map(p => parseInt(p) || 0);
|
|
336
|
+
|
|
337
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
338
|
+
const p1 = parts1[i] || 0;
|
|
339
|
+
const p2 = parts2[i] || 0;
|
|
340
|
+
if (p1 !== p2) {
|
|
341
|
+
return p1 - p2;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Find best server for a given code system/value set
|
|
350
|
+
*/
|
|
351
|
+
findBestServer(type, url, version) {
|
|
352
|
+
let rows;
|
|
353
|
+
|
|
354
|
+
if (type === 'codesystem') {
|
|
355
|
+
rows = this.buildRowsForCodeSystem({ codeSystem: url, version });
|
|
356
|
+
} else if (type === 'valueset') {
|
|
357
|
+
rows = this.buildRowsForValueSet({ valueSet: url, version });
|
|
358
|
+
} else {
|
|
359
|
+
throw new Error(`Unknown type: ${type}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (rows.length === 0) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Return the top-ranked server
|
|
367
|
+
return rows[0];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get the current data (for direct access)
|
|
372
|
+
*/
|
|
373
|
+
getData() {
|
|
374
|
+
return this.crawler.getData();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Express middleware for handling API requests
|
|
379
|
+
*/
|
|
380
|
+
expressMiddleware() {
|
|
381
|
+
return (req, res, next) => {
|
|
382
|
+
// Attach API instance to request
|
|
383
|
+
req.registryAPI = this;
|
|
384
|
+
next();
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* NEW FUNCTION: Resolve the best server for a code system
|
|
390
|
+
* Based on Pascal resolveCS function
|
|
391
|
+
*/
|
|
392
|
+
resolveCodeSystem(fhirVersion, codeSystem, authoritativeOnly, usage = '') {
|
|
393
|
+
if (!fhirVersion) {
|
|
394
|
+
throw new Error('A FHIR version is required');
|
|
395
|
+
}
|
|
396
|
+
if (!codeSystem) {
|
|
397
|
+
throw new Error('A code system URL is required');
|
|
398
|
+
}
|
|
399
|
+
const normalizedVersion = this._normalizeFhirVersion(fhirVersion);
|
|
400
|
+
|
|
401
|
+
const result = {
|
|
402
|
+
formatVersion: '1',
|
|
403
|
+
'registry-url': this.getData().address,
|
|
404
|
+
authoritative: [],
|
|
405
|
+
candidates: []
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const matchedServers = [];
|
|
409
|
+
const data = this.crawler.getData();
|
|
410
|
+
|
|
411
|
+
// Extract base code system URL (before any pipe)
|
|
412
|
+
let baseCodeSystem = codeSystem;
|
|
413
|
+
if (codeSystem.includes('|')) {
|
|
414
|
+
baseCodeSystem = codeSystem.substring(0, codeSystem.indexOf('|'));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Lock for thread safety during read
|
|
418
|
+
data.lock('resolveCS');
|
|
419
|
+
try {
|
|
420
|
+
data.registries.forEach(registry => {
|
|
421
|
+
registry.servers.forEach(server => {
|
|
422
|
+
let added = false;
|
|
423
|
+
|
|
424
|
+
// Check if server supports the requested usage tag
|
|
425
|
+
if (server.usageList.length === 0 ||
|
|
426
|
+
(usage && server.usageList.includes(usage))) {
|
|
427
|
+
|
|
428
|
+
// Check if server is authoritative for this code system
|
|
429
|
+
const isAuth = server.isAuthCS(codeSystem);
|
|
430
|
+
|
|
431
|
+
server.versions.forEach(version => {
|
|
432
|
+
if (ServerRegistryUtilities.versionMatches(normalizedVersion, version.version)) {
|
|
433
|
+
// Check if the server has the code system
|
|
434
|
+
// Test against both the full URL and the base URL
|
|
435
|
+
const hasMatchingCS =
|
|
436
|
+
ServerRegistryUtilities.hasMatchingCodeSystem(baseCodeSystem, version.codeSystems, false) ||
|
|
437
|
+
(baseCodeSystem !== codeSystem &&
|
|
438
|
+
ServerRegistryUtilities.hasMatchingCodeSystem(codeSystem, version.codeSystems, false));
|
|
439
|
+
|
|
440
|
+
if (hasMatchingCS) {
|
|
441
|
+
if (isAuth) {
|
|
442
|
+
result.authoritative.push(this.createServerEntry(server, version));
|
|
443
|
+
} else if (!authoritativeOnly) {
|
|
444
|
+
result.candidates.push(this.createServerEntry(server, version));
|
|
445
|
+
}
|
|
446
|
+
added = true;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (added) {
|
|
452
|
+
matchedServers.push(server.code);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// NEW: Fallback - if no matches found, check for authoritative pattern matches
|
|
459
|
+
if (result.authoritative.length === 0 && result.candidates.length === 0) {
|
|
460
|
+
data.registries.forEach(registry => {
|
|
461
|
+
registry.servers.forEach(server => {
|
|
462
|
+
// Check if server supports the requested usage tag
|
|
463
|
+
if (server.usageList.length === 0 ||
|
|
464
|
+
(usage && server.usageList.includes(usage))) {
|
|
465
|
+
|
|
466
|
+
// Check if server is authoritative for this code system
|
|
467
|
+
const isAuth = server.isAuthCS(codeSystem);
|
|
468
|
+
|
|
469
|
+
if (isAuth) {
|
|
470
|
+
server.versions.forEach(version => {
|
|
471
|
+
if (ServerRegistryUtilities.versionMatches(normalizedVersion, version.version)) {
|
|
472
|
+
result.authoritative.push(this.createServerEntry(server, version));
|
|
473
|
+
if (!matchedServers.includes(server.code)) {
|
|
474
|
+
matchedServers.push(server.code);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
data.unlock();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
result : this._cleanEmptyArrays(result),
|
|
489
|
+
matches: matchedServers.length > 0 ? matchedServers.join(',') : '--'
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* NEW FUNCTION: Resolve the best server for a value set
|
|
495
|
+
* Based on Pascal resolveVS function
|
|
496
|
+
*/
|
|
497
|
+
resolveValueSet(fhirVersion, valueSet, authoritativeOnly, usage = '') {
|
|
498
|
+
if (!fhirVersion) {
|
|
499
|
+
throw new Error('A FHIR version is required');
|
|
500
|
+
}
|
|
501
|
+
if (!valueSet) {
|
|
502
|
+
throw new Error('A value set URL is required');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const normalizedVersion = this._normalizeFhirVersion(fhirVersion);
|
|
506
|
+
|
|
507
|
+
const result = {
|
|
508
|
+
formatVersion: '1',
|
|
509
|
+
'registry-url': this.getData().address,
|
|
510
|
+
authoritative: [],
|
|
511
|
+
candidates: []
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const matchedServers = [];
|
|
515
|
+
const data = this.crawler.getData();
|
|
516
|
+
|
|
517
|
+
// Extract base value set URL (before any pipe)
|
|
518
|
+
let baseValueSet = valueSet;
|
|
519
|
+
if (valueSet.includes('|')) {
|
|
520
|
+
baseValueSet = valueSet.substring(0, valueSet.indexOf('|'));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Lock for thread safety during read
|
|
524
|
+
data.lock('resolveVS');
|
|
525
|
+
try {
|
|
526
|
+
data.registries.forEach(registry => {
|
|
527
|
+
registry.servers.forEach(server => {
|
|
528
|
+
let added = false;
|
|
529
|
+
|
|
530
|
+
// Check if server supports the requested usage tag
|
|
531
|
+
if (server.usageList.length === 0 ||
|
|
532
|
+
(usage && server.usageList.includes(usage))) {
|
|
533
|
+
|
|
534
|
+
// Check if server is authoritative for this value set
|
|
535
|
+
const isAuth = server.isAuthVS(baseValueSet);
|
|
536
|
+
|
|
537
|
+
server.versions.forEach(version => {
|
|
538
|
+
if (ServerRegistryUtilities.versionMatches(normalizedVersion, version.version)) {
|
|
539
|
+
// For authoritative servers, we don't need to check if they have the value set
|
|
540
|
+
if (isAuth) {
|
|
541
|
+
result.authoritative.push(this.createServerEntry(server, version));
|
|
542
|
+
added = true;
|
|
543
|
+
}
|
|
544
|
+
// For non-authoritative servers, check if they have the value set
|
|
545
|
+
else if (ServerRegistryUtilities.hasMatchingValueSet(baseValueSet, version.valueSets, false) ||
|
|
546
|
+
(baseValueSet !== valueSet &&
|
|
547
|
+
ServerRegistryUtilities.hasMatchingValueSet(valueSet, version.valueSets, false))) {
|
|
548
|
+
if (!authoritativeOnly) {
|
|
549
|
+
result.candidates.push(this.createServerEntry(server, version));
|
|
550
|
+
}
|
|
551
|
+
added = true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
if (added) {
|
|
557
|
+
matchedServers.push(server.code);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
} finally {
|
|
563
|
+
data.unlock();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
result : this._cleanEmptyArrays(result),
|
|
568
|
+
matches: matchedServers.length > 0 ? matchedServers.join(',') : '--'
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
_cleanEmptyArrays(result) {
|
|
573
|
+
const cleanedResult = { ...result };
|
|
574
|
+
|
|
575
|
+
// Remove empty arrays
|
|
576
|
+
Object.keys(cleanedResult).forEach(key => {
|
|
577
|
+
if (Array.isArray(cleanedResult[key]) && cleanedResult[key].length === 0) {
|
|
578
|
+
delete cleanedResult[key];
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
return cleanedResult;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Helper function to create a server entry for resolve results
|
|
587
|
+
*/
|
|
588
|
+
createServerEntry(server, version) {
|
|
589
|
+
const entry = {
|
|
590
|
+
'server-name': server.name,
|
|
591
|
+
url: version.address
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
if (version.security) {
|
|
595
|
+
entry.security = version.security;
|
|
596
|
+
}
|
|
597
|
+
if (server.accessInfo) {
|
|
598
|
+
entry.access_info = server.accessInfo;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return entry;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* NEW FUNCTION: Render a JSON result as an HTML table
|
|
606
|
+
* Based on Pascal renderJson function
|
|
607
|
+
*/
|
|
608
|
+
renderJsonToHtml(json, path, regCode = '', serverCode = '', versionCode = '') {
|
|
609
|
+
let html = '<table class="grid">\n';
|
|
610
|
+
html += '<tr>\n';
|
|
611
|
+
|
|
612
|
+
if (!regCode) {
|
|
613
|
+
html += '<td><b>Registry</b></td>\n';
|
|
614
|
+
}
|
|
615
|
+
if (!serverCode) {
|
|
616
|
+
html += '<td><b>Server</b></td>\n';
|
|
617
|
+
}
|
|
618
|
+
if (!versionCode) {
|
|
619
|
+
html += '<td><b>FHIR Version</b></td>\n';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
html += '<td><b>Url</b></td>\n';
|
|
623
|
+
html += '<td><b>Status</b></td>\n';
|
|
624
|
+
html += '<td><b>Content</b></td>\n';
|
|
625
|
+
html += '<td><b>Authoritative</b></td>\n';
|
|
626
|
+
html += '<td><b>Security</b></td>\n';
|
|
627
|
+
html += '</tr>\n';
|
|
628
|
+
|
|
629
|
+
const results = json.results || [];
|
|
630
|
+
for (const row of results) {
|
|
631
|
+
html += '<tr>\n';
|
|
632
|
+
|
|
633
|
+
if (!regCode) {
|
|
634
|
+
html += `<td><a href="${path}®istry=${row['registry-code']}">${this._escapeHtml(row['registry-name'])}</a></td>\n`;
|
|
635
|
+
}
|
|
636
|
+
if (!serverCode) {
|
|
637
|
+
html += `<td><a href="${path}&server=${row['server-code']}">${this._escapeHtml(row['server-name'])}</a></td>\n`;
|
|
638
|
+
}
|
|
639
|
+
if (!versionCode) {
|
|
640
|
+
html += `<td><a href="${path}&fhirVersion=${row.fhirVersion}">${row.fhirVersion}</a></td>\n`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
html += `<td><a href="${this._escapeHtml(row.url)}">${this._escapeHtml(row.url)}</a></td>\n`;
|
|
644
|
+
|
|
645
|
+
if (row.error) {
|
|
646
|
+
html += `<td><span style="color: maroon">Error: ${this._escapeHtml(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
|
|
647
|
+
} else {
|
|
648
|
+
html += `<td>Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
html += `<td>${row.systems} systems</td>\n`;
|
|
652
|
+
|
|
653
|
+
html += '<td>';
|
|
654
|
+
if (row['is-authoritative']) {
|
|
655
|
+
html += 'true';
|
|
656
|
+
}
|
|
657
|
+
html += '</td>\n';
|
|
658
|
+
|
|
659
|
+
html += `<td>${row.security}/td>\n`;
|
|
660
|
+
|
|
661
|
+
html += '</tr>\n';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
html += '</table>\n';
|
|
665
|
+
return html;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* NEW FUNCTION: Render registry info as HTML
|
|
670
|
+
* Based on Pascal renderInfo function
|
|
671
|
+
*/
|
|
672
|
+
renderInfoToHtml() {
|
|
673
|
+
const data = this.crawler.getData();
|
|
674
|
+
let html = '<table class="grid">';
|
|
675
|
+
|
|
676
|
+
html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif"> Registries</td><td>${data.address} (${this._escapeHtml(data.outcome)})</td></tr>`;
|
|
677
|
+
|
|
678
|
+
data.registries.forEach(registry => {
|
|
679
|
+
if (registry.error) {
|
|
680
|
+
html += `<tr><td title="${this._escapeHtml(registry.name)}"> <img src="/assets/images/tx-registry.png"> ${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a>. Error: ${this._escapeHtml(registry.error)}</td></tr>`;
|
|
681
|
+
} else {
|
|
682
|
+
html += `<tr><td title="${this._escapeHtml(registry.name)}"> <img src="/assets/images/tx-registry.png"> ${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a></td></tr>`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
registry.servers.forEach(server => {
|
|
686
|
+
if (server.authCSList.length > 0 || server.authVSList.length > 0 || server.usageList.length > 0) {
|
|
687
|
+
html += `<tr><td title="${this._escapeHtml(server.name)}"> <img src="/assets/images/tx-server.png"> ${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a>. ${server.description}</td></tr>`;
|
|
688
|
+
} else {
|
|
689
|
+
html += `<tr><td title="${this._escapeHtml(server.name)}"> <img src="/assets/images/tx-server.png"> ${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a></td></tr>`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
server.versions.forEach(version => {
|
|
693
|
+
// Get major.minor version only
|
|
694
|
+
const versionParts = version.version.split('.');
|
|
695
|
+
const majorMinor = versionParts.slice(0, 2).join('.');
|
|
696
|
+
|
|
697
|
+
html += `<tr><td> <img src="/assets/images/tx-version.png"> v${majorMinor}</td><td><a href="${this._escapeHtml(version.address)}">${this._escapeHtml(version.address)}</a>. Status: ${this._escapeHtml(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
html += '</table>';
|
|
703
|
+
return html;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Helper function to format a duration in seconds to a human-readable string
|
|
708
|
+
*/
|
|
709
|
+
_formatDuration(seconds) {
|
|
710
|
+
if (seconds < 60) {
|
|
711
|
+
return `${seconds} seconds`;
|
|
712
|
+
} else if (seconds < 3600) {
|
|
713
|
+
return `${Math.floor(seconds / 60)} minutes`;
|
|
714
|
+
} else if (seconds < 86400) {
|
|
715
|
+
return `${Math.floor(seconds / 3600)} hours`;
|
|
716
|
+
} else {
|
|
717
|
+
return `${Math.floor(seconds / 86400)} days`;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Helper function to escape HTML special characters
|
|
723
|
+
*/
|
|
724
|
+
_escapeHtml(text) {
|
|
725
|
+
if (!text) return '';
|
|
726
|
+
return text
|
|
727
|
+
.replace(/&/g, '&')
|
|
728
|
+
.replace(/</g, '<')
|
|
729
|
+
.replace(/>/g, '>')
|
|
730
|
+
.replace(/"/g, '"')
|
|
731
|
+
.replace(/'/g, ''');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
module.exports = RegistryAPI;
|