@sjcrh/proteinpaint-server 2.113.0 → 2.115.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.113.0",
3
+ "version": "2.115.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",
@@ -19,8 +19,9 @@
19
19
  "start": "tsx watch . /start.js",
20
20
  "test:unit": "tsx emitImports.js unit > serverTests.js && c8 tsx serverTests.js && rm -rf ./cache",
21
21
  "precombined:coverage": "tsx emitImports.js unit > serverTests.js",
22
- "combined:coverage": "closeCoverageKey=test c8 --all --src=proteinpaint/server --experimental-monocart -r=v8 -r=html -r=json -r=markdown-summary -r=markdown-details -o=./.coverage tsx ./coverage.js & ",
22
+ "combined:coverage": "coverageKey=test c8 --all --src=proteinpaint/server --experimental-monocart -r=v8 -r=html -r=json -r=markdown-summary -r=markdown-details -o=./.coverage tsx ./coverage.js & ",
23
23
  "postcombined:coverage": "rm -rf ./cache",
24
+ "spec:coverage": "tsx test/relevant.js",
24
25
  "getconf": "../build/getConfigProp.js",
25
26
  "doc": "../augen/build.sh routes shared/types/routes shared/checkers ../public/docs/server",
26
27
  "mjs": "esbuild \"$DIR/*.ts\" --platform=node --outdir=\"$DIR\" --format=esm",
@@ -64,10 +65,10 @@
64
65
  "typescript": "^5.6.3"
65
66
  },
66
67
  "dependencies": {
67
- "@sjcrh/augen": "2.113.0",
68
- "@sjcrh/proteinpaint-rust": "2.112.0",
69
- "@sjcrh/proteinpaint-shared": "2.113.0",
70
- "@sjcrh/proteinpaint-types": "2.113.0",
68
+ "@sjcrh/augen": "2.115.0",
69
+ "@sjcrh/proteinpaint-rust": "2.114.0",
70
+ "@sjcrh/proteinpaint-shared": "2.115.0",
71
+ "@sjcrh/proteinpaint-types": "2.115.0",
71
72
  "@types/express": "^5.0.0",
72
73
  "@types/express-session": "^1.18.1",
73
74
  "better-sqlite3": "^9.4.1",
@@ -5,6 +5,7 @@ import serverconfig from "../src/serverconfig.js";
5
5
  import { mayLog } from "#src/helpers.ts";
6
6
  import path from "path";
7
7
  import { getStdDev } from "#shared/descriptive.stats.js";
8
+ import { formatElapsedTime } from "#shared/time.js";
8
9
  const minArrayLength = 3;
9
10
  const minSD = 0.05;
10
11
  const api = {
@@ -92,7 +93,9 @@ async function compute(q, ds, genome) {
92
93
  const output = {
93
94
  terms: JSON.parse(await run_R(path.join(serverconfig.binpath, "utils", "corr.R"), JSON.stringify(input)))
94
95
  };
95
- mayLog("Time taken to run correlation analysis:", Date.now() - time1);
96
+ const elapsedMs = Date.now() - time1;
97
+ const formattedTime = formatElapsedTime(elapsedMs);
98
+ mayLog("Time taken to run correlation analysis:", formattedTime);
96
99
  for (const t of output.terms) {
97
100
  const t2 = {
98
101
  tw$id: t.id,
@@ -23,18 +23,7 @@ function init({ genomes }) {
23
23
  if (!ds)
24
24
  throw "invalid dataset name";
25
25
  const sampleId = query.sample_id;
26
- if (ds.queries.WSImages.sources) {
27
- const images2 = [];
28
- if (ds.queries.WSImages.sources) {
29
- images2.push({
30
- filename: sampleId + "_fsspec.json",
31
- metadata: ""
32
- });
33
- }
34
- res.send({ sampleWSImages: images2 });
35
- return;
36
- }
37
- const images = await ds.queries.WSImages.getWSImages({ sampleId });
26
+ const images = await ds.queries.WSImages.getWSImages(sampleId);
38
27
  res.send({ sampleWSImages: images });
39
28
  } catch (e) {
40
29
  console.log(e);
@@ -42,32 +31,31 @@ function init({ genomes }) {
42
31
  }
43
32
  };
44
33
  }
45
- function validate_query_getSampleWSImages(ds) {
46
- const q = ds.queries?.WSImages;
47
- if (!q)
34
+ async function validate_query_getSampleWSImages(ds) {
35
+ if (!ds.queries?.WSImages)
48
36
  return;
49
- nativeValidateQuery(ds);
37
+ validateQuery(ds);
50
38
  }
51
- function nativeValidateQuery(ds) {
52
- ds.queries.WSImages.getWSImages = async (q) => {
53
- return await getWSImages(ds, q.sampleId);
54
- };
55
- }
56
- async function getWSImages(ds, sampleName) {
57
- const sql = `SELECT wsimages.filename as filename, wsimages.metadata as metadata
58
- FROM wsimages
59
- INNER JOIN sampleidmap
60
- ON wsimages.sample = sampleidmap.id
61
- WHERE sampleidmap.name = ?`;
62
- const rows = ds.cohort.db.connection.prepare(sql).all(sampleName);
63
- const images = [];
64
- for (const row of rows) {
65
- images.push({
66
- filename: row.filename,
67
- metadata: row.metadata
68
- });
39
+ function validateQuery(ds) {
40
+ if (typeof ds.queries.WSImages.getWSImages == "function") {
41
+ return;
69
42
  }
70
- return images;
43
+ ds.queries.WSImages.getWSImages = async (sampleName) => {
44
+ const sql = `SELECT wsimages.filename as filename, wsimages.metadata as metadata
45
+ FROM wsimages
46
+ INNER JOIN sampleidmap
47
+ ON wsimages.sample = sampleidmap.id
48
+ WHERE sampleidmap.name = ?`;
49
+ const rows = ds.cohort.db.connection.prepare(sql).all(sampleName);
50
+ const images = [];
51
+ for (const row of rows) {
52
+ images.push({
53
+ filename: row.filename,
54
+ metadata: row.metadata
55
+ });
56
+ }
57
+ return images;
58
+ };
71
59
  }
72
60
  export {
73
61
  api,
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import run_R from "#src/run_R.js";
3
+ import { run_rust } from "@sjcrh/proteinpaint-rust";
3
4
  import { termdbClusterPayload } from "#types/checkers";
4
5
  import * as utils from "#src/utils.js";
5
6
  import serverconfig from "#src/serverconfig.js";
@@ -223,92 +224,199 @@ async function validate_query_geneExpression(ds, genome) {
223
224
  }
224
225
  throw "unknown queries.geneExpression.src";
225
226
  }
227
+ async function validateHDF5File(filePath) {
228
+ try {
229
+ const jsonInput = JSON.stringify({
230
+ hdf5_file: filePath
231
+ });
232
+ const result = await run_rust("validateHDF5", jsonInput);
233
+ return JSON.parse(result);
234
+ } catch (error) {
235
+ console.error(`Error validating file: ${error}`);
236
+ return {
237
+ status: "error",
238
+ message: `Validation error`
239
+ };
240
+ }
241
+ }
242
+ async function queryGeneExpression(hdf5_file, geneName) {
243
+ const jsonInput = JSON.stringify({
244
+ hdf5_file,
245
+ gene: geneName
246
+ });
247
+ try {
248
+ const result = await run_rust("readHDF5", jsonInput);
249
+ if (!result || Object.keys(result).length === 0) {
250
+ throw new Error("Failed to retrieve expression data: Empty or missing response");
251
+ }
252
+ return result;
253
+ } catch (error) {
254
+ console.error(`Error querying gene expression for ${geneName}`);
255
+ throw error;
256
+ }
257
+ }
226
258
  async function validateNative(q, ds, genome) {
227
- if (!q.file.startsWith(serverconfig.tpmasterdir))
259
+ if (q.hdf5File === true) {
228
260
  q.file = path.join(serverconfig.tpmasterdir, q.file);
229
- if (!q.samples)
230
261
  q.samples = [];
231
- await utils.validate_tabixfile(q.file);
232
- q.nochr = await utils.tabix_is_nochr(q.file, null, genome);
233
- q.samples = [];
234
- {
235
- const lines = await utils.get_header_tabix(q.file);
236
- if (!lines[0])
237
- throw "header line missing from " + q.file;
238
- const l = lines[0].split(" ");
239
- if (l.slice(0, 4).join(" ") != "#chr start stop gene")
240
- throw "header line has wrong content for columns 1-4";
241
- for (let i = 4; i < l.length; i++) {
242
- const id = ds.cohort.termdb.q.sampleName2id(l[i]);
243
- if (id == void 0)
244
- throw "queries.geneExpression: unknown sample from header: " + l[i];
245
- q.samples.push(id);
246
- }
247
- }
248
- q.get = async (param) => {
249
- const limitSamples = await mayLimitSamples(param, q.samples, ds);
250
- if (limitSamples?.size == 0) {
251
- return { term2sample2value: /* @__PURE__ */ new Map(), byTermId: {}, bySampleId: {} };
252
- }
253
- const bySampleId = {};
254
- const samples = q.samples || [];
255
- if (limitSamples) {
256
- for (const sid of limitSamples) {
257
- bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
258
- }
259
- } else {
260
- for (const sid of samples) {
261
- bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
262
+ await utils.file_is_readable(q.file);
263
+ try {
264
+ const vr = await validateHDF5File(q.file);
265
+ if (vr.status !== "success")
266
+ throw vr.message;
267
+ if (!vr.sampleNames?.length)
268
+ throw "HDF5 file has no samples";
269
+ for (const sn of vr.sampleNames) {
270
+ const si = ds.cohort.termdb.q.sampleName2id(sn);
271
+ if (si == void 0)
272
+ throw "unknown sample from HDF5: " + sn;
273
+ q.samples.push(si);
262
274
  }
275
+ console.log(`${ds.label}: HDF5 file validated. Format: ${vr.format}, Samples:`, vr.sampleNames.length);
276
+ } catch (error) {
277
+ throw `${ds.label}: Failed to validate HDF5 file: ${error}`;
263
278
  }
264
- const term2sample2value = /* @__PURE__ */ new Map();
265
- for (const geneTerm of param.terms) {
266
- if (!geneTerm.gene)
267
- continue;
268
- if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop)) {
269
- const re = getResultGene(genome, { input: geneTerm.gene, deep: 1 });
270
- if (!re.gmlst || re.gmlst.length == 0) {
271
- console.warn("unknown gene:" + geneTerm.gene);
272
- continue;
279
+ q.get = async (param) => {
280
+ const limitSamples = await mayLimitSamples(param, q.samples, ds);
281
+ if (limitSamples?.size == 0) {
282
+ return { term2sample2value: /* @__PURE__ */ new Map(), byTermId: {}, bySampleId: {} };
283
+ }
284
+ const bySampleId = {};
285
+ const samples = q.samples || [];
286
+ if (limitSamples) {
287
+ for (const sid of limitSamples) {
288
+ bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
289
+ }
290
+ } else {
291
+ for (const sid of samples) {
292
+ bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
273
293
  }
274
- const i = re.gmlst.find((i2) => i2.isdefault) || re.gmlst[0];
275
- geneTerm.start = i.start;
276
- geneTerm.stop = i.stop;
277
- geneTerm.chr = i.chr;
278
294
  }
279
- const s2v = {};
280
- if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop))
281
- throw "missing chr/start/stop";
282
- await utils.get_lines_bigfile({
283
- args: [
284
- q.file,
285
- (q.nochr ? geneTerm.chr.replace("chr", "") : geneTerm.chr) + ":" + geneTerm.start + "-" + geneTerm.stop
286
- ],
287
- callback: (line) => {
288
- const l = line.split(" ");
289
- if (l[3].toLowerCase() != geneTerm.gene.toLowerCase())
290
- return;
291
- for (let i = 4; i < l.length; i++) {
292
- const sampleId = samples[i - 4];
293
- if (limitSamples && !limitSamples.has(sampleId))
295
+ const term2sample2value = /* @__PURE__ */ new Map();
296
+ const byTermId = {};
297
+ for (const geneTerm of param.terms) {
298
+ if (!geneTerm.gene)
299
+ continue;
300
+ try {
301
+ const geneQuery = await queryGeneExpression(q.file, geneTerm.gene);
302
+ const geneData = JSON.parse(geneQuery);
303
+ const samplesData = geneData.samples || {};
304
+ const s2v = {};
305
+ for (const [sampleName, value] of Object.entries(samplesData)) {
306
+ const sampleId = ds.cohort.termdb.q.sampleName2id(sampleName);
307
+ if (!sampleId)
294
308
  continue;
295
- if (!l[i])
309
+ if (limitSamples && !limitSamples.has(sampleId))
296
310
  continue;
297
- const v = Number(l[i]);
298
- if (Number.isNaN(v))
299
- throw "exp value not number";
300
- s2v[sampleId] = v;
311
+ s2v[sampleId] = Number(value);
312
+ }
313
+ console.log(`Gene ${geneTerm.gene} has ${Object.keys(s2v).length} samples with data`);
314
+ if (Object.keys(s2v).length) {
315
+ term2sample2value.set(geneTerm.gene, s2v);
301
316
  }
317
+ } catch (error) {
318
+ console.warn(`Error processing gene ${geneTerm.gene}:`, error);
319
+ continue;
320
+ }
321
+ }
322
+ if (term2sample2value.size == 0) {
323
+ throw "No data available for the input " + param.terms?.map((g) => g.gene).join(", ");
324
+ }
325
+ return { term2sample2value, byTermId, bySampleId };
326
+ };
327
+ } else {
328
+ if (!q.file.startsWith(serverconfig.tpmasterdir)) {
329
+ q.file = path.join(serverconfig.tpmasterdir, q.file);
330
+ }
331
+ if (!q.samples)
332
+ q.samples = [];
333
+ await utils.validate_tabixfile(q.file);
334
+ q.nochr = await utils.tabix_is_nochr(q.file, null, genome);
335
+ q.samples = [];
336
+ {
337
+ const lines = await utils.get_header_tabix(q.file);
338
+ if (!lines[0])
339
+ throw "Header line missing from " + q.file;
340
+ const l = lines[0].split(" ");
341
+ if (l.slice(0, 4).join(" ") != "#chr start stop gene") {
342
+ throw "Header line has wrong content for columns 1-4";
343
+ }
344
+ for (let i = 4; i < l.length; i++) {
345
+ const id = ds.cohort.termdb.q.sampleName2id(l[i]);
346
+ if (id == void 0) {
347
+ throw "queries.geneExpression: unknown sample from header: " + l[i];
302
348
  }
303
- });
304
- if (Object.keys(s2v).length)
305
- term2sample2value.set(geneTerm.gene, s2v);
349
+ q.samples.push(id);
350
+ }
306
351
  }
307
- const byTermId = {};
308
- if (term2sample2value.size == 0)
309
- throw "no data available for the input " + param.terms?.map((g) => g.gene).join(", ");
310
- return { term2sample2value, byTermId, bySampleId };
311
- };
352
+ q.get = async (param) => {
353
+ const limitSamples = await mayLimitSamples(param, q.samples, ds);
354
+ if (limitSamples?.size == 0) {
355
+ return { term2sample2value: /* @__PURE__ */ new Map(), byTermId: {}, bySampleId: {} };
356
+ }
357
+ const bySampleId = {};
358
+ const samples = q.samples || [];
359
+ if (limitSamples) {
360
+ for (const sid of limitSamples) {
361
+ bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
362
+ }
363
+ } else {
364
+ for (const sid of samples) {
365
+ bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
366
+ }
367
+ }
368
+ const term2sample2value = /* @__PURE__ */ new Map();
369
+ for (const geneTerm of param.terms) {
370
+ if (!geneTerm.gene)
371
+ continue;
372
+ if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop)) {
373
+ const re = getResultGene(genome, { input: geneTerm.gene, deep: 1 });
374
+ if (!re.gmlst || re.gmlst.length == 0) {
375
+ console.warn("Unknown gene:" + geneTerm.gene);
376
+ continue;
377
+ }
378
+ const i = re.gmlst.find((i2) => i2.isdefault) || re.gmlst[0];
379
+ geneTerm.start = i.start;
380
+ geneTerm.stop = i.stop;
381
+ geneTerm.chr = i.chr;
382
+ }
383
+ const s2v = {};
384
+ if (!geneTerm.chr || !Number.isInteger(geneTerm.start) || !Number.isInteger(geneTerm.stop)) {
385
+ throw "Missing chr/start/stop";
386
+ }
387
+ await utils.get_lines_bigfile({
388
+ args: [
389
+ q.file,
390
+ (q.nochr ? geneTerm.chr.replace("chr", "") : geneTerm.chr) + ":" + geneTerm.start + "-" + geneTerm.stop
391
+ ],
392
+ callback: (line) => {
393
+ const l = line.split(" ");
394
+ if (l[3].toLowerCase() != geneTerm.gene.toLowerCase())
395
+ return;
396
+ for (let i = 4; i < l.length; i++) {
397
+ const sampleId = samples[i - 4];
398
+ if (limitSamples && !limitSamples.has(sampleId))
399
+ continue;
400
+ if (!l[i])
401
+ continue;
402
+ const v = Number(l[i]);
403
+ if (Number.isNaN(v))
404
+ throw "Expression value not number";
405
+ s2v[sampleId] = v;
406
+ }
407
+ }
408
+ });
409
+ if (Object.keys(s2v).length) {
410
+ term2sample2value.set(geneTerm.gene, s2v);
411
+ }
412
+ }
413
+ const byTermId = {};
414
+ if (term2sample2value.size == 0) {
415
+ throw "No data available for the input " + param.terms?.map((g) => g.gene).join(", ");
416
+ }
417
+ return { term2sample2value, byTermId, bySampleId };
418
+ };
419
+ }
312
420
  }
313
421
  export {
314
422
  api,
@@ -155,6 +155,8 @@ function addNonDictionaryQueries(c, ds, genome) {
155
155
  details: q.snvindel.details,
156
156
  populations: q.snvindel.populations
157
157
  };
158
+ if (q.snvindel.byisoform?.processTwsInOneQuery)
159
+ q2.snvindel.byisoform = { processTwsInOneQuery: true };
158
160
  }
159
161
  if (q.trackLst) {
160
162
  q2.trackLst = q.trackLst;
@@ -0,0 +1,51 @@
1
+ import { topMutatedGenePayload } from "#types/checkers";
2
+ const api = {
3
+ endpoint: "termdb/topMutatedGenes",
4
+ methods: {
5
+ get: {
6
+ init,
7
+ ...topMutatedGenePayload
8
+ },
9
+ post: {
10
+ init,
11
+ ...topMutatedGenePayload
12
+ }
13
+ }
14
+ };
15
+ function init({ genomes }) {
16
+ return async (req, res) => {
17
+ try {
18
+ const q = req.query;
19
+ const g = genomes[q.genome];
20
+ if (!g)
21
+ throw "genome missing";
22
+ const ds = g.datasets?.[q.dslabel];
23
+ if (!ds)
24
+ throw "ds missing";
25
+ if (!ds.queries?.topMutatedGenes)
26
+ throw "not supported by ds";
27
+ const genes = await ds.queries.topMutatedGenes.get(q);
28
+ const payload = { genes };
29
+ res.send(payload);
30
+ } catch (e) {
31
+ res.send({ status: "error", error: e.message || e });
32
+ if (e.stack)
33
+ console.log(e.stack);
34
+ else
35
+ console.trace(e);
36
+ }
37
+ };
38
+ }
39
+ function validate_query_getTopMutatedGenes(ds, genome) {
40
+ const q = ds.queries?.topMutatedGenes;
41
+ if (!q)
42
+ return;
43
+ if (typeof q.get == "function")
44
+ return;
45
+ q.get = async (param) => {
46
+ };
47
+ }
48
+ export {
49
+ api,
50
+ validate_query_getTopMutatedGenes
51
+ };
@@ -0,0 +1,74 @@
1
+ import { wsiSamplesPayload } from "#types/checkers";
2
+ const routePath = "wsisamples";
3
+ const api = {
4
+ endpoint: `${routePath}`,
5
+ methods: {
6
+ get: {
7
+ ...wsiSamplesPayload,
8
+ init
9
+ },
10
+ post: {
11
+ ...wsiSamplesPayload,
12
+ init
13
+ }
14
+ }
15
+ };
16
+ function init({ genomes }) {
17
+ return async (req, res) => {
18
+ try {
19
+ const query = req.query;
20
+ const g = genomes[query.genome];
21
+ if (!g)
22
+ throw new Error("Invalid genome name");
23
+ const ds = g.datasets[query.dslabel];
24
+ if (!ds)
25
+ throw "Invalid dslabel";
26
+ const images = await ds.queries.WSImages.getSamples();
27
+ const payload = {
28
+ samples: images
29
+ };
30
+ res.status(200).json(payload);
31
+ } catch (e) {
32
+ console.warn(e);
33
+ res.status(500).send({
34
+ status: "error",
35
+ error: e.message || e
36
+ });
37
+ }
38
+ };
39
+ }
40
+ async function validate_query_getWSISamples(ds) {
41
+ const q = ds.queries?.WSImages;
42
+ if (!q)
43
+ return;
44
+ validateQuery(ds);
45
+ }
46
+ function validateQuery(ds) {
47
+ if (typeof ds.queries.WSImages.getSamples == "function") {
48
+ return;
49
+ }
50
+ ds.queries.WSImages.getSamples = async () => {
51
+ const sql = `SELECT wsimages.sample as sample, wsimages.filename as filename, wsimages.metadata as metadata, sampleidmap.name as sampleName
52
+ FROM wsimages
53
+ INNER JOIN sampleidmap
54
+ ON wsimages.sample = sampleidmap.id`;
55
+ try {
56
+ const rows = ds.cohort.db.connection.prepare(sql).all();
57
+ const sampleMap = {};
58
+ for (const row of rows) {
59
+ if (!sampleMap[row.sample]) {
60
+ sampleMap[row.sample] = { sampleId: row.sampleName, wsimages: [] };
61
+ }
62
+ sampleMap[row.sample].wsimages.push(row.filename);
63
+ }
64
+ return Object.values(sampleMap);
65
+ } catch (error) {
66
+ console.error("Error fetching samples:", error);
67
+ throw error;
68
+ }
69
+ };
70
+ }
71
+ export {
72
+ api,
73
+ validate_query_getWSISamples
74
+ };