@sjcrh/proteinpaint-server 2.118.3-1 → 2.119.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 +5 -5
- package/routes/samplewsimages.js +10 -2
- package/routes/termdb.DE.js +7 -2
- package/routes/termdb.cluster.js +98 -4
- package/routes/termdb.singlecellDEgenes.js +6 -0
- package/routes/tileserver.js +3 -3
- package/routes/wsimages.js +45 -2
- package/src/app.js +232 -22
- package/utils/edge.R +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.119.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "a genomics visualization tool for exploring a cohort's genotype and phenotype data",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -65,11 +65,11 @@
|
|
|
65
65
|
"typescript": "^5.6.3"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@sjcrh/augen": "2.
|
|
68
|
+
"@sjcrh/augen": "2.119.0",
|
|
69
69
|
"@sjcrh/proteinpaint-python": "2.118.0",
|
|
70
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
71
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
72
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
70
|
+
"@sjcrh/proteinpaint-rust": "2.119.0",
|
|
71
|
+
"@sjcrh/proteinpaint-shared": "2.119.0",
|
|
72
|
+
"@sjcrh/proteinpaint-types": "2.119.0",
|
|
73
73
|
"@types/express": "^5.0.0",
|
|
74
74
|
"@types/express-session": "^1.18.1",
|
|
75
75
|
"better-sqlite3": "^9.4.1",
|
package/routes/samplewsimages.js
CHANGED
|
@@ -23,8 +23,16 @@ function init({ genomes }) {
|
|
|
23
23
|
if (!ds)
|
|
24
24
|
throw "invalid dataset name";
|
|
25
25
|
const sampleId = query.sample_id;
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const wsimages = await ds.queries.WSImages.getWSImages(sampleId);
|
|
27
|
+
if (ds.queries.WSImages.getWSIAnnotations) {
|
|
28
|
+
for (const wsimage of wsimages) {
|
|
29
|
+
const annotations = await ds.queries.WSImages.getWSIAnnotations(sampleId, wsimage.filename);
|
|
30
|
+
if (annotations) {
|
|
31
|
+
wsimage.overlays = annotations;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
res.send({ sampleWSImages: wsimages });
|
|
28
36
|
} catch (e) {
|
|
29
37
|
console.log(e);
|
|
30
38
|
res.status(404).send("Sample images not found");
|
package/routes/termdb.DE.js
CHANGED
|
@@ -9,6 +9,7 @@ import { mayLog } from "#src/helpers.ts";
|
|
|
9
9
|
import serverconfig from "../src/serverconfig.js";
|
|
10
10
|
import imagesize from "image-size";
|
|
11
11
|
import { get_header_txt } from "#src/utils.js";
|
|
12
|
+
import { formatElapsedTime } from "@sjcrh/proteinpaint-shared/time.js";
|
|
12
13
|
const api = {
|
|
13
14
|
endpoint: "DEanalysis",
|
|
14
15
|
methods: {
|
|
@@ -186,6 +187,10 @@ async function run_DE(param, ds, term_results, term_results2) {
|
|
|
186
187
|
throw "sample size of group1 < 1";
|
|
187
188
|
if (sample_size2 < 1)
|
|
188
189
|
throw "sample size of group2 < 1";
|
|
190
|
+
const commonnames = group1names.filter((element) => group2names.includes(element));
|
|
191
|
+
if (commonnames.length > 0) {
|
|
192
|
+
throw "Common elements found between both groups:" + commonnames.map((i) => i).join(",");
|
|
193
|
+
}
|
|
189
194
|
const cases_string = group1names.map((i) => i).join(",");
|
|
190
195
|
const controls_string = group2names.map((i) => i).join(",");
|
|
191
196
|
const expression_input = {
|
|
@@ -221,7 +226,7 @@ async function run_DE(param, ds, term_results, term_results2) {
|
|
|
221
226
|
const result2 = JSON.parse(
|
|
222
227
|
await run_R(path.join(serverconfig.binpath, "utils", "edge.R"), JSON.stringify(expression_input))
|
|
223
228
|
);
|
|
224
|
-
mayLog("Time taken to run edgeR:", Date.now() - time12
|
|
229
|
+
mayLog("Time taken to run edgeR:", formatElapsedTime(Date.now() - time12));
|
|
225
230
|
param.method = "edgeR";
|
|
226
231
|
const ql_imagePath = path.join(serverconfig.cachedir, result2.edgeR_ql_image_name[0]);
|
|
227
232
|
mayLog("ql_imagePath:", ql_imagePath);
|
|
@@ -244,7 +249,7 @@ async function run_DE(param, ds, term_results, term_results2) {
|
|
|
244
249
|
}
|
|
245
250
|
const time1 = (/* @__PURE__ */ new Date()).valueOf();
|
|
246
251
|
const result = JSON.parse(await run_rust("DEanalysis", JSON.stringify(expression_input)));
|
|
247
|
-
mayLog("Time taken to run rust DE pipeline:", Date.now() - time1
|
|
252
|
+
mayLog("Time taken to run rust DE pipeline:", formatElapsedTime(Date.now() - time1));
|
|
248
253
|
param.method = "wilcoxon";
|
|
249
254
|
return { data: result, sample_size1, sample_size2, method: param.method };
|
|
250
255
|
}
|
package/routes/termdb.cluster.js
CHANGED
|
@@ -12,6 +12,7 @@ import { getData } from "#src/termdb.matrix.js";
|
|
|
12
12
|
import { termType2label } from "#shared/terms.js";
|
|
13
13
|
import { mayLog } from "#src/helpers.ts";
|
|
14
14
|
import { formatElapsedTime } from "#shared/time.js";
|
|
15
|
+
import { getResult as getResultGene } from "#src/gene.js";
|
|
15
16
|
const api = {
|
|
16
17
|
endpoint: "termdb/cluster",
|
|
17
18
|
methods: {
|
|
@@ -219,7 +220,7 @@ async function validate_query_geneExpression(ds, genome) {
|
|
|
219
220
|
return;
|
|
220
221
|
}
|
|
221
222
|
if (q.src == "native") {
|
|
222
|
-
await validateNative(q, ds);
|
|
223
|
+
await validateNative(q, ds, genome);
|
|
223
224
|
return;
|
|
224
225
|
}
|
|
225
226
|
throw "unknown queries.geneExpression.src";
|
|
@@ -255,7 +256,7 @@ async function queryGeneExpression(hdf5_file, geneNames) {
|
|
|
255
256
|
throw error;
|
|
256
257
|
}
|
|
257
258
|
}
|
|
258
|
-
async function validateNative(q, ds) {
|
|
259
|
+
async function validateNative(q, ds, genome) {
|
|
259
260
|
q.file = path.join(serverconfig.tpmasterdir, q.file);
|
|
260
261
|
q.samples = [];
|
|
261
262
|
await utils.file_is_readable(q.file);
|
|
@@ -271,9 +272,102 @@ async function validateNative(q, ds) {
|
|
|
271
272
|
throw "unknown sample from HDF5: " + sn;
|
|
272
273
|
q.samples.push(si);
|
|
273
274
|
}
|
|
274
|
-
console.log(
|
|
275
|
+
console.log(
|
|
276
|
+
`${ds.label}: geneExpression HDF5 file validated. Format: ${vr.format}, Samples:`,
|
|
277
|
+
vr.sampleNames.length
|
|
278
|
+
);
|
|
275
279
|
} catch (error) {
|
|
276
|
-
|
|
280
|
+
console.log(`${ds.label}: geneExpression HDF5 validation failed, falling back to tabix handling: ${error}`);
|
|
281
|
+
try {
|
|
282
|
+
q.samples = [];
|
|
283
|
+
await utils.validate_tabixfile(q.file);
|
|
284
|
+
q.nochr = await utils.tabix_is_nochr(q.file, null, genome);
|
|
285
|
+
const lines = await utils.get_header_tabix(q.file);
|
|
286
|
+
if (!lines[0])
|
|
287
|
+
throw "Header line missing from " + q.file;
|
|
288
|
+
const l = lines[0].split(" ");
|
|
289
|
+
if (l.slice(0, 4).join(" ") != "#chr start stop gene") {
|
|
290
|
+
throw "Header line has wrong content for columns 1-4";
|
|
291
|
+
}
|
|
292
|
+
for (let i = 4; i < l.length; i++) {
|
|
293
|
+
const id = ds.cohort.termdb.q.sampleName2id(l[i]);
|
|
294
|
+
if (id == void 0) {
|
|
295
|
+
throw "queries.geneExpression: unknown sample from header: " + l[i];
|
|
296
|
+
}
|
|
297
|
+
q.samples.push(id);
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
throw `${ds.label} geneExpression file cannot be validated as tabix file`;
|
|
301
|
+
}
|
|
302
|
+
console.log(`${ds.label}: Tabix file successfully initialized. Samples: ${q.samples.length}`);
|
|
303
|
+
q.get = async (param) => {
|
|
304
|
+
const limitSamples = await mayLimitSamples(param, q.samples, ds);
|
|
305
|
+
if (limitSamples?.size == 0) {
|
|
306
|
+
return { term2sample2value: /* @__PURE__ */ new Map(), byTermId: {}, bySampleId: {} };
|
|
307
|
+
}
|
|
308
|
+
const bySampleId = {};
|
|
309
|
+
const samples = q.samples || [];
|
|
310
|
+
if (limitSamples) {
|
|
311
|
+
for (const sid of limitSamples) {
|
|
312
|
+
bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
for (const sid of samples) {
|
|
316
|
+
bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const term2sample2value = /* @__PURE__ */ new Map();
|
|
320
|
+
const byTermId = {};
|
|
321
|
+
for (const geneTerm of param.terms) {
|
|
322
|
+
if (!geneTerm.gene)
|
|
323
|
+
continue;
|
|
324
|
+
if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop)) {
|
|
325
|
+
const re = getResultGene(genome, { input: geneTerm.gene, deep: 1 });
|
|
326
|
+
if (!re.gmlst || re.gmlst.length == 0) {
|
|
327
|
+
console.warn("Unknown gene:" + geneTerm.gene);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const i = re.gmlst.find((i2) => i2.isdefault) || re.gmlst[0];
|
|
331
|
+
geneTerm.start = i.start;
|
|
332
|
+
geneTerm.stop = i.stop;
|
|
333
|
+
geneTerm.chr = i.chr;
|
|
334
|
+
}
|
|
335
|
+
if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop)) {
|
|
336
|
+
throw "Missing chr/start/stop";
|
|
337
|
+
}
|
|
338
|
+
const s2v = {};
|
|
339
|
+
await utils.get_lines_bigfile({
|
|
340
|
+
args: [
|
|
341
|
+
q.file,
|
|
342
|
+
(q.nochr ? geneTerm.chr.replace("chr", "") : geneTerm.chr) + ":" + geneTerm.start + "-" + geneTerm.stop
|
|
343
|
+
],
|
|
344
|
+
callback: (line) => {
|
|
345
|
+
const l = line.split(" ");
|
|
346
|
+
if (l[3].toLowerCase() != geneTerm.gene.toLowerCase())
|
|
347
|
+
return;
|
|
348
|
+
for (let i = 4; i < l.length; i++) {
|
|
349
|
+
const sampleId = samples[i - 4];
|
|
350
|
+
if (limitSamples && !limitSamples.has(sampleId))
|
|
351
|
+
continue;
|
|
352
|
+
if (!l[i])
|
|
353
|
+
continue;
|
|
354
|
+
const v = Number(l[i]);
|
|
355
|
+
if (Number.isNaN(v))
|
|
356
|
+
throw "Expression value not number";
|
|
357
|
+
s2v[sampleId] = v;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
if (Object.keys(s2v).length) {
|
|
362
|
+
term2sample2value.set(geneTerm.gene, s2v);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (term2sample2value.size == 0) {
|
|
366
|
+
throw "No data available for the input " + param.terms?.map((g) => g.gene).join(", ");
|
|
367
|
+
}
|
|
368
|
+
return { term2sample2value, byTermId, bySampleId };
|
|
369
|
+
};
|
|
370
|
+
return;
|
|
277
371
|
}
|
|
278
372
|
q.get = async (param) => {
|
|
279
373
|
const limitSamples = await mayLimitSamples(param, q.samples, ds);
|
|
@@ -27,6 +27,12 @@ function init({ genomes }) {
|
|
|
27
27
|
if (!ds.queries?.singleCell?.DEgenes)
|
|
28
28
|
throw "not supported on this dataset";
|
|
29
29
|
result = await ds.queries.singleCell.DEgenes.get(q);
|
|
30
|
+
if (!result || !result.genes || !result?.genes?.length) {
|
|
31
|
+
result = {
|
|
32
|
+
status: 404,
|
|
33
|
+
error: !result ? "No data found." : "No differentially expressed genes found."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
30
36
|
} catch (e) {
|
|
31
37
|
if (e.stack)
|
|
32
38
|
console.log(e.stack);
|
package/routes/tileserver.js
CHANGED
|
@@ -6,7 +6,7 @@ import SessionManager from "#src/wsisessions/SessionManager.ts";
|
|
|
6
6
|
import serverconfig from "#src/serverconfig.js";
|
|
7
7
|
import path from "path";
|
|
8
8
|
const api = {
|
|
9
|
-
endpoint: `tileserver/layer
|
|
9
|
+
endpoint: `tileserver/layer/:type/:sessionId/zoomify/:TileGroup/:z-:x-:y@1x.jpg`,
|
|
10
10
|
methods: {
|
|
11
11
|
get: {
|
|
12
12
|
...tilePayload,
|
|
@@ -21,7 +21,7 @@ const api = {
|
|
|
21
21
|
function init({ genomes }) {
|
|
22
22
|
return async (req, res) => {
|
|
23
23
|
try {
|
|
24
|
-
const { sessionId, TileGroup, z, x, y } = req.params;
|
|
24
|
+
const { type, sessionId, TileGroup, z, x, y } = req.params;
|
|
25
25
|
const query = req.query;
|
|
26
26
|
const wsiImage = query.wsi_image;
|
|
27
27
|
if (!wsiImage)
|
|
@@ -49,7 +49,7 @@ function init({ genomes }) {
|
|
|
49
49
|
if (!tileServer) {
|
|
50
50
|
throw new Error("No tile server");
|
|
51
51
|
}
|
|
52
|
-
const url = `${tileServer.url}/tileserver/layer
|
|
52
|
+
const url = `${tileServer.url}/tileserver/layer/${type}/${sessionId}/zoomify/${TileGroup}/${z}-${x}-${y}@1x.jpg`;
|
|
53
53
|
const response = await ky.get(url, { timeout: 12e4 });
|
|
54
54
|
await SessionManager.getInstance().updateSession(wsiImagePath);
|
|
55
55
|
const buffer = await response.arrayBuffer();
|
package/routes/wsimages.js
CHANGED
|
@@ -45,7 +45,7 @@ function init({ genomes }) {
|
|
|
45
45
|
if (!mount)
|
|
46
46
|
throw new Error("No mount available for TileServer");
|
|
47
47
|
const wsiImagePath = path.join(`${mount}/${ds.queries.WSImages.imageBySampleFolder}/${sampleId}`, wsimage);
|
|
48
|
-
const sessionId = await getSessionId(cookieJar, getCookieString, setCookie, wsiImagePath);
|
|
48
|
+
const sessionId = await getSessionId(ds, sampleId, cookieJar, getCookieString, setCookie, wsiImagePath);
|
|
49
49
|
const getWsiImageResponse = await getWsiImageDimensions(sessionId, getCookieString, wsiImagePath);
|
|
50
50
|
const payload = {
|
|
51
51
|
status: "ok",
|
|
@@ -62,7 +62,7 @@ function init({ genomes }) {
|
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
async function getSessionId(cookieJar, getCookieString, setCookie, wsimage) {
|
|
65
|
+
async function getSessionId(ds, sampleId, cookieJar, getCookieString, setCookie, wsimage) {
|
|
66
66
|
const sessionManager = SessionManager.getInstance();
|
|
67
67
|
const invalidateResult = await sessionManager.syncAndInvalidateSessions(wsimage);
|
|
68
68
|
if (!invalidateResult)
|
|
@@ -93,6 +93,49 @@ async function getSessionId(cookieJar, getCookieString, setCookie, wsimage) {
|
|
|
93
93
|
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
94
94
|
});
|
|
95
95
|
await sessionManager.setSession(wsimage, sessionId, tileServer);
|
|
96
|
+
if (ds.queries.WSImages.getWSIAnnotations) {
|
|
97
|
+
const annotationFiles = await ds.queries.WSImages.getWSIAnnotations(sampleId, wsimage);
|
|
98
|
+
if (!annotationFiles)
|
|
99
|
+
throw new Error("No annotations files found");
|
|
100
|
+
const mount = serverconfig.features?.tileserver?.mount;
|
|
101
|
+
if (!mount)
|
|
102
|
+
throw new Error("No mount available for TileServer");
|
|
103
|
+
if (annotationFiles.length > 0) {
|
|
104
|
+
for (const annotationFile of annotationFiles) {
|
|
105
|
+
const annotationsFilePath = path.join(
|
|
106
|
+
`${mount}/${ds.queries.WSImages.imageBySampleFolder}/${sampleId}`,
|
|
107
|
+
annotationFile
|
|
108
|
+
);
|
|
109
|
+
const annotationsData = qs.stringify({
|
|
110
|
+
overlay_path: annotationsFilePath
|
|
111
|
+
});
|
|
112
|
+
await ky.put(`${tileServer.url}/tileserver/overlay`, {
|
|
113
|
+
body: annotationsData,
|
|
114
|
+
timeout: 5e4,
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
117
|
+
Cookie: `session_id=${sessionId}`
|
|
118
|
+
},
|
|
119
|
+
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const cmapData = qs.stringify({
|
|
123
|
+
cmap: JSON.stringify({
|
|
124
|
+
keys: ["annotation"],
|
|
125
|
+
values: [ds.queries.WSImages.annotationsColor]
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
await ky.put(`${tileServer.url}/tileserver/cmap`, {
|
|
129
|
+
body: cmapData,
|
|
130
|
+
timeout: 5e4,
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
133
|
+
Cookie: `session_id=${sessionId}`
|
|
134
|
+
},
|
|
135
|
+
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
96
139
|
return sessionId;
|
|
97
140
|
}
|
|
98
141
|
async function getWsiImageDimensions(sessionId, getCookieString, wsimage) {
|