@sjcrh/proteinpaint-server 2.215.0 → 2.216.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/package.json +7 -7
- package/routes/profile.impressionDistribution.js +14 -12
- package/src/app.js +2193 -1907
- package/src/serverconfig.js +36 -0
- 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;
|
|
@@ -361,6 +390,13 @@ if (!serverconfig.backend_only && fs.existsSync(publicDir)) serverconfig.publicD
|
|
|
361
390
|
const binDir = path.join(process.cwd(), './bin')
|
|
362
391
|
if (!serverconfig.backend_only) serverconfig.binDir = binDir
|
|
363
392
|
|
|
393
|
+
// The proteinpaint-front package's own public/index.html and public/cards, which its init copies into public/ when
|
|
394
|
+
// missing there. The server serves these paths from this dir when public/ does not have them, such as a public/ mount
|
|
395
|
+
// that the init cannot write to (see app.middlewares.js). Auto-computed from cwd like binDir, never operator-set.
|
|
396
|
+
const frontPublicDir = path.join(process.cwd(), 'node_modules/@sjcrh/proteinpaint-front/public')
|
|
397
|
+
delete serverconfig.frontPublicDir
|
|
398
|
+
if (!serverconfig.backend_only && fs.existsSync(frontPublicDir)) serverconfig.frontPublicDir = frontPublicDir
|
|
399
|
+
|
|
364
400
|
if (serverconfig.publicDir) {
|
|
365
401
|
const defaultTarget = path.join(serverconfig.binpath, 'cards')
|
|
366
402
|
if (!serverconfig.cards) {
|
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
|
-
};
|