@sjcrh/proteinpaint-server 2.214.0 → 2.216.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/package.json +8 -8
- package/routes/profile.impressionDistribution.js +14 -12
- package/src/app.js +2435 -3001
- package/src/serverconfig.js +30 -1
- package/routes/brainImaging.js +0 -154
- package/routes/brainImagingSamples.js +0 -71
package/src/serverconfig.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from 'fs'
|
|
7
|
+
import os from 'os'
|
|
7
8
|
import path from 'path'
|
|
8
9
|
import { fileURLToPath } from 'url'
|
|
9
10
|
|
|
@@ -202,6 +203,34 @@ if (serverconfig.debugmode && !serverconfig.binpath.includes('sjcrh/')) {
|
|
|
202
203
|
serverconfig.routeSetters = routeSetters
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
if (process.env.PP_CREDS_HANDOFF_FILE) {
|
|
207
|
+
// set by container/envHelpers.mjs, which passes {<NAME>_CREDS: value} as JSON in a private temp file instead
|
|
208
|
+
// of in this process env, so that the credentials are not in the initial env of this process
|
|
209
|
+
const file = process.env.PP_CREDS_HANDOFF_FILE
|
|
210
|
+
delete process.env.PP_CREDS_HANDOFF_FILE
|
|
211
|
+
const dir = path.dirname(file)
|
|
212
|
+
// only remove a dir as created by envHelpers.mjs
|
|
213
|
+
if (
|
|
214
|
+
path.basename(file) != 'creds.json' ||
|
|
215
|
+
// resolved, like the dir from path.join() in envHelpers.mjs, such as for TMPDIR=/tmp/.
|
|
216
|
+
path.dirname(dir) != path.resolve(os.tmpdir()) ||
|
|
217
|
+
!path.basename(dir).startsWith('pp-creds-')
|
|
218
|
+
)
|
|
219
|
+
throw `invalid process.env.PP_CREDS_HANDOFF_FILE`
|
|
220
|
+
let creds
|
|
221
|
+
try {
|
|
222
|
+
creds = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
223
|
+
} catch {
|
|
224
|
+
// do not include the parse error message, since it may quote part of the credentials
|
|
225
|
+
throw `unable to read credentials from process.env.PP_CREDS_HANDOFF_FILE`
|
|
226
|
+
} finally {
|
|
227
|
+
// before the server starts listening, so that a request cannot read this file
|
|
228
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
229
|
+
}
|
|
230
|
+
// set at runtime instead of in the initial env; each <NAME>_CREDS is then read and deleted as before
|
|
231
|
+
for (const [name, value] of Object.entries(creds)) process.env[name] = value
|
|
232
|
+
}
|
|
233
|
+
|
|
205
234
|
if (process.env.PP_CREDS) {
|
|
206
235
|
// preferred over a dsCredentials file path, so that the server process does not need
|
|
207
236
|
// read access to a credentials file that a path traversal bug could expose;
|
|
@@ -413,7 +442,7 @@ export default serverconfig
|
|
|
413
442
|
the top-level serverconfig{} already has a null prototype, as set above right after parsing
|
|
414
443
|
|
|
415
444
|
objects are locked in place instead of copied, so that a reference that a module captured at import time,
|
|
416
|
-
e.g. `const
|
|
445
|
+
e.g. `const opts = serverconfig.features.cacheMonitor`, is locked too
|
|
417
446
|
|
|
418
447
|
throws, to fail the launch, when a value cannot be fully locked
|
|
419
448
|
*/
|
package/routes/brainImaging.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import serverconfig from "#src/serverconfig.js";
|
|
4
|
-
import { getData } from "../src/termdb.matrix.js";
|
|
5
|
-
import { filterSampleNamesByAccess } from "#src/termdb.sql.js";
|
|
6
|
-
import { isNumericTerm } from "#shared";
|
|
7
|
-
import { getColors } from "#shared";
|
|
8
|
-
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
9
|
-
function init({ genomes }) {
|
|
10
|
-
return async (req, res) => {
|
|
11
|
-
try {
|
|
12
|
-
const query = req.query;
|
|
13
|
-
const g = genomes[query.genome];
|
|
14
|
-
if (!g) throw "invalid genome name";
|
|
15
|
-
const ds = g.datasets[query.dslabel];
|
|
16
|
-
if (!ds) throw "invalid dataset name";
|
|
17
|
-
let plane, index;
|
|
18
|
-
if (query.l != void 0) {
|
|
19
|
-
plane = "L";
|
|
20
|
-
index = query.l;
|
|
21
|
-
} else if (query.f != void 0) {
|
|
22
|
-
plane = "F";
|
|
23
|
-
index = query.f;
|
|
24
|
-
} else {
|
|
25
|
-
plane = "T";
|
|
26
|
-
index = query.t;
|
|
27
|
-
}
|
|
28
|
-
const [brainImage, legend] = await getBrainImage(query, genomes, plane, index);
|
|
29
|
-
res.send({ brainImage, plane, legend });
|
|
30
|
-
} catch (e) {
|
|
31
|
-
console.log(e);
|
|
32
|
-
res.status(404).send(typeof e == "string" ? e : "Sample brain image not found");
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
async function getBrainImage(query, genomes, plane, index) {
|
|
37
|
-
const ds = genomes[query.genome].datasets[query.dslabel];
|
|
38
|
-
const q = ds.queries.NIdata;
|
|
39
|
-
if (q.checkDataAccess) {
|
|
40
|
-
if (!q.checkDataAccess(query)) throw "no access";
|
|
41
|
-
}
|
|
42
|
-
const ref = q.references[query.refKey];
|
|
43
|
-
if (!ref) throw "invalid refKey";
|
|
44
|
-
if (ref.referenceFile && ref.samples) {
|
|
45
|
-
const refFile = path.join(serverconfig.tpmasterdir, ref.referenceFile);
|
|
46
|
-
const dirPath = path.join(serverconfig.tpmasterdir, ref.samples);
|
|
47
|
-
const terms = [];
|
|
48
|
-
const divideByTW = query.divideByTW;
|
|
49
|
-
const overlayTW = query.overlayTW;
|
|
50
|
-
if (divideByTW) terms.push(divideByTW);
|
|
51
|
-
if (overlayTW) terms.push(overlayTW);
|
|
52
|
-
let selectedSampleNames = query.selectedSampleFileNames.map((s) => s.split(".nii")[0]);
|
|
53
|
-
const existingFiles = new Set(await fs.promises.readdir(dirPath));
|
|
54
|
-
selectedSampleNames = selectedSampleNames.filter((s) => existingFiles.has(s + ".nii"));
|
|
55
|
-
if (!selectedSampleNames.length) throw "no brain imaging data for the requested sample(s)";
|
|
56
|
-
selectedSampleNames = await filterSampleNamesByAccess(query, ds, selectedSampleNames);
|
|
57
|
-
if (!selectedSampleNames.length) throw "no selected samples pass the current filter";
|
|
58
|
-
const data = await getData({ terms, __protected__: query.__protected__ }, ds);
|
|
59
|
-
if (data.error) throw data.error;
|
|
60
|
-
const divideByCat = {};
|
|
61
|
-
const uniqueOverlayTwCats = /* @__PURE__ */ new Set();
|
|
62
|
-
const getCategories = (tw, value) => {
|
|
63
|
-
const keys = value.values ? value.values.map((v) => v.key) : [value.key];
|
|
64
|
-
return keys.map((k) => tw.term.values?.[k]?.label || k);
|
|
65
|
-
};
|
|
66
|
-
for (const sampleName of selectedSampleNames) {
|
|
67
|
-
const sampleId = ds.sampleName2Id.get(sampleName);
|
|
68
|
-
const sampleData = data.samples[sampleId];
|
|
69
|
-
const samplePath = path.join(dirPath, sampleName) + ".nii";
|
|
70
|
-
let divideCategories = ["default"];
|
|
71
|
-
let overlayCategory = "default";
|
|
72
|
-
if (divideByTW && sampleData) {
|
|
73
|
-
const value = sampleData[divideByTW.$id];
|
|
74
|
-
if (value) divideCategories = getCategories(divideByTW, value);
|
|
75
|
-
}
|
|
76
|
-
if (overlayTW && sampleData) {
|
|
77
|
-
const value = sampleData[overlayTW.$id];
|
|
78
|
-
if (value) {
|
|
79
|
-
overlayCategory = getCategories(overlayTW, value).join(", ");
|
|
80
|
-
uniqueOverlayTwCats.add(overlayCategory);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
for (const divideCategory of divideCategories) {
|
|
84
|
-
if (!divideByCat[divideCategory]) divideByCat[divideCategory] = {};
|
|
85
|
-
if (!query.legendFilter?.includes(overlayCategory)) {
|
|
86
|
-
if (!divideByCat[divideCategory][overlayCategory]) {
|
|
87
|
-
let color = overlayTW?.term?.values?.[overlayCategory]?.color;
|
|
88
|
-
if (overlayTW && isNumericTerm(overlayTW.term)) {
|
|
89
|
-
const bins = data.refs.byTermId[overlayTW.$id].bins;
|
|
90
|
-
color = bins.find((b) => b.label == overlayCategory).color;
|
|
91
|
-
}
|
|
92
|
-
divideByCat[divideCategory][overlayCategory] = {
|
|
93
|
-
samples: [],
|
|
94
|
-
color
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
divideByCat[divideCategory][overlayCategory].samples.push(samplePath);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const k2c = getColors(uniqueOverlayTwCats.size);
|
|
102
|
-
const lengths = [];
|
|
103
|
-
for (const dcategory in divideByCat) {
|
|
104
|
-
for (const category in divideByCat[dcategory]) {
|
|
105
|
-
const overlayCat = divideByCat[dcategory][category];
|
|
106
|
-
const samples = overlayCat.samples;
|
|
107
|
-
lengths.push(samples.length);
|
|
108
|
-
if (!overlayCat.color) overlayCat.color = category == "default" ? "red" : k2c(category);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const maxLength = Math.max(...lengths);
|
|
112
|
-
const brainImageDict = {};
|
|
113
|
-
const legend = {};
|
|
114
|
-
for (const dcategory in divideByCat) {
|
|
115
|
-
let catNum = 0;
|
|
116
|
-
const filesByCat = divideByCat[dcategory];
|
|
117
|
-
for (const category in filesByCat) {
|
|
118
|
-
if (filesByCat[category].samples.length < 1) continue;
|
|
119
|
-
catNum += filesByCat[category].samples.length;
|
|
120
|
-
if (!legend[category]) legend[category] = { color: filesByCat[category].color, maxLength };
|
|
121
|
-
}
|
|
122
|
-
const arg = {
|
|
123
|
-
refFile,
|
|
124
|
-
plane,
|
|
125
|
-
index,
|
|
126
|
-
maxLength,
|
|
127
|
-
filesByCat: JSON.stringify(filesByCat)
|
|
128
|
-
};
|
|
129
|
-
let url;
|
|
130
|
-
try {
|
|
131
|
-
url = await run_python("plotBrainImaging.py", JSON.stringify(arg));
|
|
132
|
-
} catch (error) {
|
|
133
|
-
const errmsg = "Error running Python script:" + error;
|
|
134
|
-
throw new Error(errmsg);
|
|
135
|
-
}
|
|
136
|
-
brainImageDict[dcategory] = { url, catNum };
|
|
137
|
-
}
|
|
138
|
-
if (query.legendFilter) {
|
|
139
|
-
for (const cat of query.legendFilter) {
|
|
140
|
-
legend[cat] = {
|
|
141
|
-
color: "white",
|
|
142
|
-
maxLength,
|
|
143
|
-
crossedOut: true
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return [brainImageDict, legend];
|
|
148
|
-
} else {
|
|
149
|
-
throw "no reference or sample files";
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
export {
|
|
153
|
-
init
|
|
154
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import serverconfig from "#src/serverconfig.js";
|
|
4
|
-
import { getData } from "#src/termdb.matrix.js";
|
|
5
|
-
import { filterSampleNamesByAccess } from "#src/termdb.sql.js";
|
|
6
|
-
function init({ genomes }) {
|
|
7
|
-
return async (req, res) => {
|
|
8
|
-
try {
|
|
9
|
-
const query = req.query;
|
|
10
|
-
const g = genomes[query.genome];
|
|
11
|
-
if (!g) throw "invalid genome name";
|
|
12
|
-
const ds = g.datasets[query.dslabel];
|
|
13
|
-
if (!ds) throw "invalid dataset name";
|
|
14
|
-
const samples = await getBrainImageSamples(query, genomes);
|
|
15
|
-
res.send({ samples });
|
|
16
|
-
} catch (e) {
|
|
17
|
-
console.log(e);
|
|
18
|
-
res.status(404).send(typeof e == "string" ? e : "Cannot get brain imaging samples");
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
async function getBrainImageSamples(query, genomes) {
|
|
23
|
-
const ds = genomes[query.genome].datasets[query.dslabel];
|
|
24
|
-
const q = ds.queries.NIdata;
|
|
25
|
-
if (q.checkDataAccess) {
|
|
26
|
-
if (!q.checkDataAccess(query)) throw "no access";
|
|
27
|
-
}
|
|
28
|
-
const ref = q.references[query.refKey];
|
|
29
|
-
if (!ref) throw "invalid refKey";
|
|
30
|
-
if (ref.referenceFile && ref.samples) {
|
|
31
|
-
const dirPath = path.join(serverconfig.tpmasterdir, ref.samples);
|
|
32
|
-
const files = (await fs.promises.readdir(dirPath, { withFileTypes: true })).filter((f) => f.isFile() && f.name.endsWith(".nii")).map((f) => f.name);
|
|
33
|
-
let sampleNames = files.map((name) => name.split(".nii")[0]);
|
|
34
|
-
sampleNames = await filterSampleNamesByAccess(query, ds, sampleNames);
|
|
35
|
-
if (query.samplesOnly) return sampleNames.map((name) => ({ sample: name }));
|
|
36
|
-
if (ref.sampleColumns) {
|
|
37
|
-
const terms = ref.sampleColumns.map((term) => {
|
|
38
|
-
const termjson = ds.cohort.termdb.q.termjsonByOneid(term.termid);
|
|
39
|
-
return {
|
|
40
|
-
$id: term.termid,
|
|
41
|
-
term: termjson,
|
|
42
|
-
q: termjson?.type == "float" || termjson?.type == "integer" ? { mode: "continuous" } : {}
|
|
43
|
-
};
|
|
44
|
-
});
|
|
45
|
-
const data = await getData({ terms, __protected__: query.__protected__ }, ds);
|
|
46
|
-
if (data.error) throw data.error;
|
|
47
|
-
const samples = {};
|
|
48
|
-
for (const s of sampleNames) {
|
|
49
|
-
const annoForOneS = { sample: s };
|
|
50
|
-
const sid = ds.cohort.termdb.q.sampleName2id(s);
|
|
51
|
-
const sampleData = data.samples?.[sid];
|
|
52
|
-
if (sampleData) {
|
|
53
|
-
for (const term of ref.sampleColumns) {
|
|
54
|
-
const v = sampleData[term.termid];
|
|
55
|
-
if (!v) continue;
|
|
56
|
-
if (v.values) annoForOneS[term.termid] = v.values.map((x) => x.key).join(", ");
|
|
57
|
-
else if (v.value !== void 0) annoForOneS[term.termid] = v.value;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
samples[s] = annoForOneS;
|
|
61
|
-
}
|
|
62
|
-
return Object.values(samples);
|
|
63
|
-
}
|
|
64
|
-
return sampleNames.map((name) => ({ sample: name }));
|
|
65
|
-
} else {
|
|
66
|
-
throw "no reference or sample files";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
export {
|
|
70
|
-
init
|
|
71
|
-
};
|