@sjcrh/proteinpaint-server 2.203.1 → 2.205.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/dataset/termdb.test.js +17 -9
- package/package.json +8 -13
- package/routes/brainImaging.js +15 -7
- package/routes/brainImagingSamples.js +15 -9
- package/routes/termdb.diffMeth.js +61 -15
- package/routes/termdb.proteome.js +132 -253
- package/src/app.js +2061 -4197
package/dataset/termdb.test.js
CHANGED
|
@@ -412,8 +412,8 @@ function termdb_test_default() {
|
|
|
412
412
|
},
|
|
413
413
|
singleCell: {
|
|
414
414
|
samples: {
|
|
415
|
-
sampleColumns: [{ termid: "
|
|
416
|
-
extraSampleTabLabel: "
|
|
415
|
+
sampleColumns: [{ termid: "diaggrp" }],
|
|
416
|
+
extraSampleTabLabel: "diaggrp"
|
|
417
417
|
},
|
|
418
418
|
data: {
|
|
419
419
|
sameLegend: true,
|
|
@@ -437,14 +437,22 @@ function termdb_test_default() {
|
|
|
437
437
|
folder: "files/hg38/TermdbTest/scrna/geneExpHdf5"
|
|
438
438
|
}
|
|
439
439
|
},
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
// w2 plot: each sample with images is a subfolder of this directory,
|
|
445
|
-
// holding that sample's .svs files (folder/<sample>/<fileName>)
|
|
440
|
+
// w2 plot. folder: spatial images, one per subfolder of the sample's
|
|
441
|
+
// directory (folder/<sample>/<imageName>/), files inside found by the
|
|
442
|
+
// *FileSuffix fields. wsiFolder: plain whole-slide images, laid out as
|
|
443
|
+
// wsiFolder/<sample>/<imageName>/<slide file>
|
|
446
444
|
w2: {
|
|
447
|
-
folder: "files/hg38/TermdbTest/
|
|
445
|
+
folder: "files/hg38/TermdbTest/spatial",
|
|
446
|
+
wsiFolder: "files/hg38/TermdbTest/wsimages",
|
|
447
|
+
// .ome.tif extension is required for the JPEG-2000 OME-TIFF reader
|
|
448
|
+
// in wsi_tile.py open_slide() to engage
|
|
449
|
+
tiffFileSuffix: "morphology.ome.tif",
|
|
450
|
+
nucleusBoundariesFileSuffix: "nucleus_boundaries.csv",
|
|
451
|
+
cellBoundariesFileSuffix: "cell_boundaries.csv",
|
|
452
|
+
geneExpressionFileSuffix: "gene_expression.h5",
|
|
453
|
+
// default viewer settings, overridable in the burger menu
|
|
454
|
+
geneExpression: "PTPRC",
|
|
455
|
+
annotationLevel: 1
|
|
448
456
|
},
|
|
449
457
|
trackLst: {
|
|
450
458
|
jsonFile: "files/hg38/TermdbTest/trackLst/facet.json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.205.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",
|
|
@@ -46,9 +46,8 @@
|
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
|
47
47
|
"babel-loader": "^8.2.2",
|
|
48
48
|
"c8": "^10.1.3",
|
|
49
|
-
"esbuild": "^0.
|
|
49
|
+
"esbuild": "^0.28.1",
|
|
50
50
|
"monocart-coverage-reports": "^2.12.1",
|
|
51
|
-
"node-notifier": "^9.0.1",
|
|
52
51
|
"node-watch": "^0.7.1",
|
|
53
52
|
"nodemon": "^3.1.10",
|
|
54
53
|
"prettier": "^2.8.8",
|
|
@@ -57,33 +56,29 @@
|
|
|
57
56
|
"typescript": "^5.6.3"
|
|
58
57
|
},
|
|
59
58
|
"dependencies": {
|
|
60
|
-
"@sjcrh/augen": "2.
|
|
61
|
-
"@sjcrh/proteinpaint-python": "2.
|
|
62
|
-
"@sjcrh/proteinpaint-r": "2.
|
|
63
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
64
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
65
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
59
|
+
"@sjcrh/augen": "2.204.0",
|
|
60
|
+
"@sjcrh/proteinpaint-python": "2.204.0",
|
|
61
|
+
"@sjcrh/proteinpaint-r": "2.204.0",
|
|
62
|
+
"@sjcrh/proteinpaint-rust": "2.204.0",
|
|
63
|
+
"@sjcrh/proteinpaint-shared": "2.205.0",
|
|
64
|
+
"@sjcrh/proteinpaint-types": "2.205.0",
|
|
66
65
|
"@types/express": "^5.0.0",
|
|
67
66
|
"@types/express-session": "^1.18.1",
|
|
68
67
|
"better-sqlite3": "^12.4.1",
|
|
69
68
|
"body-parser": "^1.15.2",
|
|
70
69
|
"canvas": "~3.2.0",
|
|
71
70
|
"compression": "^1.6.2",
|
|
72
|
-
"connect-redis": "^6.1.3",
|
|
73
71
|
"cookie-parser": "^1.4.5",
|
|
74
72
|
"d3": "^7.6.1",
|
|
75
73
|
"deep-object-diff": "^1.1.0",
|
|
76
74
|
"express": "^4.17.1",
|
|
77
75
|
"express-basic-auth": "^1.1.5",
|
|
78
76
|
"express-session": "^1.18.1",
|
|
79
|
-
"got": "^14.2.0",
|
|
80
77
|
"jsonwebtoken": "^9.0.3",
|
|
81
78
|
"ky": "^1.2.1",
|
|
82
79
|
"micromatch": "^4.0.5",
|
|
83
80
|
"minimatch": "^10.0.1",
|
|
84
|
-
"node-fetch": "^2.6.1",
|
|
85
81
|
"partjson": "^0.58.2",
|
|
86
|
-
"redis": "^4.7.0",
|
|
87
82
|
"skia-canvas": "~3.0.8",
|
|
88
83
|
"tiny-async-pool": "^1.2.0",
|
|
89
84
|
"tough-cookie": "^4.1.4"
|
package/routes/brainImaging.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import fs from "fs";
|
|
1
2
|
import path from "path";
|
|
2
3
|
import serverconfig from "#src/serverconfig.js";
|
|
3
4
|
import { getData } from "../src/termdb.matrix.js";
|
|
5
|
+
import { filterSampleNamesByAccess } from "#src/termdb.sql.js";
|
|
4
6
|
import { isNumericTerm } from "#shared";
|
|
5
7
|
import { getColors } from "#shared";
|
|
6
8
|
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
@@ -13,10 +15,10 @@ function init({ genomes }) {
|
|
|
13
15
|
const ds = g.datasets[query.dslabel];
|
|
14
16
|
if (!ds) throw "invalid dataset name";
|
|
15
17
|
let plane, index;
|
|
16
|
-
if (query.l) {
|
|
18
|
+
if (query.l != void 0) {
|
|
17
19
|
plane = "L";
|
|
18
20
|
index = query.l;
|
|
19
|
-
} else if (query.f) {
|
|
21
|
+
} else if (query.f != void 0) {
|
|
20
22
|
plane = "F";
|
|
21
23
|
index = query.f;
|
|
22
24
|
} else {
|
|
@@ -27,7 +29,7 @@ function init({ genomes }) {
|
|
|
27
29
|
res.send({ brainImage, plane, legend });
|
|
28
30
|
} catch (e) {
|
|
29
31
|
console.log(e);
|
|
30
|
-
res.status(404).send("Sample brain image not found");
|
|
32
|
+
res.status(404).send(typeof e == "string" ? e : "Sample brain image not found");
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -43,8 +45,14 @@ async function getBrainImage(query, genomes, plane, index) {
|
|
|
43
45
|
const overlayTW = query.overlayTW;
|
|
44
46
|
if (divideByTW) terms.push(divideByTW);
|
|
45
47
|
if (overlayTW) terms.push(overlayTW);
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
+
let selectedSampleNames = query.selectedSampleFileNames.map((s) => s.split(".nii")[0]);
|
|
49
|
+
const existingFiles = new Set(await fs.promises.readdir(dirPath));
|
|
50
|
+
selectedSampleNames = selectedSampleNames.filter((s) => existingFiles.has(s + ".nii"));
|
|
51
|
+
if (!selectedSampleNames.length) throw "no brain imaging data for the requested sample(s)";
|
|
52
|
+
selectedSampleNames = await filterSampleNamesByAccess(query, ds, selectedSampleNames);
|
|
53
|
+
if (!selectedSampleNames.length) throw "no selected samples pass the current filter";
|
|
54
|
+
const data = await getData({ terms, __protected__: query.__protected__ }, ds);
|
|
55
|
+
if (data.error) throw data.error;
|
|
48
56
|
const divideByCat = {};
|
|
49
57
|
const uniqueOverlayTwCats = /* @__PURE__ */ new Set();
|
|
50
58
|
for (const sampleName of selectedSampleNames) {
|
|
@@ -53,11 +61,11 @@ async function getBrainImage(query, genomes, plane, index) {
|
|
|
53
61
|
const samplePath = path.join(dirPath, sampleName) + ".nii";
|
|
54
62
|
let divideCategory = "default";
|
|
55
63
|
let overlayCategory = "default";
|
|
56
|
-
if (divideByTW) {
|
|
64
|
+
if (divideByTW && sampleData) {
|
|
57
65
|
const value = sampleData[divideByTW.$id];
|
|
58
66
|
if (value) divideCategory = divideByTW.term.values?.[value.key]?.label || value.key;
|
|
59
67
|
}
|
|
60
|
-
if (overlayTW) {
|
|
68
|
+
if (overlayTW && sampleData) {
|
|
61
69
|
const value = sampleData[overlayTW.$id];
|
|
62
70
|
if (value) {
|
|
63
71
|
overlayCategory = overlayTW.term.values?.[value.key]?.label || value.key;
|
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import serverconfig from "#src/serverconfig.js";
|
|
4
4
|
import { getData } from "#src/termdb.matrix.js";
|
|
5
|
+
import { filterSampleNamesByAccess } from "#src/termdb.sql.js";
|
|
5
6
|
function init({ genomes }) {
|
|
6
7
|
return async (req, res) => {
|
|
7
8
|
try {
|
|
@@ -14,7 +15,7 @@ function init({ genomes }) {
|
|
|
14
15
|
res.send({ samples });
|
|
15
16
|
} catch (e) {
|
|
16
17
|
console.log(e);
|
|
17
|
-
res.status(404).send("
|
|
18
|
+
res.status(404).send(typeof e == "string" ? e : "Cannot get brain imaging samples");
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -24,15 +25,20 @@ async function getBrainImageSamples(query, genomes) {
|
|
|
24
25
|
const key = query.refKey;
|
|
25
26
|
if (q[key].referenceFile && q[key].samples) {
|
|
26
27
|
const dirPath = path.join(serverconfig.tpmasterdir, q[key].samples);
|
|
27
|
-
const files = fs.
|
|
28
|
-
|
|
28
|
+
const files = (await fs.promises.readdir(dirPath, { withFileTypes: true })).filter((f) => f.isFile() && f.name.endsWith(".nii")).map((f) => f.name);
|
|
29
|
+
let sampleNames = files.map((name) => name.split(".nii")[0]);
|
|
30
|
+
sampleNames = await filterSampleNamesByAccess(query, ds, sampleNames);
|
|
31
|
+
if (query.samplesOnly) return sampleNames.map((name) => ({ sample: name }));
|
|
29
32
|
if (q[key].sampleColumns) {
|
|
30
|
-
const terms = q[key].sampleColumns.map((term) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const terms = q[key].sampleColumns.map((term) => {
|
|
34
|
+
const termjson = ds.cohort.termdb.q.termjsonByOneid(term.termid);
|
|
35
|
+
return {
|
|
36
|
+
$id: term.termid,
|
|
37
|
+
term: termjson,
|
|
38
|
+
q: termjson?.type == "float" || termjson?.type == "integer" ? { mode: "continuous" } : {}
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const data = await getData({ terms, __protected__: query.__protected__ }, ds);
|
|
36
42
|
if (data.error) throw data.error;
|
|
37
43
|
const samples = {};
|
|
38
44
|
for (const s of sampleNames) {
|
|
@@ -20,9 +20,9 @@ function init({ genomes }) {
|
|
|
20
20
|
res.send({
|
|
21
21
|
data: {
|
|
22
22
|
[group1Name]: groups.group1names.length,
|
|
23
|
-
[group2Name]: groups.group2names.length
|
|
24
|
-
|
|
25
|
-
}
|
|
23
|
+
[group2Name]: groups.group2names.length
|
|
24
|
+
},
|
|
25
|
+
...groups.alerts.length ? { alert: groups.alerts.join(" | ") } : {}
|
|
26
26
|
});
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
@@ -43,12 +43,51 @@ function init({ genomes }) {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
function
|
|
46
|
+
function resolveElementQuery(ds, elementType) {
|
|
47
|
+
const key = elementType ?? "promoter";
|
|
48
|
+
const dm = ds?.queries?.dnaMethylation;
|
|
49
|
+
if (!dm) throw new Error("This dataset does not have methylation data configured.");
|
|
50
|
+
const q = dm.elements?.[key] ?? (key === "promoter" ? dm.promoter : void 0);
|
|
51
|
+
if (!q) {
|
|
52
|
+
const available = [
|
|
53
|
+
...Object.keys(dm.elements ?? {}),
|
|
54
|
+
...dm.promoter && !dm.elements?.promoter ? ["promoter"] : []
|
|
55
|
+
];
|
|
56
|
+
throw new Error(
|
|
57
|
+
available.length ? `Unknown element type '${key}'. This dataset offers: ${available.join(", ")}.` : "This dataset does not have element-level methylation data configured."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!q.file) throw new Error(`Methylation matrix file is not configured for element type '${key}'.`);
|
|
61
|
+
return { key, q };
|
|
62
|
+
}
|
|
63
|
+
function listElementTypes(ds) {
|
|
64
|
+
const dm = ds?.queries?.dnaMethylation;
|
|
65
|
+
if (!dm) return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const [key, e] of Object.entries(dm.elements ?? {})) {
|
|
68
|
+
if (e?.file) out.push({ key, label: e.label || key });
|
|
69
|
+
}
|
|
70
|
+
if (dm.promoter?.file && !dm.elements?.promoter) {
|
|
71
|
+
out.unshift({ key: "promoter", label: dm.promoter.label || "Promoters" });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function dmKeyInputs(req, imputeMissing) {
|
|
47
76
|
return {
|
|
48
77
|
genome: req.genome,
|
|
49
78
|
dslabel: req.dslabel,
|
|
79
|
+
/* Which element matrix was tested. Without this field a block request with the
|
|
80
|
+
same sample groups as an earlier promoter request hashes to the same key and is
|
|
81
|
+
served the promoter result — wrong rows, wrong coordinates, no error. Defaulting
|
|
82
|
+
to 'promoter' rather than null keeps pre-existing cache entries valid on deploy. */
|
|
83
|
+
element_type: req.element_type ?? "promoter",
|
|
84
|
+
/* Derived from ds config rather than sent by the client, but it still belongs in the
|
|
85
|
+
key: flipping a dataset's platform changes every p-value, and without it the on-disk
|
|
86
|
+
cache would keep serving results computed under the old missingness model. */
|
|
87
|
+
impute_missing: imputeMissing,
|
|
50
88
|
samplelst: canonicalizeSamplelst(req.samplelst),
|
|
51
89
|
min_samples_per_group: req.min_samples_per_group ?? null,
|
|
90
|
+
exclude_sex_chr: req.exclude_sex_chr ?? null,
|
|
52
91
|
tw: req.tw ?? null,
|
|
53
92
|
tw2: req.tw2 ?? null,
|
|
54
93
|
filter: req.filter ?? null,
|
|
@@ -56,26 +95,33 @@ function dmKeyInputs(req) {
|
|
|
56
95
|
};
|
|
57
96
|
}
|
|
58
97
|
async function getDmCacheResult(req, genomes) {
|
|
98
|
+
const imputeMissing = genomes?.[req.genome]?.datasets?.[req.dslabel]?.queries?.dnaMethylation?.platform != "wgbs";
|
|
59
99
|
const { result, cacheId } = await cacheOrRecompute({
|
|
60
|
-
computeArgument: dmKeyInputs(req),
|
|
100
|
+
computeArgument: dmKeyInputs(req, imputeMissing),
|
|
61
101
|
cacheSubdir: "dm",
|
|
62
102
|
computeFresh: async () => {
|
|
63
103
|
const { ds, term_results, term_results2 } = await resolveDaContext(req, genomes);
|
|
64
|
-
return runDmFresh(req, ds, term_results, term_results2);
|
|
104
|
+
return runDmFresh(req, ds, term_results, term_results2, imputeMissing);
|
|
65
105
|
}
|
|
66
106
|
});
|
|
67
107
|
return { result, cacheId };
|
|
68
108
|
}
|
|
69
|
-
async function runDmFresh(param, ds, term_results, term_results2) {
|
|
109
|
+
async function runDmFresh(param, ds, term_results, term_results2, imputeMissing) {
|
|
70
110
|
const groups = await resolveDmSampleGroups(param, ds, term_results, term_results2);
|
|
71
111
|
if (groups.alerts.length) throw new Error(groups.alerts.join(" | "));
|
|
72
|
-
const q = ds.
|
|
112
|
+
const { q } = resolveElementQuery(ds, param.element_type);
|
|
73
113
|
const diffMethInput = {
|
|
74
114
|
// Group 1 is control, group 2 is case (same convention as DE).
|
|
75
115
|
case: groups.group2names.join(","),
|
|
76
116
|
control: groups.group1names.join(","),
|
|
77
117
|
input_file: q.file,
|
|
78
|
-
|
|
118
|
+
// Omitted entirely when the entry does not set it, so an unfiltered request stays
|
|
119
|
+
// byte-identical to what it was before this field existed.
|
|
120
|
+
...q.element_class ? { element_class: q.element_class } : {},
|
|
121
|
+
min_samples_per_group: param.min_samples_per_group,
|
|
122
|
+
exclude_sex_chr: param.exclude_sex_chr,
|
|
123
|
+
// ds-derived, not a user setting: see the platform field on queries.dnaMethylation
|
|
124
|
+
impute_missing: imputeMissing
|
|
79
125
|
};
|
|
80
126
|
if (param.tw) {
|
|
81
127
|
diffMethInput.conf1 = [...groups.conf1_group2, ...groups.conf1_group1];
|
|
@@ -104,12 +150,10 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
104
150
|
throw new Error("Group 1 has no samples. Please select at least one sample.");
|
|
105
151
|
if (param.samplelst.groups[1].values?.length < 1)
|
|
106
152
|
throw new Error("Group 2 has no samples. Please select at least one sample.");
|
|
107
|
-
const q = ds.
|
|
108
|
-
if (!q) throw new Error("This dataset does not have promoter-level methylation data configured.");
|
|
109
|
-
if (!q.file) throw new Error("Promoter methylation data file is not configured for this dataset.");
|
|
153
|
+
const { q } = resolveElementQuery(ds, param.element_type);
|
|
110
154
|
const g1 = await buildGroupValues(
|
|
111
155
|
param.samplelst.groups[0].values,
|
|
112
|
-
q,
|
|
156
|
+
q.allSampleSet,
|
|
113
157
|
ds,
|
|
114
158
|
param.tw,
|
|
115
159
|
param.tw2,
|
|
@@ -118,7 +162,7 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
118
162
|
);
|
|
119
163
|
const g2 = await buildGroupValues(
|
|
120
164
|
param.samplelst.groups[1].values,
|
|
121
|
-
q,
|
|
165
|
+
q.allSampleSet,
|
|
122
166
|
ds,
|
|
123
167
|
param.tw,
|
|
124
168
|
param.tw2,
|
|
@@ -146,5 +190,7 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
146
190
|
export {
|
|
147
191
|
getDmCacheResult,
|
|
148
192
|
init,
|
|
149
|
-
|
|
193
|
+
listElementTypes,
|
|
194
|
+
resolveDmSampleGroups,
|
|
195
|
+
resolveElementQuery
|
|
150
196
|
};
|