fhirsmith 0.13.4 → 0.14.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 +32 -1
- package/README.md +16 -3
- package/config-template.json +70 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/nginx.md +391 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher.js +146 -14
- package/server.js +51 -14
- package/stats.js +19 -13
- package/testing/readme.md +112 -0
- package/testing/render.js +482 -0
- package/testing/search.js +378 -0
- package/testing/store.js +266 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +486 -0
- package/translations/Messages.properties +32 -0
- package/tx/closure/closure-store.js +396 -0
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-rxnorm.js +35 -8
- package/tx/cs/cs-snomed.js +22 -3
- package/tx/html/codesystem-operations.liquid +177 -0
- package/tx/html/ecl-panel.liquid +93 -0
- package/tx/html/home-metrics.liquid +39 -76
- package/tx/html/tx-template.html +1 -0
- package/tx/library/conceptmap.js +22 -6
- package/tx/library/designations.js +12 -3
- package/tx/library/operation-outcome.js +3 -1
- package/tx/library/renderer.js +42 -5
- package/tx/operation-context.js +80 -17
- package/tx/provider.js +85 -0
- package/tx/sct/expressions.js +128 -44
- package/tx/tx-html.js +426 -44
- package/tx/tx.js +117 -9
- package/tx/workers/cache-control.js +2 -2
- package/tx/workers/closure.js +483 -13
- package/tx/workers/metadata.js +8 -3
- package/tx/workers/read.js +18 -3
- package/tx/workers/subsumes.js +18 -2
- package/tx/workers/translate.js +167 -20
- package/tx/workers/validate.js +61 -182
- package/tx/workers/worker.js +9 -0
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
<!-- Text Search: the whole code system as an anonymous value set, expanded with the
|
|
2
|
+
text as the filter parameter. This is what a person opening a code system usually
|
|
3
|
+
wants - "is there a code for X" - so it comes first. -->
|
|
4
|
+
<div class="operation-form" style="margin-bottom: 15px;">
|
|
5
|
+
<strong>Text Search</strong>
|
|
6
|
+
<form method="post" action="{{ expandUrl }}" id="{{ searchId }}_form"
|
|
7
|
+
data-system="{{ url }}" data-version="{{ systemVersion }}"
|
|
8
|
+
style="display: inline-block; margin-left: 10px;" onsubmit="return txSearchSubmit('{{ searchId }}')">
|
|
9
|
+
<input type="text" name="filter" placeholder="Text" size="30"/>
|
|
10
|
+
<button type="submit" class="btn btn-sm btn-primary">Search</button>
|
|
11
|
+
</form>
|
|
12
|
+
<div id="{{ searchId }}_preview" style="display: none; margin-left: 10px; margin-top: 8px;"></div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
1
15
|
<div class="operation-form" style="margin-bottom: 15px;">
|
|
2
16
|
<strong>Lookup</strong>
|
|
3
17
|
<form method="get" action="$lookup" style="display: inline-block; margin-left: 10px;">
|
|
@@ -16,3 +30,166 @@
|
|
|
16
30
|
<button type="submit" class="btn btn-sm btn-primary">Subsumes</button>
|
|
17
31
|
</form>
|
|
18
32
|
</div>
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
<!-- Filter: builds a throwaway ValueSet (uuid url, one include for this code system carrying
|
|
36
|
+
every filter row) and posts it to $expand, replacing the page with the rendered expansion. -->
|
|
37
|
+
<div class="operation-form" style="margin-bottom: 15px;">
|
|
38
|
+
<strong>Filter</strong>
|
|
39
|
+
<form method="post" action="{{ expandUrl }}" id="{{ filterId }}_form"
|
|
40
|
+
data-system="{{ url }}" data-version="{{ systemVersion }}"
|
|
41
|
+
style="margin-left: 10px; margin-top: 5px;" onsubmit="return txFilterSubmit('{{ filterId }}')">
|
|
42
|
+
<datalist id="{{ filterId }}_props">{{ filterPropertyOptions }}</datalist>
|
|
43
|
+
<table class="grid" cellpadding="0" cellspacing="0">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr><th>Property</th><th>Operation</th><th>Value</th><th></th></tr>
|
|
46
|
+
</thead>
|
|
47
|
+
<tbody id="{{ filterId }}_rows">
|
|
48
|
+
<tr>
|
|
49
|
+
<td><input type="text" name="property" list="{{ filterId }}_props" size="24" autocomplete="off"/></td>
|
|
50
|
+
<td><select name="op">{{ filterOpOptions }}</select></td>
|
|
51
|
+
<td><input type="text" name="value" size="24"/></td>
|
|
52
|
+
<td><button type="button" class="btn btn-sm" title="Remove this filter" onclick="txFilterRemoveRow(this)">×</button></td>
|
|
53
|
+
</tr>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
<button type="button" class="btn btn-sm" onclick="txFilterAddRow('{{ filterId }}')">Add Filter</button>
|
|
57
|
+
<button type="submit" class="btn btn-sm btn-primary">Expand</button>
|
|
58
|
+
</form>
|
|
59
|
+
<div id="{{ filterId }}_preview" style="display: none; margin-left: 10px; margin-top: 8px;"></div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<script>
|
|
63
|
+
// The two forms on this tab that expand something both do the same thing: build an
|
|
64
|
+
// anonymous value set over the code system being viewed and post it to $expand, then
|
|
65
|
+
// replace the page with whatever comes back. They differ only in what they put on the
|
|
66
|
+
// include (nothing, for a text search) and what they put on the request (the filter).
|
|
67
|
+
|
|
68
|
+
function txOpMessage(id, text) {
|
|
69
|
+
var preview = document.getElementById(id + '_preview');
|
|
70
|
+
preview.textContent = text;
|
|
71
|
+
preview.style.display = 'block';
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function txOpUuid() {
|
|
76
|
+
if (window.crypto && typeof crypto.randomUUID === 'function') {
|
|
77
|
+
return crypto.randomUUID();
|
|
78
|
+
}
|
|
79
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
80
|
+
var r = Math.random() * 16 | 0;
|
|
81
|
+
return (c === 'x' ? r : ((r & 0x3) | 0x8)).toString(16);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// One include, so that filters are ANDed. The value set exists only for the duration of
|
|
86
|
+
// the expansion - it is never stored - so its url is a uuid.
|
|
87
|
+
function txOpBuildValueSet(form, filters) {
|
|
88
|
+
var include = { system: form.dataset.system };
|
|
89
|
+
if (form.dataset.version) {
|
|
90
|
+
include.version = form.dataset.version;
|
|
91
|
+
}
|
|
92
|
+
if (filters && filters.length > 0) {
|
|
93
|
+
include.filter = filters.map(function (f) {
|
|
94
|
+
return { property: f.property, op: f.op, value: f.value };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
resourceType: 'ValueSet',
|
|
99
|
+
// Marks the value set as one that exists only for this request, so that the server
|
|
100
|
+
// renders the expansion rather than a summary of a value set nobody can look up.
|
|
101
|
+
extension: [{ url: 'http://healthintersections.com.au/fhirsmith/StructureDefinition/valueset-transient', valueBoolean: true }],
|
|
102
|
+
url: 'urn:uuid:' + txOpUuid(),
|
|
103
|
+
status: 'active',
|
|
104
|
+
compose: { include: [include] }
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The value set goes in the body; anything that is an expansion parameter rather than
|
|
109
|
+
// part of the value set goes on the query, which $expand reads either way.
|
|
110
|
+
function txOpExpand(id, form, valueSet, params) {
|
|
111
|
+
var url = new URL(form.getAttribute('action'), window.location.href);
|
|
112
|
+
// The definition has to come back for the transient marker and the filters to survive
|
|
113
|
+
// the expansion - $expand drops both along with the rest of the definition otherwise.
|
|
114
|
+
url.searchParams.set('includeDefinition', 'true');
|
|
115
|
+
for (var name in params || {}) {
|
|
116
|
+
url.searchParams.set(name, params[name]);
|
|
117
|
+
}
|
|
118
|
+
txOpMessage(id, 'Expanding...');
|
|
119
|
+
fetch(url.toString(), {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/fhir+json', 'Accept': 'text/html' },
|
|
122
|
+
body: JSON.stringify(valueSet)
|
|
123
|
+
}).then(function (response) {
|
|
124
|
+
return response.text();
|
|
125
|
+
}).then(function (html) {
|
|
126
|
+
document.open();
|
|
127
|
+
document.write(html);
|
|
128
|
+
document.close();
|
|
129
|
+
}).catch(function (err) {
|
|
130
|
+
txOpMessage(id, 'Expansion failed: ' + err.message);
|
|
131
|
+
});
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function txSearchSubmit(id) {
|
|
136
|
+
var form = document.getElementById(id + '_form');
|
|
137
|
+
var text = form.querySelector('input[name="filter"]').value.trim();
|
|
138
|
+
if (text === '') {
|
|
139
|
+
return txOpMessage(id, 'Enter some text to search for.');
|
|
140
|
+
}
|
|
141
|
+
return txOpExpand(id, form, txOpBuildValueSet(form, null), { filter: text });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function txFilterClearRow(row) {
|
|
145
|
+
row.querySelectorAll('input').forEach(function (input) { input.value = ''; });
|
|
146
|
+
row.querySelectorAll('select').forEach(function (select) { select.selectedIndex = 0; });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function txFilterAddRow(id) {
|
|
150
|
+
var rows = document.getElementById(id + '_rows');
|
|
151
|
+
var row = rows.rows[0].cloneNode(true);
|
|
152
|
+
txFilterClearRow(row);
|
|
153
|
+
rows.appendChild(row);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function txFilterRemoveRow(btn) {
|
|
157
|
+
var row = btn.closest('tr');
|
|
158
|
+
var rows = row.parentNode;
|
|
159
|
+
if (rows.rows.length > 1) {
|
|
160
|
+
rows.removeChild(row);
|
|
161
|
+
} else {
|
|
162
|
+
txFilterClearRow(row);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Every row that has anything in it. Rows left entirely blank are ignored, so an unused
|
|
167
|
+
// extra row does not stop the expansion; a half-filled one is an error the user must fix.
|
|
168
|
+
function txFilterCollect(id) {
|
|
169
|
+
var filters = [];
|
|
170
|
+
var rows = document.getElementById(id + '_rows').rows;
|
|
171
|
+
for (var i = 0; i < rows.length; i++) {
|
|
172
|
+
var property = rows[i].querySelector('input[name="property"]').value.trim();
|
|
173
|
+
var op = rows[i].querySelector('select[name="op"]').value;
|
|
174
|
+
var value = rows[i].querySelector('input[name="value"]').value.trim();
|
|
175
|
+
if (property !== '' || value !== '') {
|
|
176
|
+
filters.push({ row: i + 1, property: property, op: op, value: value });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return filters;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function txFilterSubmit(id) {
|
|
183
|
+
var form = document.getElementById(id + '_form');
|
|
184
|
+
var filters = txFilterCollect(id);
|
|
185
|
+
if (filters.length === 0) {
|
|
186
|
+
return txOpMessage(id, 'No filters specified.');
|
|
187
|
+
}
|
|
188
|
+
for (var i = 0; i < filters.length; i++) {
|
|
189
|
+
if (filters[i].property === '' || filters[i].value === '') {
|
|
190
|
+
return txOpMessage(id, 'Filter ' + filters[i].row + ' needs both a property and a value.');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return txOpExpand(id, form, txOpBuildValueSet(form, filters), null);
|
|
194
|
+
}
|
|
195
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<p>Expand a SNOMED CT expression constraint. The expression is run as a value set that
|
|
2
|
+
composes SNOMED CT with a <code>constraint</code> filter; the result appears below, and
|
|
3
|
+
the inputs stay where they are so the expression can be adjusted and run again.</p>
|
|
4
|
+
|
|
5
|
+
{% if hasVersions %}
|
|
6
|
+
<div class="operation-form" style="margin-bottom: 15px;">
|
|
7
|
+
<form id="{{ eclId }}_form" action="{{ expandUrl }}" data-system="{{ system }}"
|
|
8
|
+
data-transient="{{ transientExtension }}" onsubmit="return eclRun('{{ eclId }}')">
|
|
9
|
+
<table class="grid" cellpadding="0" cellspacing="0" style="width: 100%;">
|
|
10
|
+
<tr>
|
|
11
|
+
<td style="width: 100%;">ECL: <input type="text" name="ecl" id="{{ eclId }}_ecl" size="80"
|
|
12
|
+
style="width: 90%;" autocomplete="off" placeholder="< 404684003 |Clinical finding|"/></td>
|
|
13
|
+
</tr>
|
|
14
|
+
<tr>
|
|
15
|
+
<td>Edition: <select name="version" id="{{ eclId }}_version">{{ versionOptions }}</select>
|
|
16
|
+
<button type="submit" class="btn btn-sm btn-primary" id="{{ eclId }}_go">Go</button>
|
|
17
|
+
</td>
|
|
18
|
+
</tr>
|
|
19
|
+
</table>
|
|
20
|
+
</form>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div id="{{ eclId }}_results"></div>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
function eclUuid() {
|
|
27
|
+
if (window.crypto && typeof crypto.randomUUID === 'function') {
|
|
28
|
+
return crypto.randomUUID();
|
|
29
|
+
}
|
|
30
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
31
|
+
var r = Math.random() * 16 | 0;
|
|
32
|
+
return (c === 'x' ? r : ((r & 0x3) | 0x8)).toString(16);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function eclMessage(id, text) {
|
|
37
|
+
document.getElementById(id + '_results').innerHTML = '';
|
|
38
|
+
var p = document.createElement('p');
|
|
39
|
+
p.textContent = text;
|
|
40
|
+
document.getElementById(id + '_results').appendChild(p);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The expansion comes back as a fragment rather than a page, so it can go straight under
|
|
45
|
+
// the inputs. An error arrives the same way - as the rendered OperationOutcome - which is
|
|
46
|
+
// the point: an ECL is usually wrong several times before it is right, and the message
|
|
47
|
+
// belongs next to the expression that caused it.
|
|
48
|
+
function eclRun(id) {
|
|
49
|
+
var form = document.getElementById(id + '_form');
|
|
50
|
+
var ecl = document.getElementById(id + '_ecl').value.trim();
|
|
51
|
+
var version = document.getElementById(id + '_version').value;
|
|
52
|
+
var go = document.getElementById(id + '_go');
|
|
53
|
+
|
|
54
|
+
if (ecl === '') {
|
|
55
|
+
return eclMessage(id, 'Enter an expression constraint.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var valueSet = {
|
|
59
|
+
resourceType: 'ValueSet',
|
|
60
|
+
extension: [{ url: form.dataset.transient, valueBoolean: true }],
|
|
61
|
+
url: 'urn:uuid:' + eclUuid(),
|
|
62
|
+
status: 'active',
|
|
63
|
+
compose: {
|
|
64
|
+
include: [{
|
|
65
|
+
system: form.dataset.system,
|
|
66
|
+
version: version,
|
|
67
|
+
filter: [{ property: 'constraint', op: '=', value: ecl }]
|
|
68
|
+
}]
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
eclMessage(id, 'Expanding...');
|
|
73
|
+
go.disabled = true;
|
|
74
|
+
fetch(form.getAttribute('action'), {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/fhir+json', 'Accept': 'text/html' },
|
|
77
|
+
body: JSON.stringify(valueSet)
|
|
78
|
+
}).then(function (response) {
|
|
79
|
+
return response.text();
|
|
80
|
+
}).then(function (html) {
|
|
81
|
+
document.getElementById(id + '_results').innerHTML = html;
|
|
82
|
+
}).catch(function (err) {
|
|
83
|
+
eclMessage(id, 'Expansion failed: ' + err.message);
|
|
84
|
+
}).then(function () {
|
|
85
|
+
go.disabled = false;
|
|
86
|
+
});
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
{% else %}
|
|
91
|
+
<p><em>This endpoint has no SNOMED CT editions loaded, so there is nothing to run an
|
|
92
|
+
expression constraint against.</em></p>
|
|
93
|
+
{% endif %}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<canvas id="memoryChart" height="200"></canvas>
|
|
5
5
|
</div>
|
|
6
6
|
<div style="flex: 1;">
|
|
7
|
-
<h6>
|
|
8
|
-
<canvas id="
|
|
7
|
+
<h6>Requests per Minute</h6>
|
|
8
|
+
<canvas id="requestChart" height="200"></canvas>
|
|
9
9
|
</div>
|
|
10
10
|
<div style="flex: 1;">
|
|
11
11
|
<h6>Turnaround Time</h6>
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
|
16
|
-
<div style="flex: 1;">
|
|
17
|
-
<h6>Requests per Minute</h6>
|
|
18
|
-
<canvas id="requestChart" height="200"></canvas>
|
|
19
|
-
</div>
|
|
20
16
|
<div style="flex: 1;">
|
|
21
17
|
<h6>Client Cache</h6>
|
|
22
18
|
<canvas id="clientCacheChart" height="200"></canvas>
|
|
23
19
|
</div>
|
|
24
20
|
<div style="flex: 1;">
|
|
25
|
-
<h6>Node Blocking</h6>
|
|
21
|
+
<h6>Node Blocking (mean)</h6>
|
|
26
22
|
<canvas id="blockChart" height="200"></canvas>
|
|
27
23
|
</div>
|
|
24
|
+
<div style="flex: 1;">
|
|
25
|
+
<h6>Node Blocking (max)</h6>
|
|
26
|
+
<canvas id="blockMaxChart" height="200"></canvas>
|
|
27
|
+
</div>
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
@@ -99,36 +99,6 @@
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
// Expansion cache chart
|
|
103
|
-
new Chart(document.getElementById('cacheChart'), {
|
|
104
|
-
type: 'line',
|
|
105
|
-
data: {
|
|
106
|
-
labels: historyData.map(d => formatTime(d.time)),
|
|
107
|
-
datasets: [{
|
|
108
|
-
data: historyData.map(d => d.expansion),
|
|
109
|
-
borderColor: '#7b7b7b',
|
|
110
|
-
backgroundColor: 'rgba(123, 123, 123, 0.1)',
|
|
111
|
-
fill: true,
|
|
112
|
-
tension: 0.3,
|
|
113
|
-
pointRadius: 2
|
|
114
|
-
}]
|
|
115
|
-
},
|
|
116
|
-
options: {
|
|
117
|
-
...chartOptions,
|
|
118
|
-
scales: {
|
|
119
|
-
...chartOptions.scales,
|
|
120
|
-
y: {
|
|
121
|
-
...chartOptions.scales.y,
|
|
122
|
-
title: {
|
|
123
|
-
display: true,
|
|
124
|
-
text: 'Expansion cache # items'
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
|
|
132
102
|
// Tat chart
|
|
133
103
|
new Chart(document.getElementById('tatChart'), {
|
|
134
104
|
type: 'line',
|
|
@@ -215,48 +185,41 @@
|
|
|
215
185
|
}
|
|
216
186
|
}
|
|
217
187
|
});
|
|
218
|
-
// Blocking
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// request-freezing stalls.
|
|
235
|
-
label: 'max',
|
|
236
|
-
data: historyData.map(d => d.blockMax),
|
|
237
|
-
borderColor: '#a72828',
|
|
238
|
-
fill: false,
|
|
239
|
-
tension: 0.3,
|
|
240
|
-
pointRadius: 2
|
|
241
|
-
}]
|
|
242
|
-
},
|
|
243
|
-
options: {
|
|
244
|
-
...chartOptions,
|
|
245
|
-
plugins: {
|
|
246
|
-
...chartOptions.plugins,
|
|
247
|
-
legend: { display: true }
|
|
188
|
+
// Blocking charts. Mean and max are separate charts because they are on different
|
|
189
|
+
// scales: a multi-second stall puts max in the thousands of ms while the mean sits at
|
|
190
|
+
// a few ms, so on one chart the mean is a flat line along the bottom.
|
|
191
|
+
function blockChart(id, field, colour, fill) {
|
|
192
|
+
new Chart(document.getElementById(id), {
|
|
193
|
+
type: 'line',
|
|
194
|
+
data: {
|
|
195
|
+
labels: historyData.map(d => formatTime(d.time)),
|
|
196
|
+
datasets: [{
|
|
197
|
+
data: historyData.map(d => d[field]),
|
|
198
|
+
borderColor: colour,
|
|
199
|
+
backgroundColor: fill,
|
|
200
|
+
fill: true,
|
|
201
|
+
tension: 0.3,
|
|
202
|
+
pointRadius: 2
|
|
203
|
+
}]
|
|
248
204
|
},
|
|
249
|
-
|
|
250
|
-
...chartOptions
|
|
251
|
-
|
|
252
|
-
...chartOptions.scales
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
205
|
+
options: {
|
|
206
|
+
...chartOptions,
|
|
207
|
+
scales: {
|
|
208
|
+
...chartOptions.scales,
|
|
209
|
+
y: {
|
|
210
|
+
...chartOptions.scales.y,
|
|
211
|
+
title: {
|
|
212
|
+
display: true,
|
|
213
|
+
text: 'ms'
|
|
214
|
+
}
|
|
256
215
|
}
|
|
257
216
|
}
|
|
258
217
|
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// the average event-loop delay over each interval
|
|
221
|
+
blockChart('blockChart', 'block', '#2845a7', 'rgba(40, 69, 167, 0.1)');
|
|
222
|
+
// the worst single stall in each interval: the one that shows request-freezing blocks
|
|
223
|
+
blockChart('blockMaxChart', 'blockMax', '#a72828', 'rgba(167, 40, 40, 0.1)');
|
|
261
224
|
})();
|
|
262
225
|
</script>
|
package/tx/html/tx-template.html
CHANGED
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
<a href="[%endpoint-path%]/metadata" style="color: gold">Capability Statement</a> |
|
|
90
90
|
<a href="[%endpoint-path%]/metadata?mode=terminology" style="color: gold">Terminology Capabilities</a> |
|
|
91
91
|
<a href="[%endpoint-path%]/op.html" style="color: gold">Operations</a> |
|
|
92
|
+
<a href="[%endpoint-path%]/ecl" style="color: gold">ECL</a> |
|
|
92
93
|
<a href="[%endpoint-path%]/problems.html" style="color: gold">Problems</a> |
|
|
93
94
|
[%library-link%]
|
|
94
95
|
|
package/tx/library/conceptmap.js
CHANGED
|
@@ -196,6 +196,14 @@ class ConceptMap extends CanonicalResource {
|
|
|
196
196
|
* their elements matched. group.unmapped only applies to a group that was in scope
|
|
197
197
|
* for the translation, so the caller needs the same source/target test that
|
|
198
198
|
* listTranslations applies - without the per-element filter.
|
|
199
|
+
*
|
|
200
|
+
* "In scope" here is only about this group's source and target code systems. It assumes
|
|
201
|
+
* the MAP itself is in scope, which is a separate and prior question: ConceptMap.sourceScope
|
|
202
|
+
* limits the map to codes that are members of that value set, and a map that does not cover
|
|
203
|
+
* the code has no groups in scope at all, whatever their systems say. That test needs a value
|
|
204
|
+
* set lookup and a $validate-code, so it lives in the worker (TranslateWorker.filterMapsByScope)
|
|
205
|
+
* and is applied to the map before this is ever called. Do not read a group coming back from
|
|
206
|
+
* here as evidence that the map applies.
|
|
199
207
|
* @returns {Object[]} the matching group objects
|
|
200
208
|
*/
|
|
201
209
|
listGroupsInScope(coding, targetScope, targetSystem) {
|
|
@@ -238,19 +246,27 @@ class ConceptMap extends CanonicalResource {
|
|
|
238
246
|
return result;
|
|
239
247
|
}
|
|
240
248
|
/**
|
|
241
|
-
* Gets the source scope (R5) or source
|
|
242
|
-
*
|
|
249
|
+
* Gets the source scope (R5+) or source (R3/R4).
|
|
250
|
+
*
|
|
251
|
+
* R5 renamed ConceptMap.source[x] to ConceptMap.sourceScope[x], so an R4 resource spells this
|
|
252
|
+
* sourceUri / sourceCanonical and an R5 one sourceScopeUri / sourceScopeCanonical. Both have to
|
|
253
|
+
* be read: this server serves R4 as well as R5, and the R4 client sends the R4 spelling. Reading
|
|
254
|
+
* only the R5 names made this silently undefined on an R4 server, which made every map look
|
|
255
|
+
* unscoped - so the scope never limited anything, and $translate answered for codes outside it.
|
|
256
|
+
* @returns {string|undefined} Source scope
|
|
243
257
|
*/
|
|
244
258
|
get sourceScope() {
|
|
245
|
-
return this.jsonObj.sourceScopeUri
|
|
259
|
+
return this.jsonObj.sourceScopeUri || this.jsonObj.sourceScopeCanonical
|
|
260
|
+
|| this.jsonObj.sourceUri || this.jsonObj.sourceCanonical;
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
/**
|
|
249
|
-
* Gets the target scope (R5) or target
|
|
250
|
-
* @returns {string|undefined} Target scope
|
|
264
|
+
* Gets the target scope (R5+) or target (R3/R4). See sourceScope for why both are read.
|
|
265
|
+
* @returns {string|undefined} Target scope
|
|
251
266
|
*/
|
|
252
267
|
get targetScope() {
|
|
253
|
-
return this.jsonObj.targetScopeUri
|
|
268
|
+
return this.jsonObj.targetScopeUri || this.jsonObj.targetScopeCanonical
|
|
269
|
+
|| this.jsonObj.targetUri || this.jsonObj.targetCanonical;
|
|
254
270
|
}
|
|
255
271
|
|
|
256
272
|
/**
|
|
@@ -26,6 +26,10 @@ class SearchFilterText {
|
|
|
26
26
|
|
|
27
27
|
this.filter = filter ? filter.toLowerCase() : null;
|
|
28
28
|
this.stems = [];
|
|
29
|
+
// The words the stems came from, index-aligned with stems. A provider whose text
|
|
30
|
+
// index cannot hold a particular token (RxNorm's stem table has nothing that starts
|
|
31
|
+
// with a digit, for one) needs the word itself to search the text with instead.
|
|
32
|
+
this.words = [];
|
|
29
33
|
this.stemmer = natural.PorterStemmer;
|
|
30
34
|
if (filter) {
|
|
31
35
|
this._process();
|
|
@@ -135,6 +139,7 @@ class SearchFilterText {
|
|
|
135
139
|
|
|
136
140
|
_process() {
|
|
137
141
|
let i = 0;
|
|
142
|
+
const found = [];
|
|
138
143
|
|
|
139
144
|
while (i < this.filter.length) {
|
|
140
145
|
if (this._isAlphaNumeric(this.filter[i])) {
|
|
@@ -142,14 +147,18 @@ class SearchFilterText {
|
|
|
142
147
|
while (i < this.filter.length && this._isAlphaNumeric(this.filter[i])) {
|
|
143
148
|
i++;
|
|
144
149
|
}
|
|
145
|
-
const word = this.filter.substring(j, i);
|
|
146
|
-
|
|
150
|
+
const word = this.filter.substring(j, i).toLowerCase();
|
|
151
|
+
found.push({ word: word, stem: this.stemmer.stem(word) });
|
|
147
152
|
} else {
|
|
148
153
|
i++;
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
// Sorted by stem, because _find() binary searches stems - words ride along so that
|
|
158
|
+
// words[n] is always the word stems[n] came from.
|
|
159
|
+
found.sort((a, b) => (a.stem < b.stem ? -1 : (a.stem > b.stem ? 1 : 0)));
|
|
160
|
+
this.stems = found.map((f) => f.stem);
|
|
161
|
+
this.words = found.map((f) => f.word);
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
_isAlphaNumeric(char) {
|
|
@@ -198,7 +198,9 @@ class OperationOutcome {
|
|
|
198
198
|
listMissedErrors(list) {
|
|
199
199
|
for (let iss of this.jsonObj.issue || []) {
|
|
200
200
|
if (iss.severity === 'error' && iss.details && iss.details.text && !list.find(msg => msg === iss.details.text )) {
|
|
201
|
-
return
|
|
201
|
+
// every error that nothing else reported, not just the first one - an early return here
|
|
202
|
+
// meant the summary message named only one of them
|
|
203
|
+
list.push(iss.details.text);
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
|
package/tx/library/renderer.js
CHANGED
|
@@ -1445,6 +1445,7 @@ class Renderer {
|
|
|
1445
1445
|
|
|
1446
1446
|
// Analyze columns needed
|
|
1447
1447
|
const columnInfo = this.analyzeExpansionColumns(vs.expansion);
|
|
1448
|
+
columnInfo.versions = this.expansionVersions(vs);
|
|
1448
1449
|
|
|
1449
1450
|
// Build the expansion table
|
|
1450
1451
|
const expTbl = x.table("codes");
|
|
@@ -1595,6 +1596,40 @@ class Renderer {
|
|
|
1595
1596
|
/**
|
|
1596
1597
|
* Add a row for an expansion contains entry
|
|
1597
1598
|
*/
|
|
1599
|
+
/**
|
|
1600
|
+
* The version each code system was expanded at, by system uri.
|
|
1601
|
+
*
|
|
1602
|
+
* An expansion.contains only carries a version when the expansion needed to distinguish
|
|
1603
|
+
* one, so a link built from the row alone would silently point at whichever version the
|
|
1604
|
+
* server happens to prefer - not what the reader is looking at. The expansion says which
|
|
1605
|
+
* version it used in its used-codesystem parameters, and a value set that came with its
|
|
1606
|
+
* definition says so in its compose; either will do.
|
|
1607
|
+
*
|
|
1608
|
+
* @param {Object} vs the expanded ValueSet
|
|
1609
|
+
* @returns {Map<string, string>} system uri to version
|
|
1610
|
+
*/
|
|
1611
|
+
expansionVersions(vs) {
|
|
1612
|
+
const versions = new Map();
|
|
1613
|
+
for (const inc of vs.compose?.include || []) {
|
|
1614
|
+
if (inc.system && inc.version && !versions.has(inc.system)) {
|
|
1615
|
+
versions.set(inc.system, inc.version);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
for (const p of vs.expansion?.parameter || []) {
|
|
1619
|
+
if (p.name !== 'used-codesystem') {
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
const vurl = getValuePrimitive(p);
|
|
1623
|
+
if (vurl && vurl.includes('|')) {
|
|
1624
|
+
const system = vurl.substring(0, vurl.indexOf('|'));
|
|
1625
|
+
if (!versions.has(system)) {
|
|
1626
|
+
versions.set(system, vurl.substring(vurl.indexOf('|') + 1));
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return versions;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1598
1633
|
async addExpansionRow(tbl, contains, level, columnInfo) {
|
|
1599
1634
|
const tr = tbl.tr();
|
|
1600
1635
|
|
|
@@ -1617,12 +1652,14 @@ class Renderer {
|
|
|
1617
1652
|
codeTd.tx('\u00A0'.repeat(level * 2)); // Non-breaking spaces for indentation
|
|
1618
1653
|
}
|
|
1619
1654
|
|
|
1620
|
-
//
|
|
1655
|
+
// The code links to $lookup on it - what a reader wants from a row in an expansion is
|
|
1656
|
+
// to find out what that code is - at the version the expansion used.
|
|
1621
1657
|
if (contains.code) {
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1658
|
+
const version = contains.version || columnInfo.versions?.get(contains.system) || null;
|
|
1659
|
+
const link = this.linkResolver?.lookupLink
|
|
1660
|
+
? this.linkResolver.lookupLink(contains.system, version, contains.code) : null;
|
|
1624
1661
|
if (link) {
|
|
1625
|
-
codeTd.ah(link.
|
|
1662
|
+
codeTd.ah(link, 'Look this code up').code().tx(contains.code);
|
|
1626
1663
|
} else {
|
|
1627
1664
|
codeTd.code().tx(contains.code);
|
|
1628
1665
|
}
|
|
@@ -2732,4 +2769,4 @@ class Renderer {
|
|
|
2732
2769
|
|
|
2733
2770
|
}
|
|
2734
2771
|
|
|
2735
|
-
module.exports = { Renderer };
|
|
2772
|
+
module.exports = { Renderer, VALID_FILTER_OPS };
|