@sjcrh/proteinpaint-server 2.71.0 → 2.73.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.71.0",
3
+ "version": "2.73.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",
@@ -44,6 +44,7 @@
44
44
  "@babel/preset-typescript": "^7.21.4",
45
45
  "@babel/register": "^7.14.5",
46
46
  "@types/node": "^20.11.24",
47
+ "@types/tough-cookie": "^4.0.5",
47
48
  "@typescript-eslint/eslint-plugin": "^5.60.0",
48
49
  "babel-loader": "^8.2.2",
49
50
  "esbuild": "^0.19.12",
@@ -61,7 +62,7 @@
61
62
  },
62
63
  "dependencies": {
63
64
  "@sjcrh/augen": "2.46.0",
64
- "@sjcrh/proteinpaint-rust": "2.61.1",
65
+ "@sjcrh/proteinpaint-rust": "2.73.0",
65
66
  "better-sqlite3": "^9.4.1",
66
67
  "body-parser": "^1.15.2",
67
68
  "canvas": "~2.11.2",
@@ -82,6 +83,7 @@
82
83
  "node-fetch": "^2.6.1",
83
84
  "partjson": "^0.58.2",
84
85
  "tiny-async-pool": "^1.2.0",
86
+ "tough-cookie": "^4.1.4",
85
87
  "typedoc-plugin-missing-exports": "^2.0.1"
86
88
  },
87
89
  "repository": {
@@ -0,0 +1,51 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import serverconfig from "#src/serverconfig.js";
4
+ const api = {
5
+ endpoint: "samplewsimages",
6
+ methods: {
7
+ get: {
8
+ init,
9
+ request: {
10
+ typeId: "GetSampleWSImagesRequest"
11
+ },
12
+ response: {
13
+ typeId: "GetSampleWSImagesResponse"
14
+ }
15
+ },
16
+ post: {
17
+ alternativeFor: "get",
18
+ init
19
+ }
20
+ }
21
+ };
22
+ function init({ genomes }) {
23
+ return async (req, res) => {
24
+ try {
25
+ const query = req.query;
26
+ const g = genomes[query.genome];
27
+ if (!g)
28
+ throw "invalid genome name";
29
+ const ds = g.datasets[query.dslabel];
30
+ if (!ds)
31
+ throw "invalid dataset name";
32
+ const sampleId = query.sample_id;
33
+ const sampleWSImagesPath = path.join(
34
+ `${serverconfig.tpmasterdir}/${ds.queries.WSImages.imageBySampleFolder}`,
35
+ sampleId
36
+ );
37
+ const sampleWSImages = await getWSImages(sampleWSImagesPath);
38
+ res.send({ sampleWSImages });
39
+ } catch (e) {
40
+ console.log(e);
41
+ res.status(404).send("Sample images not found");
42
+ }
43
+ };
44
+ }
45
+ async function getWSImages(sampleImagesPath) {
46
+ const files = await fs.promises.readdir(sampleImagesPath);
47
+ return files.filter((file) => [".svs", ".mrxs", ".scn", ".ndpi", ".tiff"].includes(path.extname(file)));
48
+ }
49
+ export {
50
+ api
51
+ };
@@ -92,7 +92,7 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
92
92
  if (data.error)
93
93
  throw data.error;
94
94
  const lst = [];
95
- if (q.tw.term.type == "geneVariant" && !q.tw.q.groupsetting?.inuse) {
95
+ if (q.tw.term.type == "geneVariant" && q.tw.q.type != "predefined-groupset" && q.tw.q.type != "custom-groupset") {
96
96
  const samples = data.samples;
97
97
  const dtClassMap = /* @__PURE__ */ new Map();
98
98
  if (ds.assayAvailability?.byDt) {
@@ -192,11 +192,10 @@ async function validateNative(q, ds, genome) {
192
192
  }
193
193
  }
194
194
  const term2sample2value = /* @__PURE__ */ new Map();
195
- for (const g of param.terms) {
196
- const geneTerm = g;
195
+ for (const geneTerm of param.terms) {
197
196
  if (!geneTerm.gene)
198
197
  continue;
199
- if (!geneTerm.chr) {
198
+ if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop)) {
200
199
  const re = getResultGene(genome, { input: geneTerm.gene, deep: 1 });
201
200
  if (!re.gmlst || re.gmlst.length == 0) {
202
201
  console.warn("unknown gene:" + geneTerm.gene);
@@ -208,12 +207,13 @@ async function validateNative(q, ds, genome) {
208
207
  geneTerm.chr = i.chr;
209
208
  }
210
209
  const s2v = {};
210
+ if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop))
211
+ throw "missing chr/start/stop";
211
212
  await utils.get_lines_bigfile({
212
213
  args: [
213
214
  q.file,
214
- (q.nochr ? geneTerm.chr?.replace("chr", "") : geneTerm.chr) + ":" + geneTerm.start + "-" + geneTerm.stop
215
+ (q.nochr ? geneTerm.chr.replace("chr", "") : geneTerm.chr) + ":" + geneTerm.start + "-" + geneTerm.stop
215
216
  ],
216
- // must do g.chr?.replace to avoid tsc error
217
217
  callback: (line) => {
218
218
  const l = line.split(" ");
219
219
  if (l[3].toLowerCase() != geneTerm.gene.toLowerCase())
@@ -177,11 +177,16 @@ function addNonDictionaryQueries(c, ds, genome) {
177
177
  q2.NIdata[k] = JSON.parse(JSON.stringify(q.NIdata[k]));
178
178
  }
179
179
  }
180
- if (q.DZImages) {
180
+ if (q.DZImages && serverconfig.features.showDZImages) {
181
181
  q2.DZImages = {
182
182
  type: q.DZImages.type
183
183
  };
184
184
  }
185
+ if (q.WSImages && serverconfig.features.showWSImages) {
186
+ q2.WSImages = {
187
+ type: q.WSImages.type
188
+ };
189
+ }
185
190
  if (q.singleSampleGbtk) {
186
191
  q2.singleSampleGbtk = {};
187
192
  for (const k in q.singleSampleGbtk) {
@@ -84,7 +84,11 @@ function init({ genomes }) {
84
84
  }
85
85
  values.push(Number(value));
86
86
  }
87
- result = Summarystats(values);
87
+ if (values.length) {
88
+ result = Summarystats(values);
89
+ } else {
90
+ result = {};
91
+ }
88
92
  } catch (e) {
89
93
  if (e instanceof Error && e.stack)
90
94
  console.log(e);
@@ -45,7 +45,7 @@ async function trigger_gettermsbyid(q, res, tdb) {
45
45
  for (const id of q.ids) {
46
46
  const term = tdb.q.termjsonByOneid(id);
47
47
  if (term) {
48
- if (term.type == "categorical" && !term.values && !term.groupsetting?.inuse) {
48
+ if (term.type == "categorical" && !term.values) {
49
49
  term.values = {};
50
50
  term.samplecount = {};
51
51
  }
@@ -1,9 +1,9 @@
1
1
  import path from "path";
2
2
  import { run_rust } from "@sjcrh/proteinpaint-rust";
3
- import got from "got";
4
3
  import serverconfig from "#src/serverconfig.js";
5
4
  import { get_samples } from "#src/termdb.sql.js";
6
5
  import { makeFilter } from "#src/mds3.gdc.js";
6
+ import { cachedFetch } from "#src/utils.js";
7
7
  const api = {
8
8
  endpoint: "termdb/topVariablyExpressedGenes",
9
9
  methods: {
@@ -61,6 +61,7 @@ function nativeValidateQuery(ds) {
61
61
  throw "topVariablyExpressedGenes query given but geneExpression missing";
62
62
  if (gE.src != "native")
63
63
  throw "topVariablyExpressedGenes is native but geneExpression.src is not native";
64
+ addTopVEarg(ds.queries.topVariablyExpressedGenes);
64
65
  ds.queries.topVariablyExpressedGenes.getGenes = async (q) => {
65
66
  const samples = [];
66
67
  if (q.filter) {
@@ -81,18 +82,81 @@ function nativeValidateQuery(ds) {
81
82
  samples.push(n);
82
83
  }
83
84
  }
84
- const genes = await computeGenes4nativeDs(q, ds, gE.file, samples);
85
+ const genes = await computeGenes4nativeDs(q, gE.file, samples);
85
86
  return genes;
86
87
  };
87
88
  }
88
- async function computeGenes4nativeDs(q, ds, matrixFile, samples) {
89
+ function addTopVEarg(q) {
90
+ const arglst = [
91
+ { id: "maxGenes", label: "Gene Count", type: "number", value: 100 },
92
+ {
93
+ id: "filter_extreme_values",
94
+ label: "Filter Extreme Values",
95
+ type: "boolean",
96
+ value: true,
97
+ options: [
98
+ {
99
+ id: "min_count",
100
+ label: "Min count",
101
+ type: "number",
102
+ value: 10
103
+ },
104
+ {
105
+ id: "min_total_count",
106
+ label: "Min total count",
107
+ type: "number",
108
+ value: 15
109
+ }
110
+ ]
111
+ },
112
+ {
113
+ id: "rank_type",
114
+ label: "Rank by:",
115
+ type: "radio",
116
+ options: [
117
+ /** The param option in input JSON is very important.
118
+ * It instructs what method will be used to calculate variation in the counts for a particular gene.
119
+ * It supports variance as well as interquartile region.
120
+ * This is based on the recommendation of this article:
121
+ * https://www.frontiersin.org/articles/10.3389/fgene.2021.632620/full.
122
+ * This article recommends using interquartile region over variance.*/
123
+ {
124
+ type: "boolean",
125
+ label: "Variance",
126
+ value: "var"
127
+ },
128
+ {
129
+ type: "boolean",
130
+ label: "Interquartile Region",
131
+ value: "iqr"
132
+ }
133
+ ]
134
+ }
135
+ ];
136
+ if (q.arguments) {
137
+ for (const a of q.arguments) {
138
+ if (!a.id)
139
+ throw "missing id of topVE.arguments[]";
140
+ const item = arglst.find((i) => i.id == a.id);
141
+ if (!item)
142
+ throw "unknown id of topVE.arguments[]";
143
+ Object.assign(item, a);
144
+ }
145
+ }
146
+ q.arguments = arglst;
147
+ }
148
+ async function computeGenes4nativeDs(q, matrixFile, samples) {
89
149
  const input_json = {
90
150
  input_file: matrixFile,
91
151
  samples: samples.join(","),
92
- filter_extreme_values: true,
152
+ filter_extreme_values: q.filter_extreme_values,
93
153
  num_genes: q.maxGenes,
94
- param: "var"
154
+ rank_type: q.rank_type?.type
95
155
  };
156
+ if (q.filter_extreme_values == 1) {
157
+ input_json["min_count"] = q.min_count;
158
+ input_json["min_total_count"] = q.min_total_count;
159
+ }
96
160
  const rust_output = await run_rust("topGeneByExpressionVariance", JSON.stringify(input_json));
97
161
  const rust_output_list = rust_output.split("\n");
98
162
  let output_json;
@@ -109,7 +173,7 @@ async function computeGenes4nativeDs(q, ds, matrixFile, samples) {
109
173
  function gdcValidateQuery(ds, genome) {
110
174
  ds.queries.topVariablyExpressedGenes.getGenes = async (q) => {
111
175
  if (serverconfig.features.gdcGenes) {
112
- console.log(
176
+ console.error(
113
177
  "!!GDC!! using serverconfig.features.gdcGenes[] but not live api query. only use this on DEV and never on PROD!"
114
178
  );
115
179
  return serverconfig.features.gdcGenes;
@@ -120,11 +184,12 @@ function gdcValidateQuery(ds, genome) {
120
184
  const { host, headers } = ds.getHostHeaders(q);
121
185
  const url = path.join(host.geneExp, "/gene_expression/gene_selection");
122
186
  try {
123
- const response = await got.post(url, {
187
+ const response = await cachedFetch(url, {
188
+ method: "POST",
124
189
  headers,
125
- body: JSON.stringify(getGeneSelectionArg(q))
190
+ body: getGeneSelectionArg(q)
126
191
  });
127
- const re = JSON.parse(response.body);
192
+ const re = response.body;
128
193
  const genes = [];
129
194
  if (!Array.isArray(re.gene_selection))
130
195
  throw "re.gene_selection[] is not array";
@@ -141,7 +206,7 @@ function gdcValidateQuery(ds, genome) {
141
206
  }
142
207
  return genes;
143
208
  } catch (e) {
144
- console.log(e.stack || e);
209
+ console.error(e.stack || e);
145
210
  throw e;
146
211
  }
147
212
  };
@@ -0,0 +1,42 @@
1
+ import ky from "ky";
2
+ import serverconfig from "#src/serverconfig.js";
3
+ const routePath = "tileserver";
4
+ const api = {
5
+ endpoint: `${routePath}/layer/slide/:sampleId/zoomify/:TileGroup/:z-:x-:y@1x.jpg`,
6
+ methods: {
7
+ get: {
8
+ init,
9
+ request: {
10
+ typeId: "any"
11
+ },
12
+ response: {
13
+ typeId: "any"
14
+ }
15
+ },
16
+ post: {
17
+ alternativeFor: "get",
18
+ init
19
+ }
20
+ }
21
+ };
22
+ function init() {
23
+ return async (req, res) => {
24
+ try {
25
+ const { sampleId, TileGroup, z, x, y } = req.params;
26
+ const url = `${serverconfig.tileServerURL}/tileserver/layer/slide/${sampleId}/zoomify/${TileGroup}/${z}-${x}-${y}@1x.jpg`;
27
+ const response = await ky.get(url);
28
+ const buffer = await response.arrayBuffer();
29
+ res.status(response.status).send(Buffer.from(buffer));
30
+ } catch (error) {
31
+ if (error.response) {
32
+ const errorBody = await error.response.arrayBuffer();
33
+ res.status(error.response.status).send(Buffer.from(errorBody));
34
+ } else {
35
+ res.status(500).send("Internal Server Error");
36
+ }
37
+ }
38
+ };
39
+ }
40
+ export {
41
+ api
42
+ };
@@ -0,0 +1,115 @@
1
+ import ky from "ky";
2
+ import qs from "qs";
3
+ import path from "path";
4
+ import serverconfig from "#src/serverconfig.js";
5
+ import { CookieJar } from "tough-cookie";
6
+ import { promisify } from "util";
7
+ const routePath = "wsimages";
8
+ const api = {
9
+ endpoint: `${routePath}`,
10
+ methods: {
11
+ get: {
12
+ init,
13
+ request: {
14
+ typeId: "any"
15
+ },
16
+ response: {
17
+ typeId: "any"
18
+ }
19
+ },
20
+ post: {
21
+ alternativeFor: "get",
22
+ init
23
+ }
24
+ }
25
+ };
26
+ function init({ genomes }) {
27
+ return async (req, res) => {
28
+ try {
29
+ const g = genomes[req.query.genome];
30
+ if (!g)
31
+ throw "invalid genome name";
32
+ const ds = g.datasets[req.query.dslabel];
33
+ if (!ds)
34
+ throw "invalid dataset name";
35
+ const sampleId = req.query.sampleId;
36
+ if (!sampleId)
37
+ throw "invalid sampleId";
38
+ const wsimage = req.query.wsimage;
39
+ if (!wsimage)
40
+ throw "invalid wsimage";
41
+ const cookieJar = new CookieJar();
42
+ const setCookie = promisify(cookieJar.setCookie.bind(cookieJar));
43
+ const getCookieString = promisify(cookieJar.getCookieString.bind(cookieJar));
44
+ await ky.get(`${serverconfig.tileServerURL}/tileserver/session_id`, {
45
+ hooks: {
46
+ beforeRequest: [
47
+ async (request) => {
48
+ const cookie = await getCookieString(request.url);
49
+ request.headers.set("Cookie", cookie);
50
+ }
51
+ ],
52
+ afterResponse: [
53
+ async (request, options, response) => {
54
+ const setCookieHeader = response.headers.get("set-cookie");
55
+ if (setCookieHeader) {
56
+ await setCookie(setCookieHeader, request.url);
57
+ }
58
+ }
59
+ ]
60
+ }
61
+ });
62
+ const cookieString = await getCookieString(`${serverconfig.tileServerURL}/tileserver/session_id`);
63
+ const sessionId = cookieString.match(/session_id=([^;]*)/)?.[1];
64
+ const sampleWsiTileServer = path.join(
65
+ `${serverconfig.tileServerMount}/${ds.queries.WSImages.imageBySampleFolder}/${sampleId}`,
66
+ wsimage
67
+ );
68
+ const data = qs.stringify({ slide_path: sampleWsiTileServer });
69
+ await ky.put(`${serverconfig.tileServerURL}/tileserver/slide`, {
70
+ body: data,
71
+ headers: {
72
+ "Content-Type": "application/x-www-form-urlencoded",
73
+ Cookie: `session_id=${sessionId}`
74
+ // Include the session_id in the headers
75
+ },
76
+ hooks: {
77
+ beforeRequest: [
78
+ async (request) => {
79
+ const cookie = await getCookieString(request.url);
80
+ request.headers.set("Cookie", cookie);
81
+ }
82
+ ],
83
+ afterResponse: [
84
+ async (request, options, response) => {
85
+ const setCookieHeader = response.headers.get("set-cookie");
86
+ if (setCookieHeader) {
87
+ await setCookie(setCookieHeader, request.url);
88
+ }
89
+ }
90
+ ]
91
+ }
92
+ });
93
+ const getWsiImageResponse = await ky.get(`${serverconfig.tileServerURL}/tileserver/slide`, {
94
+ hooks: {
95
+ beforeRequest: [
96
+ async (request) => {
97
+ const cookie = await getCookieString(request.url);
98
+ request.headers.set("Cookie", cookie);
99
+ }
100
+ ]
101
+ }
102
+ }).json();
103
+ res.status(200).json({ sessionId, slide_dimensions: getWsiImageResponse.slide_dimensions });
104
+ } catch (e) {
105
+ console.log(e);
106
+ res.send({
107
+ status: "error",
108
+ error: e.error || e
109
+ });
110
+ }
111
+ };
112
+ }
113
+ export {
114
+ api
115
+ };