@sjcrh/proteinpaint-server 2.142.0 → 2.143.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/protected.test.js +1 -2
- package/dataset/termdb.test.js +3 -2
- package/package.json +9 -8
- package/routes/aiProjectAdmin.js +46 -60
- package/routes/aiProjectSelectedWSImages.js +161 -0
- package/routes/brainImaging.js +9 -18
- package/routes/brainImagingSamples.js +2 -4
- package/routes/burden.js +13 -26
- package/routes/correlationVolcano.js +18 -36
- package/routes/dataset.js +6 -12
- package/routes/deleteWSIAnnotation.js +75 -0
- package/routes/dsdata.js +7 -14
- package/routes/dzimages.js +4 -8
- package/routes/gdc.grin2.list.js +13 -26
- package/routes/gdc.grin2.run.js +3 -6
- package/routes/gdc.maf.js +8 -16
- package/routes/gdc.mafBuild.js +14 -28
- package/routes/gene2canonicalisoform.js +4 -8
- package/routes/genelookup.js +2 -4
- package/routes/genesetEnrichment.js +6 -12
- package/routes/genesetOverrepresentation.js +1 -2
- package/routes/genomes.js +1 -2
- package/routes/grin2.js +13 -17
- package/routes/healthcheck.js +3 -6
- package/routes/hicdata.js +4 -8
- package/routes/hicgenome.js +4 -8
- package/routes/hicstat.js +2 -4
- package/routes/img.js +1 -2
- package/routes/isoformlst.js +6 -12
- package/routes/ntseq.js +4 -8
- package/routes/pdomain.js +5 -10
- package/routes/sampledzimages.js +2 -4
- package/routes/samplewsimages.js +3 -67
- package/routes/saveWSIAnnotation.js +100 -0
- package/routes/snp.js +9 -18
- package/routes/termdb.DE.js +23 -46
- package/routes/termdb.boxplot.js +84 -84
- package/routes/termdb.categories.js +9 -18
- package/routes/termdb.cluster.js +23 -46
- package/routes/termdb.cohort.summary.js +3 -6
- package/routes/termdb.cohorts.js +4 -8
- package/routes/termdb.config.js +32 -64
- package/routes/termdb.descrstats.js +6 -12
- package/routes/termdb.filterTermValues.js +4 -8
- package/routes/termdb.numericcategories.js +5 -10
- package/routes/termdb.percentile.js +6 -12
- package/routes/termdb.profileFormScores.js +12 -24
- package/routes/termdb.profileScores.js +7 -14
- package/routes/termdb.rootterm.js +4 -8
- package/routes/termdb.sampleImages.js +4 -8
- package/routes/termdb.singleSampleMutation.js +9 -18
- package/routes/termdb.singlecellDEgenes.js +4 -8
- package/routes/termdb.singlecellData.js +4 -8
- package/routes/termdb.singlecellSamples.js +28 -56
- package/routes/termdb.termchildren.js +5 -10
- package/routes/termdb.termsbyids.js +4 -8
- package/routes/termdb.topMutatedGenes.js +15 -30
- package/routes/termdb.topTermsByType.js +9 -18
- package/routes/termdb.topVariablyExpressedGenes.js +13 -26
- package/routes/termdb.violin.js +124 -135
- package/routes/tileserver.js +14 -15
- package/routes/wsimages.js +42 -46
- package/routes/wsisamples.js +3 -6
- package/src/app.js +4345 -6708
- package/routes/sampleWsiAiApi.js +0 -33
package/routes/genomes.js
CHANGED
package/routes/grin2.js
CHANGED
|
@@ -25,13 +25,10 @@ function init({ genomes }) {
|
|
|
25
25
|
const request = req.query;
|
|
26
26
|
console.log("[GRIN2] request:", request);
|
|
27
27
|
const g = genomes[request.genome];
|
|
28
|
-
if (!g)
|
|
29
|
-
throw new Error("genome missing");
|
|
28
|
+
if (!g) throw new Error("genome missing");
|
|
30
29
|
const ds = g.datasets?.[request.dslabel];
|
|
31
|
-
if (!ds)
|
|
32
|
-
|
|
33
|
-
if (!ds.queries?.singleSampleMutation)
|
|
34
|
-
throw new Error("singleSampleMutation query missing from dataset");
|
|
30
|
+
if (!ds) throw new Error("ds missing");
|
|
31
|
+
if (!ds.queries?.singleSampleMutation) throw new Error("singleSampleMutation query missing from dataset");
|
|
35
32
|
const result = await runGrin2(g, ds, request);
|
|
36
33
|
res.json(result);
|
|
37
34
|
} catch (e) {
|
|
@@ -76,12 +73,14 @@ async function runGrin2(g, ds, request) {
|
|
|
76
73
|
const pyInput = {
|
|
77
74
|
genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
|
|
78
75
|
chromosomelist: {},
|
|
79
|
-
lesion: JSON.stringify(lesions)
|
|
76
|
+
lesion: JSON.stringify(lesions),
|
|
77
|
+
devicePixelRatio: request.devicePixelRatio,
|
|
78
|
+
plot_width: request.plot_width,
|
|
79
|
+
plot_height: request.plot_height
|
|
80
80
|
};
|
|
81
81
|
for (const c in g.majorchr) {
|
|
82
82
|
if (ds.queries.singleSampleMutation.discoPlot?.skipChrM) {
|
|
83
|
-
if (c.toLowerCase() == "chrm")
|
|
84
|
-
continue;
|
|
83
|
+
if (c.toLowerCase() == "chrm") continue;
|
|
85
84
|
}
|
|
86
85
|
pyInput.chromosomelist[c] = g.majorchr[c];
|
|
87
86
|
}
|
|
@@ -105,6 +104,7 @@ async function runGrin2(g, ds, request) {
|
|
|
105
104
|
const response = {
|
|
106
105
|
status: "success",
|
|
107
106
|
pngImg: resultData.png[0],
|
|
107
|
+
plotData: resultData.plotData,
|
|
108
108
|
topGeneTable: resultData.topGeneTable,
|
|
109
109
|
totalGenes: resultData.totalGenes,
|
|
110
110
|
showingTop: resultData.showingTop,
|
|
@@ -155,8 +155,7 @@ async function processSampleMlst(sampleName, mlst, startId, request) {
|
|
|
155
155
|
for (const m of mlst) {
|
|
156
156
|
switch (m.dt) {
|
|
157
157
|
case dtsnvindel: {
|
|
158
|
-
if (!request.snvindelOptions)
|
|
159
|
-
break;
|
|
158
|
+
if (!request.snvindelOptions) break;
|
|
160
159
|
const snvIndelLesion = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
|
|
161
160
|
if (snvIndelLesion) {
|
|
162
161
|
lesions.push(snvIndelLesion);
|
|
@@ -164,8 +163,7 @@ async function processSampleMlst(sampleName, mlst, startId, request) {
|
|
|
164
163
|
break;
|
|
165
164
|
}
|
|
166
165
|
case dtcnv: {
|
|
167
|
-
if (!request.cnvOptions)
|
|
168
|
-
break;
|
|
166
|
+
if (!request.cnvOptions) break;
|
|
169
167
|
const cnvLesion = filterAndConvertCnv(sampleName, m, request.cnvOptions);
|
|
170
168
|
if (cnvLesion) {
|
|
171
169
|
lesions.push(cnvLesion);
|
|
@@ -173,8 +171,7 @@ async function processSampleMlst(sampleName, mlst, startId, request) {
|
|
|
173
171
|
break;
|
|
174
172
|
}
|
|
175
173
|
case dtfusionrna: {
|
|
176
|
-
if (!request.fusionOptions)
|
|
177
|
-
break;
|
|
174
|
+
if (!request.fusionOptions) break;
|
|
178
175
|
const fusionLesion = filterAndConvertFusion(sampleName, m, request.fusionOptions);
|
|
179
176
|
if (fusionLesion) {
|
|
180
177
|
lesions.push(fusionLesion);
|
|
@@ -217,8 +214,7 @@ function filterAndConvertCnv(sampleName, entry, options) {
|
|
|
217
214
|
}
|
|
218
215
|
const isGain = entry.value >= options.gainThreshold;
|
|
219
216
|
const isLoss = entry.value <= options.lossThreshold;
|
|
220
|
-
if (!isGain && !isLoss)
|
|
221
|
-
return null;
|
|
217
|
+
if (!isGain && !isLoss) return null;
|
|
222
218
|
const lesionType = isGain ? "gain" : "loss";
|
|
223
219
|
return [sampleName, entry.chr, entry.start, entry.stop, lesionType];
|
|
224
220
|
}
|
package/routes/healthcheck.js
CHANGED
|
@@ -17,12 +17,9 @@ function init({ genomes }) {
|
|
|
17
17
|
if (q.dslabel) {
|
|
18
18
|
for (const gn in genomes) {
|
|
19
19
|
const ds = genomes[gn]?.datasets?.[q.dslabel];
|
|
20
|
-
if (!ds?.getHealth)
|
|
21
|
-
|
|
22
|
-
if (!health.byDataset)
|
|
23
|
-
health.byDataset = {};
|
|
24
|
-
if (!health.byDataset[q.dslabel])
|
|
25
|
-
health.byDataset[q.dslabel] = {};
|
|
20
|
+
if (!ds?.getHealth) continue;
|
|
21
|
+
if (!health.byDataset) health.byDataset = {};
|
|
22
|
+
if (!health.byDataset[q.dslabel]) health.byDataset[q.dslabel] = {};
|
|
26
23
|
health.byDataset[q.dslabel][gn] = ds.getHealth(ds);
|
|
27
24
|
}
|
|
28
25
|
}
|
package/routes/hicdata.js
CHANGED
|
@@ -24,16 +24,14 @@ function init() {
|
|
|
24
24
|
res.send(payload);
|
|
25
25
|
} catch (e) {
|
|
26
26
|
res.send({ error: e?.message || e });
|
|
27
|
-
if (e instanceof Error && e.stack)
|
|
28
|
-
console.log(e);
|
|
27
|
+
if (e instanceof Error && e.stack) console.log(e);
|
|
29
28
|
}
|
|
30
29
|
};
|
|
31
30
|
}
|
|
32
31
|
function handle_hicdata(q) {
|
|
33
32
|
return new Promise((resolve, reject) => {
|
|
34
33
|
const [e, file] = fileurl({ query: q });
|
|
35
|
-
if (e)
|
|
36
|
-
reject({ error: "illegal file name" });
|
|
34
|
+
if (e) reject({ error: "illegal file name" });
|
|
37
35
|
const matrixType = q.matrixType == "log(oe)" ? "oe" : q.matrixType ? q.matrixType : "observed";
|
|
38
36
|
const par = [matrixType, q.nmeth || "NONE", file, q.pos1, q.pos2, q.isfrag ? "FRAG" : "BP", q.resolution];
|
|
39
37
|
const ps = spawn(serverconfig.hicstraw, par);
|
|
@@ -63,10 +61,8 @@ function handle_hicdata(q) {
|
|
|
63
61
|
ps.stderr.on("data", (i) => erroutput.push(i));
|
|
64
62
|
ps.on("close", () => {
|
|
65
63
|
const err = erroutput.join("");
|
|
66
|
-
if (err)
|
|
67
|
-
|
|
68
|
-
if (linenot3fields)
|
|
69
|
-
reject({ error: linenot3fields + " lines have other than 3 fields" });
|
|
64
|
+
if (err) reject({ error: err });
|
|
65
|
+
if (linenot3fields) reject({ error: linenot3fields + " lines have other than 3 fields" });
|
|
70
66
|
if (fieldnotnumerical)
|
|
71
67
|
reject({ error: fieldnotnumerical + " lines have non-numerical values in any of the 3 fields" });
|
|
72
68
|
resolve({ items });
|
package/routes/hicgenome.js
CHANGED
|
@@ -22,8 +22,7 @@ function init() {
|
|
|
22
22
|
const data = [];
|
|
23
23
|
const erroutput = [];
|
|
24
24
|
const [e, file] = fileurl({ query });
|
|
25
|
-
if (e)
|
|
26
|
-
res.send({ error: "illegal file name" });
|
|
25
|
+
if (e) res.send({ error: "illegal file name" });
|
|
27
26
|
const matrixType = req.query.matrixType == "log(oe)" ? "oe" : req.query.matrixType ? req.query.matrixType : "observed";
|
|
28
27
|
const promises = req.query.chrlst.map((lead, i) => {
|
|
29
28
|
return req.query.chrlst.slice(0, i + 1).map((follow, j) => {
|
|
@@ -55,10 +54,8 @@ function init() {
|
|
|
55
54
|
data.push({ items, lead, follow });
|
|
56
55
|
ps.stderr.on("data", (i2) => erroutput.push(`${lead} - ${follow}: `, i2));
|
|
57
56
|
ps.on("close", () => {
|
|
58
|
-
if (erroutput.length)
|
|
59
|
-
|
|
60
|
-
if (linenot3fields)
|
|
61
|
-
reject({ error: `${linenot3fields} lines have other than 3 fields` });
|
|
57
|
+
if (erroutput.length) reject({ error: erroutput.join("") });
|
|
58
|
+
if (linenot3fields) reject({ error: `${linenot3fields} lines have other than 3 fields` });
|
|
62
59
|
if (fieldnotnumerical)
|
|
63
60
|
reject(`${fieldnotnumerical} lines have non-numerical values in any of the 3 fields`);
|
|
64
61
|
resolve();
|
|
@@ -69,8 +66,7 @@ function init() {
|
|
|
69
66
|
}).flat();
|
|
70
67
|
Promise.allSettled(promises).then(() => res.send({ data, error: erroutput.join("") })).catch((e2) => {
|
|
71
68
|
res.send({ error: e2?.message || e2 });
|
|
72
|
-
if (e2 instanceof Error && e2.stack)
|
|
73
|
-
console.log(e2);
|
|
69
|
+
if (e2 instanceof Error && e2.stack) console.log(e2);
|
|
74
70
|
});
|
|
75
71
|
};
|
|
76
72
|
}
|
package/routes/hicstat.js
CHANGED
|
@@ -18,8 +18,7 @@ function init() {
|
|
|
18
18
|
return async (req, res) => {
|
|
19
19
|
try {
|
|
20
20
|
const [e, file, isurl] = fileurl(req);
|
|
21
|
-
if (e)
|
|
22
|
-
throw "illegal file name";
|
|
21
|
+
if (e) throw "illegal file name";
|
|
23
22
|
if (!isurl) {
|
|
24
23
|
await file_is_readable(file);
|
|
25
24
|
}
|
|
@@ -27,8 +26,7 @@ function init() {
|
|
|
27
26
|
res.send({ out });
|
|
28
27
|
} catch (e) {
|
|
29
28
|
res.send({ error: e instanceof Error ? e.message : e });
|
|
30
|
-
if (e instanceof Error && e.stack)
|
|
31
|
-
console.log(e);
|
|
29
|
+
if (e instanceof Error && e.stack) console.log(e);
|
|
32
30
|
}
|
|
33
31
|
};
|
|
34
32
|
}
|
package/routes/img.js
CHANGED
|
@@ -28,8 +28,7 @@ function init() {
|
|
|
28
28
|
async function sendImage(req, res) {
|
|
29
29
|
const [e, file] = utils.fileurl(req, true);
|
|
30
30
|
try {
|
|
31
|
-
if (e)
|
|
32
|
-
throw "invalid image file";
|
|
31
|
+
if (e) throw "invalid image file";
|
|
33
32
|
const data = await fs.promises.readFile(file);
|
|
34
33
|
const ext = path.extname(file).substring(1);
|
|
35
34
|
const image = {
|
package/routes/isoformlst.js
CHANGED
|
@@ -21,20 +21,16 @@ function init({ genomes }) {
|
|
|
21
21
|
try {
|
|
22
22
|
const q = req.query;
|
|
23
23
|
const g = genomes[q.genome];
|
|
24
|
-
if (!g)
|
|
25
|
-
|
|
26
|
-
if (!Array.isArray(q.lst))
|
|
27
|
-
throw ".lst missing";
|
|
24
|
+
if (!g) throw "invalid genome";
|
|
25
|
+
if (!Array.isArray(q.lst)) throw ".lst missing";
|
|
28
26
|
const lst = [];
|
|
29
27
|
for (const isoform of q.lst) {
|
|
30
|
-
if (g.genomicNameRegexp.test(isoform))
|
|
31
|
-
continue;
|
|
28
|
+
if (g.genomicNameRegexp.test(isoform)) continue;
|
|
32
29
|
const tmp = g.genedb.getjsonbyisoform.all(isoform);
|
|
33
30
|
lst.push(
|
|
34
31
|
tmp.map((i) => {
|
|
35
32
|
const j = JSON.parse(i.genemodel);
|
|
36
|
-
if (i.isdefault)
|
|
37
|
-
j.isdefault = true;
|
|
33
|
+
if (i.isdefault) j.isdefault = true;
|
|
38
34
|
return j;
|
|
39
35
|
})
|
|
40
36
|
);
|
|
@@ -42,10 +38,8 @@ function init({ genomes }) {
|
|
|
42
38
|
res.send({ lst });
|
|
43
39
|
} catch (e) {
|
|
44
40
|
res.send({ error: e.message || e });
|
|
45
|
-
if (e.stack)
|
|
46
|
-
|
|
47
|
-
else
|
|
48
|
-
console.trace(e);
|
|
41
|
+
if (e.stack) console.log(e.stack);
|
|
42
|
+
else console.trace(e);
|
|
49
43
|
}
|
|
50
44
|
};
|
|
51
45
|
}
|
package/routes/ntseq.js
CHANGED
|
@@ -17,21 +17,17 @@ function init({ genomes }) {
|
|
|
17
17
|
return async function handle_ntseq(req, res) {
|
|
18
18
|
try {
|
|
19
19
|
const q = req.query;
|
|
20
|
-
if (!q.coord)
|
|
21
|
-
throw "coord missing";
|
|
20
|
+
if (!q.coord) throw "coord missing";
|
|
22
21
|
const g = genomes[q.genome];
|
|
23
|
-
if (!g)
|
|
24
|
-
|
|
25
|
-
if (!g.genomefile)
|
|
26
|
-
throw "no sequence file available";
|
|
22
|
+
if (!g) throw "invalid genome";
|
|
23
|
+
if (!g.genomefile) throw "no sequence file available";
|
|
27
24
|
const seq = await get_fasta(g, q.coord);
|
|
28
25
|
res.send({
|
|
29
26
|
seq: seq.split("\n").slice(1).join("")
|
|
30
27
|
});
|
|
31
28
|
} catch (e) {
|
|
32
29
|
res.send({ error: e.message || e });
|
|
33
|
-
if (e.stack)
|
|
34
|
-
console.log(e.stack);
|
|
30
|
+
if (e.stack) console.log(e.stack);
|
|
35
31
|
}
|
|
36
32
|
};
|
|
37
33
|
}
|
package/routes/pdomain.js
CHANGED
|
@@ -21,20 +21,16 @@ function init({ genomes }) {
|
|
|
21
21
|
try {
|
|
22
22
|
const q = req.query;
|
|
23
23
|
const gn = q.genome;
|
|
24
|
-
if (!gn)
|
|
25
|
-
throw "no genome";
|
|
24
|
+
if (!gn) throw "no genome";
|
|
26
25
|
const g = genomes[gn];
|
|
27
|
-
if (!g)
|
|
28
|
-
throw "invalid genome " + gn;
|
|
26
|
+
if (!g) throw "invalid genome " + gn;
|
|
29
27
|
if (!g.proteindomain) {
|
|
30
28
|
return res.send({ lst: [] });
|
|
31
29
|
}
|
|
32
|
-
if (!Array.isArray(q.isoforms))
|
|
33
|
-
throw "isoforms[] missing";
|
|
30
|
+
if (!Array.isArray(q.isoforms)) throw "isoforms[] missing";
|
|
34
31
|
const lst = [];
|
|
35
32
|
for (const isoform of q.isoforms) {
|
|
36
|
-
if (g.genomicNameRegexp.test(isoform))
|
|
37
|
-
continue;
|
|
33
|
+
if (g.genomicNameRegexp.test(isoform)) continue;
|
|
38
34
|
const tmp = g.proteindomain.getbyisoform.all(isoform);
|
|
39
35
|
lst.push({
|
|
40
36
|
name: isoform,
|
|
@@ -48,8 +44,7 @@ function init({ genomes }) {
|
|
|
48
44
|
res.send({ lst });
|
|
49
45
|
} catch (e) {
|
|
50
46
|
res.send({ error: e.message || e });
|
|
51
|
-
if (e.stack)
|
|
52
|
-
console.log(e.stack);
|
|
47
|
+
if (e.stack) console.log(e.stack);
|
|
53
48
|
}
|
|
54
49
|
};
|
|
55
50
|
}
|
package/routes/sampledzimages.js
CHANGED
|
@@ -20,11 +20,9 @@ function init({ genomes }) {
|
|
|
20
20
|
const q = req.query;
|
|
21
21
|
try {
|
|
22
22
|
const g = genomes[q.genome];
|
|
23
|
-
if (!g)
|
|
24
|
-
throw "invalid genome name";
|
|
23
|
+
if (!g) throw "invalid genome name";
|
|
25
24
|
const ds = g.datasets[q.dslabel];
|
|
26
|
-
if (!ds)
|
|
27
|
-
throw "invalid dataset name";
|
|
25
|
+
if (!ds) throw "invalid dataset name";
|
|
28
26
|
const sampleId = q.sample_id || "";
|
|
29
27
|
const sampleDZImagesPath = path.join(
|
|
30
28
|
`${serverconfig.tpmasterdir}/${ds.queries.DZImages.imageBySampleFolder}`,
|
package/routes/samplewsimages.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { sampleWSImagesPayload } from "#types/checkers";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import serverconfig from "#src/serverconfig.js";
|
|
5
2
|
const api = {
|
|
6
3
|
endpoint: "samplewsimages",
|
|
7
4
|
methods: {
|
|
@@ -20,71 +17,11 @@ function init({ genomes }) {
|
|
|
20
17
|
try {
|
|
21
18
|
const query = req.query;
|
|
22
19
|
const g = genomes[query.genome];
|
|
23
|
-
if (!g)
|
|
24
|
-
throw "invalid genome name";
|
|
20
|
+
if (!g) throw "invalid genome name";
|
|
25
21
|
const ds = g.datasets[query.dslabel];
|
|
26
|
-
if (!ds)
|
|
27
|
-
throw "invalid dataset name";
|
|
22
|
+
if (!ds) throw "invalid dataset name";
|
|
28
23
|
const sampleId = query.sample_id;
|
|
29
24
|
const wsimages = await ds.queries.WSImages.getWSImages(sampleId);
|
|
30
|
-
if (ds.queries.WSImages.getWSIAnnotations) {
|
|
31
|
-
for (const wsimage of wsimages) {
|
|
32
|
-
if (ds.queries.WSImages.makeGeoJson) {
|
|
33
|
-
await ds.queries.WSImages.makeGeoJson(sampleId, wsimage);
|
|
34
|
-
}
|
|
35
|
-
const annotations = await ds.queries.WSImages.getWSIAnnotations(sampleId, wsimage.filename);
|
|
36
|
-
if (annotations && annotations.length > 0) {
|
|
37
|
-
wsimage.overlays = annotations;
|
|
38
|
-
const annotationFilePath = path.join(
|
|
39
|
-
serverconfig.tpmasterdir,
|
|
40
|
-
ds.queries.WSImages.imageBySampleFolder,
|
|
41
|
-
sampleId,
|
|
42
|
-
annotations[0]
|
|
43
|
-
);
|
|
44
|
-
const annotationData = JSON.parse(fs.readFileSync(annotationFilePath, "utf8"));
|
|
45
|
-
if (!annotationData.features && !ds.queries.WSImages?.classes?.length) {
|
|
46
|
-
throw new Error(`No classes found for WSImage annotations in dataset ${ds.label}`);
|
|
47
|
-
}
|
|
48
|
-
wsimage.annotationsData = annotationData.features.map((d) => {
|
|
49
|
-
const featClass = ds.queries.WSImages?.classes?.find((f) => f.id == d.properties.class)?.label || d.properties.class;
|
|
50
|
-
return {
|
|
51
|
-
zoomCoordinates: d.properties.zoomCoordinates,
|
|
52
|
-
uncertainty: d.properties.uncertainty,
|
|
53
|
-
class: featClass
|
|
54
|
-
};
|
|
55
|
-
}).slice(15, 20);
|
|
56
|
-
wsimage.classes = ds.queries?.WSImages?.classes;
|
|
57
|
-
wsimage.uncertainty = ds.queries?.WSImages?.uncertainty;
|
|
58
|
-
wsimage.activePatchColor = ds.queries?.WSImages?.activePatchColor;
|
|
59
|
-
}
|
|
60
|
-
if (ds.queries.WSImages.getWSIPredictionPatches) {
|
|
61
|
-
const predictionsFile = await ds.queries.WSImages.getWSIPredictionPatches(sampleId, wsimage.filename);
|
|
62
|
-
const predictionsFilePath = path.join(
|
|
63
|
-
serverconfig.tpmasterdir,
|
|
64
|
-
ds.queries.WSImages.imageBySampleFolder,
|
|
65
|
-
sampleId,
|
|
66
|
-
predictionsFile[0]
|
|
67
|
-
);
|
|
68
|
-
const predictionsData = JSON.parse(fs.readFileSync(predictionsFilePath, "utf8"));
|
|
69
|
-
wsimage.predictions = predictionsData.features.map((d) => {
|
|
70
|
-
const featClass = ds.queries.WSImages?.classes?.find((f) => f.id == d.properties.class)?.label || d.properties.class;
|
|
71
|
-
return {
|
|
72
|
-
zoomCoordinates: d.properties.zoomCoordinates,
|
|
73
|
-
uncertainty: d.properties.uncertainty,
|
|
74
|
-
class: featClass
|
|
75
|
-
};
|
|
76
|
-
}).slice(0, 15);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (ds.queries.WSImages.getWSIPredictionOverlay) {
|
|
81
|
-
for (const wsimage of wsimages) {
|
|
82
|
-
const predictionOverlay = await ds.queries.WSImages.getWSIPredictionOverlay(sampleId, wsimage.filename);
|
|
83
|
-
if (predictionOverlay) {
|
|
84
|
-
wsimage.predictionLayers = [predictionOverlay];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
25
|
res.send({ sampleWSImages: wsimages });
|
|
89
26
|
} catch (e) {
|
|
90
27
|
console.log(e);
|
|
@@ -93,8 +30,7 @@ function init({ genomes }) {
|
|
|
93
30
|
};
|
|
94
31
|
}
|
|
95
32
|
async function validate_query_getSampleWSImages(ds) {
|
|
96
|
-
if (!ds.queries?.WSImages)
|
|
97
|
-
return;
|
|
33
|
+
if (!ds.queries?.WSImages) return;
|
|
98
34
|
validateQuery(ds);
|
|
99
35
|
}
|
|
100
36
|
function validateQuery(ds) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { saveWSIAnnotationPayload } from "#types/checkers";
|
|
2
|
+
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
|
+
const routePath = "saveWSIAnnotation";
|
|
4
|
+
const api = {
|
|
5
|
+
endpoint: `${routePath}`,
|
|
6
|
+
methods: {
|
|
7
|
+
post: {
|
|
8
|
+
...saveWSIAnnotationPayload,
|
|
9
|
+
init
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function init({ genomes }) {
|
|
14
|
+
return async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const query = req.query;
|
|
17
|
+
if (!query.genome) throw new Error(".genome is required for deleteWSIAnnotation request.");
|
|
18
|
+
if (!query.dslabel) throw new Error(".dslabel is required for deleteWSIAnnotation request.");
|
|
19
|
+
const g = genomes[query.genome];
|
|
20
|
+
if (!g) throw new Error("invalid genome name");
|
|
21
|
+
const ds = g.datasets[query.dslabel];
|
|
22
|
+
if (!ds) throw new Error("invalid dataset name");
|
|
23
|
+
if (typeof ds.queries?.WSImages?.saveWSIAnnotation === "function") {
|
|
24
|
+
const result = await ds.queries.WSImages.saveWSIAnnotation(query);
|
|
25
|
+
if (result?.status === "error") {
|
|
26
|
+
return res.status(500).send(result);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
res.status(200).send({ status: "ok" });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn(e);
|
|
32
|
+
res.status(500).send({
|
|
33
|
+
status: "error",
|
|
34
|
+
error: e?.message || String(e)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function validate_query_saveWSIAnnotation(ds) {
|
|
40
|
+
if (!ds.queries?.WSImages?.db) return;
|
|
41
|
+
const connection = getDbConnection(ds);
|
|
42
|
+
if (!connection) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
validateQuery(ds, connection);
|
|
46
|
+
}
|
|
47
|
+
function validateQuery(ds, connection) {
|
|
48
|
+
ds.queries.WSImages.saveWSIAnnotation = async (annotation) => {
|
|
49
|
+
try {
|
|
50
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
51
|
+
const projectId = annotation.projectId;
|
|
52
|
+
const wsimageFilename = annotation.wsimage;
|
|
53
|
+
const coords = JSON.stringify(annotation.coordinates ?? []);
|
|
54
|
+
const status = 1;
|
|
55
|
+
const classId = annotation.classId;
|
|
56
|
+
if (projectId == null || wsimageFilename == null) {
|
|
57
|
+
return {
|
|
58
|
+
status: "error",
|
|
59
|
+
error: "Missing required fields: projectId and wsimage."
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const getImageIdSql = `
|
|
63
|
+
SELECT id
|
|
64
|
+
FROM project_images
|
|
65
|
+
WHERE project_id = ?
|
|
66
|
+
AND image_path = ?
|
|
67
|
+
LIMIT 1
|
|
68
|
+
`;
|
|
69
|
+
const getImageStmt = connection.prepare(getImageIdSql);
|
|
70
|
+
const imageRow = getImageStmt.get(projectId, wsimageFilename);
|
|
71
|
+
if (!imageRow?.id) {
|
|
72
|
+
return {
|
|
73
|
+
status: "error",
|
|
74
|
+
error: `Image not found for project_id=${projectId} and image_path="${wsimageFilename}".`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const imageId = imageRow.id;
|
|
78
|
+
const insertSql = `
|
|
79
|
+
INSERT INTO project_annotations (
|
|
80
|
+
project_id, user_id, coordinates, timestamp, status, class_id, image_id
|
|
81
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
`;
|
|
83
|
+
const insertStmt = connection.prepare(insertSql);
|
|
84
|
+
const userRow = connection.prepare(`SELECT id FROM project_users WHERE project_id = ? ORDER BY id ASC LIMIT 1`).get(projectId);
|
|
85
|
+
const userId = userRow?.id;
|
|
86
|
+
insertStmt.run(projectId, userId, coords, timestamp, status, classId, imageId);
|
|
87
|
+
return { status: "ok" };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Error saving annotation:", error);
|
|
90
|
+
return {
|
|
91
|
+
status: "error",
|
|
92
|
+
error: error?.message || "Failed to save annotation"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
api,
|
|
99
|
+
validate_query_saveWSIAnnotation
|
|
100
|
+
};
|
package/routes/snp.js
CHANGED
|
@@ -22,28 +22,22 @@ function init({ genomes }) {
|
|
|
22
22
|
try {
|
|
23
23
|
const q = req.query;
|
|
24
24
|
const n = q.genome;
|
|
25
|
-
if (!n)
|
|
26
|
-
throw "no genome";
|
|
25
|
+
if (!n) throw "no genome";
|
|
27
26
|
const results = await searchSNP(q, genomes[n]);
|
|
28
27
|
res.send({ results });
|
|
29
28
|
} catch (e) {
|
|
30
|
-
if (e.stack)
|
|
31
|
-
console.log(e.stack);
|
|
29
|
+
if (e.stack) console.log(e.stack);
|
|
32
30
|
return res.send({ error: e.message || e });
|
|
33
31
|
}
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
34
|
async function searchSNP(q, genome) {
|
|
37
|
-
if (!genome)
|
|
38
|
-
|
|
39
|
-
if (!genome.snp)
|
|
40
|
-
throw "snp is not configured for this genome";
|
|
35
|
+
if (!genome) throw "invalid genome";
|
|
36
|
+
if (!genome.snp) throw "snp is not configured for this genome";
|
|
41
37
|
const hits = [];
|
|
42
38
|
if (q.byCoord) {
|
|
43
|
-
if (genome.genomicNameRegexp.test(q.chr))
|
|
44
|
-
|
|
45
|
-
if (!Array.isArray(q.ranges))
|
|
46
|
-
throw "ranges not an array";
|
|
39
|
+
if (genome.genomicNameRegexp.test(q.chr)) throw "invalid chr name";
|
|
40
|
+
if (!Array.isArray(q.ranges)) throw "ranges not an array";
|
|
47
41
|
for (const r of q.ranges) {
|
|
48
42
|
if (!Number.isInteger(r.start) || !Number.isInteger(r.stop) || r.start < 0 || r.stop < r.start)
|
|
49
43
|
throw "invalid start/stop";
|
|
@@ -61,18 +55,15 @@ async function searchSNP(q, genome) {
|
|
|
61
55
|
break;
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
|
-
if (missing)
|
|
65
|
-
continue;
|
|
58
|
+
if (missing) continue;
|
|
66
59
|
}
|
|
67
60
|
hits.push(hit);
|
|
68
61
|
}
|
|
69
62
|
}
|
|
70
63
|
} else if (q.byName) {
|
|
71
|
-
if (!Array.isArray(q.lst))
|
|
72
|
-
throw ".lst[] missing";
|
|
64
|
+
if (!Array.isArray(q.lst)) throw ".lst[] missing";
|
|
73
65
|
for (const n of q.lst) {
|
|
74
|
-
if (genome.genomicNameRegexp.test(n))
|
|
75
|
-
continue;
|
|
66
|
+
if (genome.genomicNameRegexp.test(n)) continue;
|
|
76
67
|
const snps = await utils.query_bigbed_by_name(genome.snp.bigbedfile, n);
|
|
77
68
|
for (const snp of snps) {
|
|
78
69
|
const hit = snp2hit(snp);
|