fhirsmith 0.13.4 → 0.14.1
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 +61 -1
- package/README.md +16 -3
- package/config-template.json +81 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/library/languages.js +15 -0
- package/library/same-origin.js +62 -0
- package/nginx.md +399 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +241 -56
- package/registry/registry.js +3 -1
- package/server.js +57 -15
- package/stats.js +19 -13
- package/testing/readme.md +144 -0
- package/testing/render.js +723 -0
- package/testing/search.js +378 -0
- package/testing/store.js +410 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +760 -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 +19 -69
- 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/editions.js +70 -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/expand.js +2 -2
- 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 +192 -21
- package/tx/workers/validate.js +70 -187
- package/tx/workers/worker.js +12 -35
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
3
|
+
//
|
|
4
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* FHIR search parameter handling for the /testing module.
|
|
9
|
+
*
|
|
10
|
+
* Turns a query (as express parses it: repeated parameters arrive as arrays) into SQL
|
|
11
|
+
* clauses over the reports table. None of the parameters here are formally defined as
|
|
12
|
+
* SearchParameter resources yet, so the behaviour is simply what this file does:
|
|
13
|
+
*
|
|
14
|
+
* name, tester string: starts-with, case-insensitive; :exact, :contains, :missing
|
|
15
|
+
* status, result token: exact code (a system| prefix is ignored); :not, :missing
|
|
16
|
+
* testscript uri: exact; :below (starts-with), :missing
|
|
17
|
+
* participant uri of any participant: exact; :below
|
|
18
|
+
* score number: eq ne gt lt ge le sa eb ap, with implicit precision for eq
|
|
19
|
+
* issued, _lastUpdated date: eq ne gt lt ge le sa eb ap, compared as ranges
|
|
20
|
+
* _id exact
|
|
21
|
+
* _sort any of the above (not _id), comma separated, '-' for descending
|
|
22
|
+
* _count, _offset paging; _summary=count for the total alone
|
|
23
|
+
*
|
|
24
|
+
* Comma separated values are ORed, repeated parameters are ANDed. Empty values are
|
|
25
|
+
* ignored, as the spec says. Unknown parameters and modifiers are reported back in
|
|
26
|
+
* `unknown` so the caller can decide whether to be lenient or strict; values that
|
|
27
|
+
* can't be understood (a bad date or number) are reported in `errors`, and those are
|
|
28
|
+
* always an error.
|
|
29
|
+
*
|
|
30
|
+
* @module testing/search
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const DEFAULT_COUNT = 50;
|
|
34
|
+
const MAX_COUNT = 500;
|
|
35
|
+
|
|
36
|
+
// the column each parameter searches, and the one it sorts on
|
|
37
|
+
const PARAMS = {
|
|
38
|
+
_id: { type: 'id', col: 'r.id' },
|
|
39
|
+
name: { type: 'string', col: 'r.name', lc: 'r.name_lc', sort: 'r.name_lc' },
|
|
40
|
+
tester: { type: 'string', col: 'r.tester', lc: 'r.tester_lc', sort: 'r.tester_lc' },
|
|
41
|
+
status: { type: 'token', col: 'r.status', sort: 'r.status' },
|
|
42
|
+
result: { type: 'token', col: 'r.result', sort: 'r.result' },
|
|
43
|
+
testscript: { type: 'uri', col: 'r.test_script', sort: 'r.test_script' },
|
|
44
|
+
participant: { type: 'participant' },
|
|
45
|
+
score: { type: 'number', col: 'r.score', sort: 'r.score' },
|
|
46
|
+
issued: { type: 'date', lo: 'r.issued_lo', hi: 'r.issued_hi', sort: 'r.issued_lo' },
|
|
47
|
+
_lastUpdated: { type: 'date', lo: 'r.received_ms', hi: '(r.received_ms + 1)', sort: 'r.received_ms' }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MODIFIERS = {
|
|
51
|
+
id: [],
|
|
52
|
+
string: ['exact', 'contains', 'missing'],
|
|
53
|
+
token: ['not', 'missing'],
|
|
54
|
+
uri: ['below', 'missing'],
|
|
55
|
+
participant: ['below'],
|
|
56
|
+
number: ['missing'],
|
|
57
|
+
date: []
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// parameters that control the search rather than filter it
|
|
61
|
+
const CONTROL = new Set(['_sort', '_count', '_offset', '_summary', '_format', '_total', '_pretty']);
|
|
62
|
+
|
|
63
|
+
const PREFIXES = ['eq', 'ne', 'gt', 'lt', 'ge', 'le', 'sa', 'eb', 'ap'];
|
|
64
|
+
|
|
65
|
+
const DATE_RE = /^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?)?)?$/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The range of instants a FHIR date, dateTime or instant covers, as [lo, hi) in epoch
|
|
69
|
+
* milliseconds. "2026" covers the whole year; "2026-09-24T10:00:00.5Z" covers 100ms.
|
|
70
|
+
* A time with no zone is taken as UTC (FHIR requires a zone, but search values often
|
|
71
|
+
* leave it off).
|
|
72
|
+
*
|
|
73
|
+
* @param {string} value
|
|
74
|
+
* @returns {{lo: number, hi: number}|null} null if the value isn't a date
|
|
75
|
+
*/
|
|
76
|
+
function dateRange(value) {
|
|
77
|
+
if (typeof value !== 'string') {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const m = DATE_RE.exec(value.trim());
|
|
81
|
+
if (!m) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const [, y, mo, d, h, mi, s, frac, tz] = m;
|
|
85
|
+
const year = parseInt(y, 10);
|
|
86
|
+
if (!mo) {
|
|
87
|
+
return { lo: Date.UTC(year, 0, 1), hi: Date.UTC(year + 1, 0, 1) };
|
|
88
|
+
}
|
|
89
|
+
const month = parseInt(mo, 10) - 1;
|
|
90
|
+
if (month < 0 || month > 11) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (!d) {
|
|
94
|
+
return { lo: Date.UTC(year, month, 1), hi: Date.UTC(year, month + 1, 1) };
|
|
95
|
+
}
|
|
96
|
+
const day = parseInt(d, 10);
|
|
97
|
+
if (day < 1 || day > new Date(Date.UTC(year, month + 1, 0)).getUTCDate()) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (!h) {
|
|
101
|
+
return { lo: Date.UTC(year, month, day), hi: Date.UTC(year, month, day + 1) };
|
|
102
|
+
}
|
|
103
|
+
const zone = tz || 'Z';
|
|
104
|
+
const text = `${y}-${mo}-${d}T${h}:${mi}:${s || '00'}${frac || ''}${zone}`;
|
|
105
|
+
const lo = Date.parse(text);
|
|
106
|
+
if (Number.isNaN(lo) || parseInt(h, 10) > 23 || parseInt(mi, 10) > 59 || (s && parseInt(s, 10) > 59)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
let width;
|
|
110
|
+
if (!s) {
|
|
111
|
+
width = 60000;
|
|
112
|
+
} else if (!frac) {
|
|
113
|
+
width = 1000;
|
|
114
|
+
} else {
|
|
115
|
+
// .5 is 100ms wide, .50 10ms, .500 and beyond 1ms
|
|
116
|
+
width = Math.max(1, Math.pow(10, 3 - (frac.length - 1)));
|
|
117
|
+
}
|
|
118
|
+
return { lo, hi: lo + width };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function splitPrefix(value) {
|
|
122
|
+
const p = value.substring(0, 2);
|
|
123
|
+
if (PREFIXES.includes(p) && value.length > 2 && !/^[a-z]/.test(value.substring(2))) {
|
|
124
|
+
return { prefix: p, rest: value.substring(2) };
|
|
125
|
+
}
|
|
126
|
+
return { prefix: 'eq', rest: value };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function escapeLike(value) {
|
|
130
|
+
return value.replace(/[\\%_]/g, (c) => '\\' + c);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Split a parameter value on commas, honouring \, as an escaped comma.
|
|
135
|
+
*/
|
|
136
|
+
function splitValues(value) {
|
|
137
|
+
const out = [];
|
|
138
|
+
let cur = '';
|
|
139
|
+
for (let i = 0; i < value.length; i++) {
|
|
140
|
+
const c = value[i];
|
|
141
|
+
if (c === '\\' && i + 1 < value.length) {
|
|
142
|
+
cur += value[i + 1];
|
|
143
|
+
i++;
|
|
144
|
+
} else if (c === ',') {
|
|
145
|
+
out.push(cur);
|
|
146
|
+
cur = '';
|
|
147
|
+
} else {
|
|
148
|
+
cur += c;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
out.push(cur);
|
|
152
|
+
return out.filter(v => v.length > 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function dateClause(def, prefix, range) {
|
|
156
|
+
const { lo, hi } = def;
|
|
157
|
+
switch (prefix) {
|
|
158
|
+
case 'eq': return { sql: `(${lo} >= ? AND ${hi} <= ?)`, args: [range.lo, range.hi] };
|
|
159
|
+
case 'ne': return { sql: `NOT (${lo} >= ? AND ${hi} <= ?)`, args: [range.lo, range.hi] };
|
|
160
|
+
case 'gt': return { sql: `${hi} > ?`, args: [range.hi] };
|
|
161
|
+
case 'lt': return { sql: `${lo} < ?`, args: [range.lo] };
|
|
162
|
+
case 'ge': return { sql: `${hi} > ?`, args: [range.lo] };
|
|
163
|
+
case 'le': return { sql: `${lo} < ?`, args: [range.hi] };
|
|
164
|
+
case 'sa': return { sql: `${lo} >= ?`, args: [range.hi] };
|
|
165
|
+
case 'eb': return { sql: `${hi} <= ?`, args: [range.lo] };
|
|
166
|
+
case 'ap': return { sql: `(${lo} < ? AND ${hi} > ?)`, args: [range.hi, range.lo] };
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function numberClause(col, prefix, text) {
|
|
172
|
+
if (!/^[+-]?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(text)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const n = Number(text);
|
|
176
|
+
// implicit precision: "90" means [89.5, 90.5), "90.0" means [89.95, 90.05)
|
|
177
|
+
const dot = text.split(/[eE]/)[0].split('.')[1];
|
|
178
|
+
const half = /[eE]/.test(text) ? 0 : 0.5 * Math.pow(10, -(dot ? dot.length : 0));
|
|
179
|
+
switch (prefix) {
|
|
180
|
+
case 'eq': return half ? { sql: `(${col} >= ? AND ${col} < ?)`, args: [n - half, n + half] } : { sql: `${col} = ?`, args: [n] };
|
|
181
|
+
case 'ne': return half ? { sql: `NOT (${col} >= ? AND ${col} < ?)`, args: [n - half, n + half] } : { sql: `${col} <> ?`, args: [n] };
|
|
182
|
+
case 'gt': case 'sa': return { sql: `${col} > ?`, args: [n] };
|
|
183
|
+
case 'lt': case 'eb': return { sql: `${col} < ?`, args: [n] };
|
|
184
|
+
case 'ge': return { sql: `${col} >= ?`, args: [n] };
|
|
185
|
+
case 'le': return { sql: `${col} <= ?`, args: [n] };
|
|
186
|
+
case 'ap': {
|
|
187
|
+
const d = Math.abs(n) * 0.1;
|
|
188
|
+
return { sql: `(${col} >= ? AND ${col} <= ?)`, args: [n - d, n + d] };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function valueClause(def, modifier, value) {
|
|
195
|
+
switch (def.type) {
|
|
196
|
+
case 'id':
|
|
197
|
+
return { sql: `${def.col} = ?`, args: [value] };
|
|
198
|
+
case 'string': {
|
|
199
|
+
if (modifier === 'exact') {
|
|
200
|
+
return { sql: `${def.col} = ?`, args: [value] };
|
|
201
|
+
}
|
|
202
|
+
const v = escapeLike(value.toLowerCase());
|
|
203
|
+
return {
|
|
204
|
+
sql: `${def.lc} LIKE ? ESCAPE '\\'`,
|
|
205
|
+
args: [modifier === 'contains' ? `%${v}%` : `${v}%`]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
case 'token': {
|
|
209
|
+
// system|code - these elements are plain codes, so the system is ignored
|
|
210
|
+
const code = value.includes('|') ? value.substring(value.indexOf('|') + 1) : value;
|
|
211
|
+
return { sql: modifier === 'not' ? `(${def.col} IS NULL OR ${def.col} <> ?)` : `${def.col} = ?`, args: [code] };
|
|
212
|
+
}
|
|
213
|
+
case 'uri':
|
|
214
|
+
if (modifier === 'below') {
|
|
215
|
+
return { sql: `${def.col} LIKE ? ESCAPE '\\'`, args: [escapeLike(value) + '%'] };
|
|
216
|
+
}
|
|
217
|
+
return { sql: `${def.col} = ?`, args: [value] };
|
|
218
|
+
case 'participant':
|
|
219
|
+
if (modifier === 'below') {
|
|
220
|
+
return {
|
|
221
|
+
sql: `EXISTS (SELECT 1 FROM participants p WHERE p.report_id = r.id AND p.uri LIKE ? ESCAPE '\\')`,
|
|
222
|
+
args: [escapeLike(value) + '%']
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return { sql: 'EXISTS (SELECT 1 FROM participants p WHERE p.report_id = r.id AND p.uri = ?)', args: [value] };
|
|
226
|
+
case 'number': {
|
|
227
|
+
const { prefix, rest } = splitPrefix(value);
|
|
228
|
+
return numberClause(def.col, prefix, rest);
|
|
229
|
+
}
|
|
230
|
+
case 'date': {
|
|
231
|
+
const { prefix, rest } = splitPrefix(value);
|
|
232
|
+
const range = dateRange(rest);
|
|
233
|
+
return range ? dateClause(def, prefix, range) : null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function asList(v) {
|
|
240
|
+
if (v === undefined || v === null) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
return (Array.isArray(v) ? v : [v]).filter(x => typeof x === 'string');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function intParam(v, dflt) {
|
|
247
|
+
const s = asList(v)[0];
|
|
248
|
+
if (s === undefined || s === '') {
|
|
249
|
+
return dflt;
|
|
250
|
+
}
|
|
251
|
+
return /^\d+$/.test(s) ? parseInt(s, 10) : NaN;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse a search.
|
|
256
|
+
*
|
|
257
|
+
* @param {Object} query - parsed query string; values are strings or arrays of strings
|
|
258
|
+
* @returns {{
|
|
259
|
+
* where: string[], args: any[], orderBy: string, count: number, offset: number,
|
|
260
|
+
* summaryCount: boolean, used: Array<[string,string]>, sort: string|null,
|
|
261
|
+
* unknown: string[], errors: string[]
|
|
262
|
+
* }}
|
|
263
|
+
*/
|
|
264
|
+
function parseSearch(query) {
|
|
265
|
+
const result = {
|
|
266
|
+
where: [], args: [], orderBy: 'r.received_ms DESC, r.rowid DESC', count: DEFAULT_COUNT, offset: 0,
|
|
267
|
+
summaryCount: false, used: [], sort: null, unknown: [], errors: []
|
|
268
|
+
};
|
|
269
|
+
for (const [key, raw] of Object.entries(query || {})) {
|
|
270
|
+
if (CONTROL.has(key)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const colon = key.indexOf(':');
|
|
274
|
+
const name = colon < 0 ? key : key.substring(0, colon);
|
|
275
|
+
const modifier = colon < 0 ? null : key.substring(colon + 1);
|
|
276
|
+
const def = Object.prototype.hasOwnProperty.call(PARAMS, name) ? PARAMS[name] : null;
|
|
277
|
+
if (!def) {
|
|
278
|
+
result.unknown.push(key);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (modifier !== null && !MODIFIERS[def.type].includes(modifier)) {
|
|
282
|
+
result.unknown.push(key);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const value of asList(raw)) {
|
|
286
|
+
if (value === '') {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (modifier === 'missing') {
|
|
290
|
+
if (value !== 'true' && value !== 'false') {
|
|
291
|
+
result.errors.push(`${key}: '${value}' is not true or false`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
result.where.push(`${def.col} IS ${value === 'true' ? '' : 'NOT '}NULL`);
|
|
295
|
+
result.used.push([key, value]);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const clauses = [];
|
|
299
|
+
for (const v of splitValues(value)) {
|
|
300
|
+
const c = valueClause(def, modifier, v);
|
|
301
|
+
if (!c) {
|
|
302
|
+
result.errors.push(`${key}: '${v}' is not a valid ${def.type}`);
|
|
303
|
+
} else {
|
|
304
|
+
clauses.push(c);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (clauses.length > 0) {
|
|
308
|
+
result.where.push(clauses.length === 1 ? clauses[0].sql : '(' + clauses.map(c => c.sql).join(' OR ') + ')');
|
|
309
|
+
for (const c of clauses) {
|
|
310
|
+
result.args.push(...c.args);
|
|
311
|
+
}
|
|
312
|
+
result.used.push([key, value]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// _sort
|
|
318
|
+
const sortSpec = asList(query && query._sort).join(',');
|
|
319
|
+
if (sortSpec) {
|
|
320
|
+
const parts = [];
|
|
321
|
+
for (const item of sortSpec.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
322
|
+
const desc = item.startsWith('-');
|
|
323
|
+
const name = desc ? item.substring(1) : item;
|
|
324
|
+
const def = Object.prototype.hasOwnProperty.call(PARAMS, name) ? PARAMS[name] : null;
|
|
325
|
+
if (!def || !def.sort) {
|
|
326
|
+
result.errors.push(`_sort: cannot sort on '${name}'`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
parts.push(`${def.sort} ${desc ? 'DESC' : 'ASC'}`);
|
|
330
|
+
}
|
|
331
|
+
if (parts.length > 0) {
|
|
332
|
+
result.orderBy = parts.join(', ') + ', r.received_ms DESC, r.rowid DESC';
|
|
333
|
+
result.sort = sortSpec;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const count = intParam(query && query._count, DEFAULT_COUNT);
|
|
338
|
+
if (Number.isNaN(count)) {
|
|
339
|
+
result.errors.push('_count must be a non-negative integer');
|
|
340
|
+
} else {
|
|
341
|
+
result.count = Math.min(count, MAX_COUNT);
|
|
342
|
+
}
|
|
343
|
+
const offset = intParam(query && query._offset, 0);
|
|
344
|
+
if (Number.isNaN(offset)) {
|
|
345
|
+
result.errors.push('_offset must be a non-negative integer');
|
|
346
|
+
} else {
|
|
347
|
+
result.offset = offset;
|
|
348
|
+
}
|
|
349
|
+
result.summaryCount = asList(query && query._summary)[0] === 'count' || result.count === 0;
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The search parameters, for the CapabilityStatement.
|
|
355
|
+
*/
|
|
356
|
+
function capabilitySearchParams() {
|
|
357
|
+
const docs = {
|
|
358
|
+
_id: 'The logical id of the report',
|
|
359
|
+
name: 'TestReport.name - starts with, case insensitive; :exact, :contains',
|
|
360
|
+
tester: 'TestReport.tester - starts with, case insensitive; :exact, :contains',
|
|
361
|
+
status: 'TestReport.status',
|
|
362
|
+
result: 'TestReport.result',
|
|
363
|
+
testscript: 'TestReport.testScript (canonical, or R4 reference); :below for starts with',
|
|
364
|
+
participant: 'TestReport.participant.uri - any participant; :below for starts with',
|
|
365
|
+
score: 'TestReport.score',
|
|
366
|
+
issued: 'TestReport.issued',
|
|
367
|
+
_lastUpdated: 'When the report was received by this server'
|
|
368
|
+
};
|
|
369
|
+
const types = { id: 'token', string: 'string', token: 'token', uri: 'uri', participant: 'uri', number: 'number', date: 'date' };
|
|
370
|
+
return Object.entries(PARAMS).map(([name, def]) => ({
|
|
371
|
+
name, type: types[def.type], documentation: docs[name]
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
parseSearch, dateRange, splitValues, capabilitySearchParams,
|
|
377
|
+
PARAMS, DEFAULT_COUNT, MAX_COUNT
|
|
378
|
+
};
|