@fnndsc/brasa 0.16.0 → 0.17.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/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/buildinfo.json +1 -1
- package/dist/builtins/fs/acl.js +57 -1
- package/dist/builtins/fs/acl.js.map +1 -1
- package/dist/builtins/fs/cat.js +46 -9
- package/dist/builtins/fs/cat.js.map +1 -1
- package/dist/builtins/fs/cp.js +42 -2
- package/dist/builtins/fs/cp.js.map +1 -1
- package/dist/builtins/fs/destination.d.ts +52 -0
- package/dist/builtins/fs/destination.js +52 -0
- package/dist/builtins/fs/destination.js.map +1 -0
- package/dist/builtins/fs/download.js +5 -2
- package/dist/builtins/fs/download.js.map +1 -1
- package/dist/builtins/fs/mv.js +41 -1
- package/dist/builtins/fs/mv.js.map +1 -1
- package/dist/builtins/fs/rm.d.ts +6 -0
- package/dist/builtins/fs/rm.js +35 -17
- package/dist/builtins/fs/rm.js.map +1 -1
- package/dist/builtins/fs/upload.js +15 -4
- package/dist/builtins/fs/upload.js.map +1 -1
- package/dist/builtins/help.js +43 -6
- package/dist/builtins/help.js.map +1 -1
- package/dist/builtins/net/pacs.js +37 -1
- package/dist/builtins/net/pacs.js.map +1 -1
- package/dist/builtins/net/query.csv.d.ts +73 -0
- package/dist/builtins/net/query.csv.js +267 -0
- package/dist/builtins/net/query.csv.js.map +1 -0
- package/dist/builtins/net/query.d.ts +28 -1
- package/dist/builtins/net/query.fanout.d.ts +92 -0
- package/dist/builtins/net/query.fanout.js +171 -0
- package/dist/builtins/net/query.fanout.js.map +1 -0
- package/dist/builtins/net/query.js +709 -20
- package/dist/builtins/net/query.js.map +1 -1
- package/dist/builtins/net/status.js +4 -1
- package/dist/builtins/net/status.js.map +1 -1
- package/dist/builtins/proc.js +18 -0
- package/dist/builtins/proc.js.map +1 -1
- package/dist/builtins/res/feed.js +43 -2
- package/dist/builtins/res/feed.js.map +1 -1
- package/dist/builtins/res/plugin.info.d.ts +32 -0
- package/dist/builtins/res/plugin.info.js +167 -0
- package/dist/builtins/res/plugin.info.js.map +1 -0
- package/dist/builtins/res/plugin.js +41 -1
- package/dist/builtins/res/plugin.js.map +1 -1
- package/dist/core/engine.d.ts +21 -0
- package/dist/core/engine.js +27 -0
- package/dist/core/engine.js.map +1 -1
- package/dist/core/promptContext.d.ts +1 -1
- package/dist/core/question.d.ts +27 -0
- package/dist/core/question.js +38 -0
- package/dist/core/question.js.map +1 -1
- package/dist/core/surface.d.ts +16 -3
- package/dist/core/surface.js.map +1 -1
- package/dist/lib/vfs/providers/static_content.js +10 -81
- package/dist/lib/vfs/providers/static_content.js.map +1 -1
- package/dist/lib/vfs/vfs.js +13 -1
- package/dist/lib/vfs/vfs.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/** The columns of a PACS answer's table, in reading order. */
|
|
2
|
+
const CSV_COLUMNS = [
|
|
3
|
+
'MRN', 'PATIENT', 'SERVER', 'STATUS', 'STUDY', 'DATE',
|
|
4
|
+
'ACCESSION', 'MODALITY', 'SERIES', 'ANSWERED',
|
|
5
|
+
];
|
|
6
|
+
/**
|
|
7
|
+
* Quotes one cell.
|
|
8
|
+
*
|
|
9
|
+
* Everything is quoted, including numbers: a study description carrying a
|
|
10
|
+
* comma is the ordinary case, not the edge one, and a renderer that quotes
|
|
11
|
+
* only when it thinks it must is a renderer that will one day think wrong.
|
|
12
|
+
*
|
|
13
|
+
* @param value - The cell's value.
|
|
14
|
+
* @returns The quoted cell.
|
|
15
|
+
*/
|
|
16
|
+
function cell_quote(value) {
|
|
17
|
+
return `"${String(value ?? '').split('"').join('""')}"`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Renders one answer as CSV.
|
|
21
|
+
*
|
|
22
|
+
* ANSWERED carries the ISO timestamp rather than `3 MONTHS AGO`: a
|
|
23
|
+
* spreadsheet sorts and subtracts dates, and the phrase a surface reads out
|
|
24
|
+
* is for a human glance, not for a column of data.
|
|
25
|
+
*
|
|
26
|
+
* @param model - The answer.
|
|
27
|
+
* @returns The CSV text, header row first.
|
|
28
|
+
*/
|
|
29
|
+
export function pacsAnswer_toCsv(model) {
|
|
30
|
+
const rows = [];
|
|
31
|
+
const patients = model.patients ?? [];
|
|
32
|
+
const studiesOf = (patient) => model.studies.filter((study) => study.patientId === patient.patientId
|
|
33
|
+
&& (patient.server === undefined || study.server === undefined || study.server === patient.server));
|
|
34
|
+
const study_row = (study, facts) => [
|
|
35
|
+
facts.mrn,
|
|
36
|
+
study.patientName,
|
|
37
|
+
facts.server,
|
|
38
|
+
facts.status,
|
|
39
|
+
study.description,
|
|
40
|
+
study.date,
|
|
41
|
+
study.accession,
|
|
42
|
+
study.modalities,
|
|
43
|
+
String(study.series.length),
|
|
44
|
+
facts.answeredAt,
|
|
45
|
+
];
|
|
46
|
+
if (patients.length > 0) {
|
|
47
|
+
for (const patient of patients) {
|
|
48
|
+
const server = patient.server ?? model.pacsName;
|
|
49
|
+
const answeredAt = patient.provenance?.answeredAt ?? '';
|
|
50
|
+
const owned = studiesOf(patient);
|
|
51
|
+
if (owned.length === 0) {
|
|
52
|
+
// The row that cannot be derived from studies, and the one an
|
|
53
|
+
// audit is usually about: a patient with no imaging, or one whose
|
|
54
|
+
// question could not be asked at all.
|
|
55
|
+
rows.push([
|
|
56
|
+
patient.patientId, patient.patientName ?? '', server, patient.status,
|
|
57
|
+
'', '', '', '', '0', answeredAt,
|
|
58
|
+
]);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const study of owned) {
|
|
62
|
+
rows.push(study_row(study, {
|
|
63
|
+
mrn: patient.patientId, server, status: patient.status, answeredAt,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (const study of model.studies) {
|
|
70
|
+
rows.push(study_row(study, {
|
|
71
|
+
mrn: study.patientId,
|
|
72
|
+
server: study.server ?? model.pacsName,
|
|
73
|
+
status: 'found',
|
|
74
|
+
answeredAt: model.provenance?.answeredAt ?? '',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const lines = [CSV_COLUMNS.map(cell_quote).join(',')];
|
|
79
|
+
for (const row of rows)
|
|
80
|
+
lines.push(row.map(cell_quote).join(','));
|
|
81
|
+
return `${lines.join('\n')}\n`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Paths a file cannot be written to.
|
|
85
|
+
*
|
|
86
|
+
* These are providers — commands, processes, PACS queries — not storage. A
|
|
87
|
+
* write into one fails inside CUBE with a message about an upload, which
|
|
88
|
+
* tells an operator nothing about why the place they chose was never a
|
|
89
|
+
* place at all.
|
|
90
|
+
*/
|
|
91
|
+
const VIRTUAL_PREFIXES = ['/bin', '/usr', '/etc', '/proc', '/net'];
|
|
92
|
+
/** How long to wait for a removed file to actually stop resolving. */
|
|
93
|
+
const GONE_TIMEOUT_MS = 10_000;
|
|
94
|
+
/** How often to ask whether it has. */
|
|
95
|
+
const GONE_POLL_MS = 400;
|
|
96
|
+
/**
|
|
97
|
+
* Whether a resolved path names a provider rather than storage.
|
|
98
|
+
*
|
|
99
|
+
* @param resolved - An absolute CFS path.
|
|
100
|
+
* @returns True when nothing can be written there.
|
|
101
|
+
*/
|
|
102
|
+
function path_isVirtual(resolved) {
|
|
103
|
+
return VIRTUAL_PREFIXES.some((prefix) => resolved === prefix || resolved.startsWith(`${prefix}/`));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Writes a rendered table into ChRIS storage.
|
|
107
|
+
*
|
|
108
|
+
* The path plumbing lives here rather than in the command: rendering a
|
|
109
|
+
* table and putting it somewhere are one job, and the command should read
|
|
110
|
+
* as the decision it makes rather than as the mechanics it performs.
|
|
111
|
+
*
|
|
112
|
+
* Imported where they are used — a CSV destination is a rare path, and
|
|
113
|
+
* pulling the session and storage stacks into this module's graph would
|
|
114
|
+
* load both for every query that never names one.
|
|
115
|
+
*
|
|
116
|
+
* The reason travels in the result rather than on the error stack, which
|
|
117
|
+
* keeps this module free of any runtime import — so the renderer beside it
|
|
118
|
+
* can be tested without loading the storage stack to do it.
|
|
119
|
+
*
|
|
120
|
+
* Four things are checked before anything is written, because a chooser
|
|
121
|
+
* can hand this a place that cannot be used:
|
|
122
|
+
*
|
|
123
|
+
* 1. A provider path is refused BY NAME. `/net/pacs/x.csv` fails inside
|
|
124
|
+
* CUBE with a message about an upload, which says nothing about why.
|
|
125
|
+
* 2. A missing parent is created, and the result says so — an operator
|
|
126
|
+
* reads what happened rather than discovering a folder later.
|
|
127
|
+
* 3. An existing file is never overwritten in silence: an audit table
|
|
128
|
+
* quietly replacing another audit table is the workaround the
|
|
129
|
+
* principles forbid, so it takes `--force` to say it twice.
|
|
130
|
+
* 4. The path that was actually written is returned, so a surface can
|
|
131
|
+
* state it rather than repeat what the operator typed.
|
|
132
|
+
*
|
|
133
|
+
* @param csv - The rendered table.
|
|
134
|
+
* @param destination - Where the operator said to put it.
|
|
135
|
+
* @param force - True when the operator asked to overwrite.
|
|
136
|
+
* @returns The resolved path it landed on, or why it did not.
|
|
137
|
+
*/
|
|
138
|
+
export async function csvFile_write(csv, destination, force = false) {
|
|
139
|
+
try {
|
|
140
|
+
return await csvFile_put(csv, destination, force);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
// CUBE's failures arrive as thrown axios errors, and a throw out of a
|
|
144
|
+
// builtin does not stop at the command: under a daemon it takes the
|
|
145
|
+
// process, and every surface attached to it, with it. Measured — a
|
|
146
|
+
// re-upload over an existing path answers 500 and threw straight
|
|
147
|
+
// through this call. A store's bad day is a refusal, not an outage.
|
|
148
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
149
|
+
return { ok: false, message: `${destination} could not be written: ${why}` };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The write itself, once the guard above owns the failures.
|
|
154
|
+
*
|
|
155
|
+
* @param csv - The rendered table.
|
|
156
|
+
* @param destination - Where the operator said to put it.
|
|
157
|
+
* @param force - True when the operator asked to replace what is there.
|
|
158
|
+
* @returns The resolved path it landed on, or why it did not.
|
|
159
|
+
*/
|
|
160
|
+
async function csvFile_put(csv, destination, force) {
|
|
161
|
+
const { errorStack } = await import('@fnndsc/cumin');
|
|
162
|
+
const { path_resolve, error_stripDebugPrefix } = await import('../utils.js');
|
|
163
|
+
const { files_create, files_delete, files_listAll, files_mkdir, files_path_isDirectory } = await import('@fnndsc/salsa');
|
|
164
|
+
const resolved = await path_resolve(destination);
|
|
165
|
+
if (path_isVirtual(resolved)) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
message: `${resolved} is a provider, not storage: nothing can be written there`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const cut = resolved.lastIndexOf('/');
|
|
172
|
+
const parent = cut <= 0 ? '/' : resolved.slice(0, cut);
|
|
173
|
+
const name = resolved.slice(cut + 1);
|
|
174
|
+
if (name === '')
|
|
175
|
+
return { ok: false, message: `${resolved} names a folder, not a file` };
|
|
176
|
+
/**
|
|
177
|
+
* Whether the folder already holds a file of that name.
|
|
178
|
+
*
|
|
179
|
+
* A null listing means the folder holds no files — `files_listAll`
|
|
180
|
+
* answers null both for an empty folder and for one that is not there,
|
|
181
|
+
* so existence is asked of `files_path_isDirectory` instead and never
|
|
182
|
+
* inferred from this.
|
|
183
|
+
*/
|
|
184
|
+
const taken_find = async () => {
|
|
185
|
+
const listed = await files_listAll({ limit: 1000, offset: 0 }, 'files', parent);
|
|
186
|
+
const rows = listed?.tableData ?? [];
|
|
187
|
+
return rows.find((row) => {
|
|
188
|
+
const candidate = row.path ?? row.fname ?? '';
|
|
189
|
+
return candidate === resolved || candidate.slice(candidate.lastIndexOf('/') + 1) === name;
|
|
190
|
+
}) ?? null;
|
|
191
|
+
};
|
|
192
|
+
let created;
|
|
193
|
+
let replaced = false;
|
|
194
|
+
if (!await files_path_isDirectory(parent)) {
|
|
195
|
+
// The folder the operator named is not there. Making it is the ordinary
|
|
196
|
+
// thing to want, so it is made — and said, since a directory appearing
|
|
197
|
+
// without a word is the silent side effect this replaces.
|
|
198
|
+
if (!await files_mkdir(parent)) {
|
|
199
|
+
return { ok: false, message: `${parent} does not exist and could not be created` };
|
|
200
|
+
}
|
|
201
|
+
created = parent;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const standing = await taken_find();
|
|
205
|
+
if (standing !== null && !force) {
|
|
206
|
+
// An audit table quietly replacing another audit table is the silent
|
|
207
|
+
// workaround the principles forbid: it takes saying so twice.
|
|
208
|
+
return { ok: false, message: `${resolved} already exists; pass --force to overwrite it` };
|
|
209
|
+
}
|
|
210
|
+
if (standing !== null) {
|
|
211
|
+
// Removed, then written. CUBE answers a re-upload of a path it
|
|
212
|
+
// already holds with a 500 — measured — so "overwrite" has to be
|
|
213
|
+
// spelled out rather than left to the store, and the removal is
|
|
214
|
+
// reported because a file disappearing unremarked is the silent act
|
|
215
|
+
// this whole check exists to prevent.
|
|
216
|
+
const id = standing.id;
|
|
217
|
+
if (id === undefined || !await files_delete(id, 'files', parent)) {
|
|
218
|
+
return { ok: false, message: `${resolved} exists and could not be replaced` };
|
|
219
|
+
}
|
|
220
|
+
// Deletion in CUBE is asynchronous — salsa's folder delete documents
|
|
221
|
+
// the same thing — and writing into a path the store still holds
|
|
222
|
+
// answers 500. So the removal is waited out rather than assumed,
|
|
223
|
+
// which is what turned a replacement into a lost file and an error.
|
|
224
|
+
let gone = false;
|
|
225
|
+
for (let waited = 0; waited < GONE_TIMEOUT_MS; waited += GONE_POLL_MS) {
|
|
226
|
+
if (await taken_find() === null) {
|
|
227
|
+
gone = true;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
await new Promise((settle) => {
|
|
231
|
+
setTimeout(settle, GONE_POLL_MS);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (!gone) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
message: `${resolved} was removed but the store still holds it; nothing was written`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
replaced = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const written = await files_create(csv, resolved);
|
|
244
|
+
if (written) {
|
|
245
|
+
// Confirmed, not assumed. A create straight after making its folder has
|
|
246
|
+
// been seen to report success and leave nothing behind, and a table an
|
|
247
|
+
// operator believes they have is worse than one they know they lack.
|
|
248
|
+
if (await taken_find() === null) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
message: `${resolved} was reported written but is not there; try again`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
path: resolved,
|
|
257
|
+
...(created === undefined ? {} : { created }),
|
|
258
|
+
...(replaced ? { replaced: true } : {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const problem = errorStack.stack_pop();
|
|
262
|
+
// Stripped where it is read: a refusal an operator acts on should not
|
|
263
|
+
// arrive wearing the stack's debugging prefix.
|
|
264
|
+
const why = problem === undefined ? 'refused' : error_stripDebugPrefix(problem.message);
|
|
265
|
+
return { ok: false, message: `could not write ${resolved}: ${why}` };
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=query.csv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.csv.js","sourceRoot":"","sources":["../../../src/builtins/net/query.csv.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
11
|
import { PACSQueryDecodedResult, type CommandEnvelope } from '@fnndsc/cumin';
|
|
12
|
-
import { type PacsQueryModel } from '@fnndsc/menu';
|
|
12
|
+
import { type PacsProvenance, type PacsQueryModel } from '@fnndsc/menu';
|
|
13
13
|
/**
|
|
14
14
|
* Result of a successful query-create-and-wait operation.
|
|
15
15
|
*
|
|
@@ -65,7 +65,34 @@ export declare function pacsQueryModel_build(decoded: PACSQueryDecodedResult, fa
|
|
|
65
65
|
vfsPath: string;
|
|
66
66
|
pacsName: string;
|
|
67
67
|
expression: string;
|
|
68
|
+
/**
|
|
69
|
+
* Where this answer came from. Said once here so chell's sentence and
|
|
70
|
+
* argus's pill are the same fact rather than two guesses at it.
|
|
71
|
+
*/
|
|
72
|
+
provenance?: PacsProvenance;
|
|
68
73
|
}): PacsQueryModel;
|
|
74
|
+
/**
|
|
75
|
+
* Resolves whatever names a PACS server into its canonical identifier.
|
|
76
|
+
*
|
|
77
|
+
* The context may hold a numeric id, a `--pacsserver` may be either, and
|
|
78
|
+
* CUBE files a query under the identifier. The index is keyed on what CUBE
|
|
79
|
+
* stores, so both the write and the lookup have to speak that.
|
|
80
|
+
*
|
|
81
|
+
* @param pacsserver - An id or an identifier.
|
|
82
|
+
* @returns The identifier, or the input unchanged when it cannot be resolved.
|
|
83
|
+
*/
|
|
84
|
+
export declare function pacsIdentifier_resolve(pacsserver: string): Promise<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Says how long ago something was answered, in the coarsest true unit.
|
|
87
|
+
*
|
|
88
|
+
* A replay states its age because mise refuses to decide staleness on the
|
|
89
|
+
* operator's behalf: an accession names a study that will not change, an
|
|
90
|
+
* MRN can gain one tomorrow, and no heuristic tells those apart reliably.
|
|
91
|
+
*
|
|
92
|
+
* @param at - An ISO timestamp.
|
|
93
|
+
* @returns A phrase like `3 months ago`, or null when the stamp is unusable.
|
|
94
|
+
*/
|
|
95
|
+
export declare function age_describe(at: string): string | null;
|
|
69
96
|
/**
|
|
70
97
|
* Creates a PACS query, waits for results, displays findings, and prints the VFS path.
|
|
71
98
|
*
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A PACS question that names several patients is several questions.
|
|
3
|
+
*
|
|
4
|
+
* A PACS will not match a list. `PatientID:4356325\4433255` — the DICOM
|
|
5
|
+
* multi-value form — returns nothing from PACSDCM, and the standard agrees:
|
|
6
|
+
* *List of UID Matching* is defined only for attributes whose VR is `UI`,
|
|
7
|
+
* and `PatientID` is `LO`. So two hundred MRNs is two hundred C-FINDs. The
|
|
8
|
+
* fan-out is forced, not chosen.
|
|
9
|
+
*
|
|
10
|
+
* This module is the arithmetic of that: reading a cohort out of what the
|
|
11
|
+
* operator typed (or out of a file in CFS), turning multi-valued terms into
|
|
12
|
+
* the individual questions they stand for, and refusing by name when the
|
|
13
|
+
* cross-product would launch hundreds.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { type Result } from '@fnndsc/cumin';
|
|
18
|
+
/** The DICOM key a cohort is expressed in. */
|
|
19
|
+
export declare const PATIENT_KEY: string;
|
|
20
|
+
/**
|
|
21
|
+
* The most questions one inline expression may stand for.
|
|
22
|
+
*
|
|
23
|
+
* A handful of MRNs typed on a line is the case this serves; a real cohort
|
|
24
|
+
* belongs in `--patients`, which is why the refusal names that flag. The
|
|
25
|
+
* cross-product of several multi-valued terms grows fast enough that a typo
|
|
26
|
+
* could otherwise launch hundreds of C-FINDs at a shared clinical system.
|
|
27
|
+
*/
|
|
28
|
+
export declare const QUERY_INLINE_FANOUT_MAX: number;
|
|
29
|
+
/**
|
|
30
|
+
* The most questions `--patients` may stand for.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately generous — a two-hundred-MRN audit is the point of the flag —
|
|
33
|
+
* but not unbounded: a file of the wrong kind should be refused rather than
|
|
34
|
+
* walked.
|
|
35
|
+
*/
|
|
36
|
+
export declare const QUERY_COHORT_MAX: number;
|
|
37
|
+
/**
|
|
38
|
+
* Parses a query expression into terms, each of which may carry several
|
|
39
|
+
* values.
|
|
40
|
+
*
|
|
41
|
+
* The operator's own syntax, accepted as written:
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* PatientID:1234,4532,6654 one key, three values → three questions
|
|
45
|
+
* PatientID:1234,StudyDate:20240101 two keys, one value each → one question
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* There is no ambiguity to resolve: every genuine term carries a colon, so a
|
|
49
|
+
* bare segment can only be another value for the key before it.
|
|
50
|
+
*
|
|
51
|
+
* @param expr - The expression as typed.
|
|
52
|
+
* @returns Each key with its values in order, or null when the expression is
|
|
53
|
+
* malformed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function queryTerms_parse(expr: string): Record<string, string[]> | null;
|
|
56
|
+
/**
|
|
57
|
+
* Expands multi-valued terms into the individual questions they stand for.
|
|
58
|
+
*
|
|
59
|
+
* Several multi-valued keys fan out over their cross-product, in the order
|
|
60
|
+
* the keys were written, so the questions come out in the order an operator
|
|
61
|
+
* would have listed them.
|
|
62
|
+
*
|
|
63
|
+
* @param terms - Parsed terms.
|
|
64
|
+
* @returns One criteria record per question.
|
|
65
|
+
*/
|
|
66
|
+
export declare function queryTerms_expand(terms: Record<string, string[]>): Array<Record<string, string>>;
|
|
67
|
+
/**
|
|
68
|
+
* Reads a `--patients` value: MRNs listed inline, or `@` and a CFS path.
|
|
69
|
+
*
|
|
70
|
+
* The file is read through the session's own CFS path, never the engine's
|
|
71
|
+
* host filesystem, so the flag behaves identically from a local shell, a
|
|
72
|
+
* remote shell and a browser — and a list sitting on somebody's own machine
|
|
73
|
+
* reaches it through `upload`, which is the gated door, rather than through
|
|
74
|
+
* a second ungated one.
|
|
75
|
+
*
|
|
76
|
+
* One MRN per line; blank lines and `#` comments are ignored. An inline
|
|
77
|
+
* value is comma-separated, which is what a handful of MRNs looks like.
|
|
78
|
+
*
|
|
79
|
+
* @param value - The flag's value.
|
|
80
|
+
* @returns The MRNs in the order given, or a failure with the reason on the
|
|
81
|
+
* error stack.
|
|
82
|
+
*/
|
|
83
|
+
export declare function patients_read(value: string): Promise<Result<string[]>>;
|
|
84
|
+
/**
|
|
85
|
+
* Guards a fan-out's size, refusing by name rather than launching it.
|
|
86
|
+
*
|
|
87
|
+
* @param count - How many questions the expansion came to.
|
|
88
|
+
* @param limit - The ceiling that applies to how they were asked.
|
|
89
|
+
* @returns True when the fan-out may proceed; otherwise the reason is on the
|
|
90
|
+
* error stack.
|
|
91
|
+
*/
|
|
92
|
+
export declare function fanout_permit(count: number, limit: number): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A PACS question that names several patients is several questions.
|
|
3
|
+
*
|
|
4
|
+
* A PACS will not match a list. `PatientID:4356325\4433255` — the DICOM
|
|
5
|
+
* multi-value form — returns nothing from PACSDCM, and the standard agrees:
|
|
6
|
+
* *List of UID Matching* is defined only for attributes whose VR is `UI`,
|
|
7
|
+
* and `PatientID` is `LO`. So two hundred MRNs is two hundred C-FINDs. The
|
|
8
|
+
* fan-out is forced, not chosen.
|
|
9
|
+
*
|
|
10
|
+
* This module is the arithmetic of that: reading a cohort out of what the
|
|
11
|
+
* operator typed (or out of a file in CFS), turning multi-valued terms into
|
|
12
|
+
* the individual questions they stand for, and refusing by name when the
|
|
13
|
+
* cross-product would launch hundreds.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { Err, Ok, errorStack } from '@fnndsc/cumin';
|
|
18
|
+
/** The DICOM key a cohort is expressed in. */
|
|
19
|
+
export const PATIENT_KEY = 'PatientID';
|
|
20
|
+
/**
|
|
21
|
+
* The most questions one inline expression may stand for.
|
|
22
|
+
*
|
|
23
|
+
* A handful of MRNs typed on a line is the case this serves; a real cohort
|
|
24
|
+
* belongs in `--patients`, which is why the refusal names that flag. The
|
|
25
|
+
* cross-product of several multi-valued terms grows fast enough that a typo
|
|
26
|
+
* could otherwise launch hundreds of C-FINDs at a shared clinical system.
|
|
27
|
+
*/
|
|
28
|
+
export const QUERY_INLINE_FANOUT_MAX = 32;
|
|
29
|
+
/**
|
|
30
|
+
* The most questions `--patients` may stand for.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately generous — a two-hundred-MRN audit is the point of the flag —
|
|
33
|
+
* but not unbounded: a file of the wrong kind should be refused rather than
|
|
34
|
+
* walked.
|
|
35
|
+
*/
|
|
36
|
+
export const QUERY_COHORT_MAX = 500;
|
|
37
|
+
/** Marks a `--patients` value as naming a file rather than listing MRNs. */
|
|
38
|
+
const FILE_SIGIL = '@';
|
|
39
|
+
/**
|
|
40
|
+
* Parses a query expression into terms, each of which may carry several
|
|
41
|
+
* values.
|
|
42
|
+
*
|
|
43
|
+
* The operator's own syntax, accepted as written:
|
|
44
|
+
*
|
|
45
|
+
* ```
|
|
46
|
+
* PatientID:1234,4532,6654 one key, three values → three questions
|
|
47
|
+
* PatientID:1234,StudyDate:20240101 two keys, one value each → one question
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* There is no ambiguity to resolve: every genuine term carries a colon, so a
|
|
51
|
+
* bare segment can only be another value for the key before it.
|
|
52
|
+
*
|
|
53
|
+
* @param expr - The expression as typed.
|
|
54
|
+
* @returns Each key with its values in order, or null when the expression is
|
|
55
|
+
* malformed.
|
|
56
|
+
*/
|
|
57
|
+
export function queryTerms_parse(expr) {
|
|
58
|
+
const terms = {};
|
|
59
|
+
let current = null;
|
|
60
|
+
for (const raw of expr.split(',')) {
|
|
61
|
+
const part = raw.trim();
|
|
62
|
+
if (part === '')
|
|
63
|
+
return null;
|
|
64
|
+
const colon = part.indexOf(':');
|
|
65
|
+
if (colon < 1) {
|
|
66
|
+
// A bare segment before any key names nothing.
|
|
67
|
+
if (current === null)
|
|
68
|
+
return null;
|
|
69
|
+
terms[current].push(part);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const key = part.slice(0, colon).trim();
|
|
73
|
+
const value = part.slice(colon + 1).trim();
|
|
74
|
+
if (key === '' || value === '')
|
|
75
|
+
return null;
|
|
76
|
+
current = key;
|
|
77
|
+
terms[key] = [...(terms[key] ?? []), value];
|
|
78
|
+
}
|
|
79
|
+
return Object.keys(terms).length > 0 ? terms : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Expands multi-valued terms into the individual questions they stand for.
|
|
83
|
+
*
|
|
84
|
+
* Several multi-valued keys fan out over their cross-product, in the order
|
|
85
|
+
* the keys were written, so the questions come out in the order an operator
|
|
86
|
+
* would have listed them.
|
|
87
|
+
*
|
|
88
|
+
* @param terms - Parsed terms.
|
|
89
|
+
* @returns One criteria record per question.
|
|
90
|
+
*/
|
|
91
|
+
export function queryTerms_expand(terms) {
|
|
92
|
+
let expanded = [{}];
|
|
93
|
+
for (const [key, values] of Object.entries(terms)) {
|
|
94
|
+
const grown = [];
|
|
95
|
+
for (const partial of expanded) {
|
|
96
|
+
for (const value of values)
|
|
97
|
+
grown.push({ ...partial, [key]: value });
|
|
98
|
+
}
|
|
99
|
+
expanded = grown;
|
|
100
|
+
}
|
|
101
|
+
return expanded;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Reads a `--patients` value: MRNs listed inline, or `@` and a CFS path.
|
|
105
|
+
*
|
|
106
|
+
* The file is read through the session's own CFS path, never the engine's
|
|
107
|
+
* host filesystem, so the flag behaves identically from a local shell, a
|
|
108
|
+
* remote shell and a browser — and a list sitting on somebody's own machine
|
|
109
|
+
* reaches it through `upload`, which is the gated door, rather than through
|
|
110
|
+
* a second ungated one.
|
|
111
|
+
*
|
|
112
|
+
* One MRN per line; blank lines and `#` comments are ignored. An inline
|
|
113
|
+
* value is comma-separated, which is what a handful of MRNs looks like.
|
|
114
|
+
*
|
|
115
|
+
* @param value - The flag's value.
|
|
116
|
+
* @returns The MRNs in the order given, or a failure with the reason on the
|
|
117
|
+
* error stack.
|
|
118
|
+
*/
|
|
119
|
+
export async function patients_read(value) {
|
|
120
|
+
if (!value.startsWith(FILE_SIGIL)) {
|
|
121
|
+
const listed = value.split(',')
|
|
122
|
+
.map((entry) => entry.trim())
|
|
123
|
+
.filter((entry) => entry !== '');
|
|
124
|
+
if (listed.length === 0) {
|
|
125
|
+
errorStack.stack_push('error', 'query: --patients names no patients.');
|
|
126
|
+
return Err();
|
|
127
|
+
}
|
|
128
|
+
return Ok(listed);
|
|
129
|
+
}
|
|
130
|
+
const named = value.slice(FILE_SIGIL.length);
|
|
131
|
+
if (named === '') {
|
|
132
|
+
errorStack.stack_push('error', 'query: --patients @ names no file.');
|
|
133
|
+
return Err();
|
|
134
|
+
}
|
|
135
|
+
// Imported where they are used: a cohort file is a rare path, and
|
|
136
|
+
// pulling the session and storage stacks into this module's graph would
|
|
137
|
+
// load both for every query that never names one.
|
|
138
|
+
const { path_resolve } = await import('../utils.js');
|
|
139
|
+
const { fileContent_get } = await import('@fnndsc/salsa');
|
|
140
|
+
const resolved = await path_resolve(named);
|
|
141
|
+
const content = await fileContent_get(resolved);
|
|
142
|
+
if (!content.ok) {
|
|
143
|
+
const problem = errorStack.stack_pop();
|
|
144
|
+
errorStack.stack_push('error', `query: cannot read ${resolved}: ${problem?.message ?? 'unreadable'}`);
|
|
145
|
+
return Err();
|
|
146
|
+
}
|
|
147
|
+
const mrns = content.value
|
|
148
|
+
.split('\n')
|
|
149
|
+
.map((line) => line.trim())
|
|
150
|
+
.filter((line) => line !== '' && !line.startsWith('#'));
|
|
151
|
+
if (mrns.length === 0) {
|
|
152
|
+
errorStack.stack_push('error', `query: ${resolved} lists no patients.`);
|
|
153
|
+
return Err();
|
|
154
|
+
}
|
|
155
|
+
return Ok(mrns);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Guards a fan-out's size, refusing by name rather than launching it.
|
|
159
|
+
*
|
|
160
|
+
* @param count - How many questions the expansion came to.
|
|
161
|
+
* @param limit - The ceiling that applies to how they were asked.
|
|
162
|
+
* @returns True when the fan-out may proceed; otherwise the reason is on the
|
|
163
|
+
* error stack.
|
|
164
|
+
*/
|
|
165
|
+
export function fanout_permit(count, limit) {
|
|
166
|
+
if (count <= limit)
|
|
167
|
+
return true;
|
|
168
|
+
errorStack.stack_push('error', `query: refusing: ${count} queries; use --patients with a file, or narrow the terms.`);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=query.fanout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.fanout.js","sourceRoot":"","sources":["../../../src/builtins/net/query.fanout.ts"],"names":[],"mappings":"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"}
|