@sjcrh/proteinpaint-server 2.205.1 → 2.206.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.205.1",
3
+ "version": "2.206.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",
@@ -59,9 +59,9 @@
59
59
  "@sjcrh/augen": "2.204.0",
60
60
  "@sjcrh/proteinpaint-python": "2.204.0",
61
61
  "@sjcrh/proteinpaint-r": "2.204.0",
62
- "@sjcrh/proteinpaint-rust": "2.205.1",
63
- "@sjcrh/proteinpaint-shared": "2.205.0",
64
- "@sjcrh/proteinpaint-types": "2.205.0",
62
+ "@sjcrh/proteinpaint-rust": "2.206.0",
63
+ "@sjcrh/proteinpaint-shared": "2.206.0",
64
+ "@sjcrh/proteinpaint-types": "2.206.0",
65
65
  "@types/express": "^5.0.0",
66
66
  "@types/express-session": "^1.18.1",
67
67
  "better-sqlite3": "^12.4.1",
@@ -26,7 +26,7 @@ function stripQuotes(s) {
26
26
  async function parseTsv(absPath) {
27
27
  const text = await fs.promises.readFile(absPath, "utf8");
28
28
  const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
29
- if (lines.length === 0) return { columns: [], rows: [] };
29
+ if (lines.length === 0) return { columns: [], rows: [], counts: [], geneIndex: /* @__PURE__ */ new Map() };
30
30
  const columns = lines[0].split(" ").map(stripQuotes);
31
31
  const rows = [];
32
32
  for (let i = 1; i < lines.length; i++) {
@@ -39,7 +39,17 @@ async function parseTsv(absPath) {
39
39
  });
40
40
  rows.push(row);
41
41
  }
42
- return { columns, rows };
42
+ const counts = columns.map(
43
+ (_, c) => c === 0 ? rows.length : rows.filter((r) => r[c] !== null && r[c] !== void 0).length
44
+ );
45
+ const geneIndex = /* @__PURE__ */ new Map();
46
+ for (const [i, r] of rows.entries()) {
47
+ if (typeof r[0] === "string") {
48
+ const g = r[0].toLowerCase();
49
+ if (!geneIndex.has(g)) geneIndex.set(g, i);
50
+ }
51
+ }
52
+ return { columns, rows, counts, geneIndex };
43
53
  }
44
54
  function zscorePerColumnIgnoringNull(matrix, ncol) {
45
55
  const out = matrix.map((r) => [...r]);
@@ -67,24 +77,53 @@ async function handleData(q, res, genomes) {
67
77
  if (!ds) throw "invalid dslabel";
68
78
  const cfg = ds.queries?.geneRanking;
69
79
  if (!cfg || !cfg.rankings) throw "geneRanking not configured for this dataset";
80
+ if (q.gene) {
81
+ const wanted = String(q.gene).trim().toLowerCase();
82
+ const geneRanks = {};
83
+ const keys = Object.keys(cfg.rankings);
84
+ const loaded = await Promise.all(
85
+ keys.map(
86
+ (key) => loadRanking(q, cfg.rankings[key], key).catch((e) => {
87
+ console.log(`geneRanking: cannot load ranking "${key}": ${e?.message || e}`);
88
+ return null;
89
+ })
90
+ )
91
+ );
92
+ for (const [i, parsed2] of loaded.entries()) {
93
+ if (!parsed2) continue;
94
+ const ri = parsed2.geneIndex.get(wanted);
95
+ geneRanks[keys[i]] = {
96
+ columns: parsed2.columns,
97
+ row: ri === void 0 ? null : parsed2.rows[ri],
98
+ counts: parsed2.counts,
99
+ total: parsed2.rows.length
100
+ };
101
+ }
102
+ res.send({ geneRanks });
103
+ return;
104
+ }
70
105
  if (!q.key) {
71
106
  res.send({ keys: Object.keys(cfg.rankings) });
72
107
  return;
73
108
  }
74
109
  const relPath = cfg.rankings[q.key];
75
110
  if (!relPath) throw "invalid key";
111
+ const parsed = await loadRanking(q, relPath, q.key);
112
+ res.send({ columns: parsed.columns, rows: parsed.rows });
113
+ }
114
+ async function loadRanking(q, relPath, key) {
76
115
  if (path.isAbsolute(relPath) || relPath.split(/[\\/]/).includes("..")) throw "invalid file path";
77
116
  const absPath = path.resolve(serverconfig.tpmasterdir, relPath);
78
117
  const tpRoot = path.resolve(serverconfig.tpmasterdir) + path.sep;
79
118
  if (!absPath.startsWith(tpRoot)) throw "invalid file path";
80
119
  const stat = await fs.promises.stat(absPath);
81
- const cacheKey = `${q.genome}|${q.dslabel}|${q.key}`;
120
+ const cacheKey = `${q.genome}|${q.dslabel}|${key}`;
82
121
  let entry = fileCache.get(cacheKey);
83
122
  if (!entry || entry.mtimeMs !== stat.mtimeMs) {
84
123
  entry = { parsed: await parseTsv(absPath), mtimeMs: stat.mtimeMs };
85
124
  fileCache.set(cacheKey, entry);
86
125
  }
87
- res.send({ columns: entry.parsed.columns, rows: entry.parsed.rows });
126
+ return entry.parsed;
88
127
  }
89
128
  async function handleCluster(q, res) {
90
129
  const { row_names, col_names } = q;
@@ -3,19 +3,7 @@ import { get_ds_tdb } from "#src/termdb.js";
3
3
  import * as utils from "#src/utils.js";
4
4
  import { mayLimitSamples } from "#src/mds3.filter.js";
5
5
  import serverconfig from "#src/serverconfig.js";
6
- import { readGeneRows } from "../src/routes/termdb.bubbleHeatmap.ts";
7
- function baseUniProtAcc(acc) {
8
- if (!acc) return "";
9
- if (acc.includes("|")) {
10
- const parts2 = acc.split("|");
11
- const id = parts2.length >= 2 ? parts2[1] : acc;
12
- const dash = id.indexOf("-");
13
- return dash > 0 ? id.slice(0, dash) : id;
14
- }
15
- const parts = acc.split(".");
16
- if (parts.length >= 3 && (parts[0] === "sp" || parts[0] === "tr")) return parts[1];
17
- return acc;
18
- }
6
+ import { readGeneRows, baseUniProtAcc } from "../src/routes/termdb.bubbleHeatmap.ts";
19
7
  const missingDapWarned = /* @__PURE__ */ new Set();
20
8
  function init({ genomes }) {
21
9
  return async (req, res) => {
@@ -93,6 +81,8 @@ function init({ genomes }) {
93
81
  for (const row of rows) {
94
82
  const entry = {
95
83
  organism: organismName,
84
+ // the study catalog (dataset config) is the authority on disease
85
+ disease: cohortCfg.catalog?.disease,
96
86
  assayName,
97
87
  cohortName,
98
88
  uniqueIdentifier: row.identifier,
@@ -101,8 +91,8 @@ function init({ genomes }) {
101
91
  // client computes log2(foldChange); DAP stores log2FC directly
102
92
  foldChange: Math.pow(2, row.fc),
103
93
  // significance is the DAP file's FDR (BH-adjusted p), consistent with
104
- // the other DAP-driven tools. pValue is a deprecated alias of fdr kept
105
- // for existing clients; new code should read fdr.
94
+ // the other DAP-driven tools; a nominal p is only present when the
95
+ // DAP file carries one (6th column)
106
96
  fdr: row.fdr,
107
97
  pValue: row.fdr,
108
98
  testedN: caseSamples.length,
@@ -130,7 +120,7 @@ function init({ genomes }) {
130
120
  const baseAcc = baseUniProtAcc(e.proteinAccession);
131
121
  if (!baseAcc) continue;
132
122
  const key = `${e.organism}|${e.cohortName}|${baseAcc}`;
133
- const p = Number.isFinite(e.pValue) ? e.pValue : Infinity;
123
+ const p = Number.isFinite(e.fdr) ? e.fdr : Infinity;
134
124
  const cur = refFcByKey.get(key);
135
125
  if (!cur || p < cur.p) refFcByKey.set(key, { fc: e.foldChange, p });
136
126
  }