@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.
Files changed (65) hide show
  1. package/dataset/protected.test.js +1 -2
  2. package/dataset/termdb.test.js +3 -2
  3. package/package.json +9 -8
  4. package/routes/aiProjectAdmin.js +46 -60
  5. package/routes/aiProjectSelectedWSImages.js +161 -0
  6. package/routes/brainImaging.js +9 -18
  7. package/routes/brainImagingSamples.js +2 -4
  8. package/routes/burden.js +13 -26
  9. package/routes/correlationVolcano.js +18 -36
  10. package/routes/dataset.js +6 -12
  11. package/routes/deleteWSIAnnotation.js +75 -0
  12. package/routes/dsdata.js +7 -14
  13. package/routes/dzimages.js +4 -8
  14. package/routes/gdc.grin2.list.js +13 -26
  15. package/routes/gdc.grin2.run.js +3 -6
  16. package/routes/gdc.maf.js +8 -16
  17. package/routes/gdc.mafBuild.js +14 -28
  18. package/routes/gene2canonicalisoform.js +4 -8
  19. package/routes/genelookup.js +2 -4
  20. package/routes/genesetEnrichment.js +6 -12
  21. package/routes/genesetOverrepresentation.js +1 -2
  22. package/routes/genomes.js +1 -2
  23. package/routes/grin2.js +13 -17
  24. package/routes/healthcheck.js +3 -6
  25. package/routes/hicdata.js +4 -8
  26. package/routes/hicgenome.js +4 -8
  27. package/routes/hicstat.js +2 -4
  28. package/routes/img.js +1 -2
  29. package/routes/isoformlst.js +6 -12
  30. package/routes/ntseq.js +4 -8
  31. package/routes/pdomain.js +5 -10
  32. package/routes/sampledzimages.js +2 -4
  33. package/routes/samplewsimages.js +3 -67
  34. package/routes/saveWSIAnnotation.js +100 -0
  35. package/routes/snp.js +9 -18
  36. package/routes/termdb.DE.js +23 -46
  37. package/routes/termdb.boxplot.js +84 -84
  38. package/routes/termdb.categories.js +9 -18
  39. package/routes/termdb.cluster.js +23 -46
  40. package/routes/termdb.cohort.summary.js +3 -6
  41. package/routes/termdb.cohorts.js +4 -8
  42. package/routes/termdb.config.js +32 -64
  43. package/routes/termdb.descrstats.js +6 -12
  44. package/routes/termdb.filterTermValues.js +4 -8
  45. package/routes/termdb.numericcategories.js +5 -10
  46. package/routes/termdb.percentile.js +6 -12
  47. package/routes/termdb.profileFormScores.js +12 -24
  48. package/routes/termdb.profileScores.js +7 -14
  49. package/routes/termdb.rootterm.js +4 -8
  50. package/routes/termdb.sampleImages.js +4 -8
  51. package/routes/termdb.singleSampleMutation.js +9 -18
  52. package/routes/termdb.singlecellDEgenes.js +4 -8
  53. package/routes/termdb.singlecellData.js +4 -8
  54. package/routes/termdb.singlecellSamples.js +28 -56
  55. package/routes/termdb.termchildren.js +5 -10
  56. package/routes/termdb.termsbyids.js +4 -8
  57. package/routes/termdb.topMutatedGenes.js +15 -30
  58. package/routes/termdb.topTermsByType.js +9 -18
  59. package/routes/termdb.topVariablyExpressedGenes.js +13 -26
  60. package/routes/termdb.violin.js +124 -135
  61. package/routes/tileserver.js +14 -15
  62. package/routes/wsimages.js +42 -46
  63. package/routes/wsisamples.js +3 -6
  64. package/src/app.js +4345 -6708
  65. package/routes/sampleWsiAiApi.js +0 -33
package/routes/genomes.js CHANGED
@@ -48,8 +48,7 @@ function init({ genomes }) {
48
48
  }
49
49
  let hasblat = false;
50
50
  for (const n in genomes) {
51
- if (genomes[n].blat)
52
- hasblat = true;
51
+ if (genomes[n].blat) hasblat = true;
53
52
  }
54
53
  res.send({
55
54
  genomes: hash,
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
- throw new Error("ds missing");
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
  }
@@ -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
- continue;
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
- reject({ error: err });
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 });
@@ -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
- reject({ error: erroutput.join("") });
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 = {
@@ -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
- throw "invalid genome";
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
- console.log(e.stack);
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
- throw "invalid genome";
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
  }
@@ -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}`,
@@ -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
- throw "invalid genome";
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
- throw "invalid chr name";
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);