@sjcrh/proteinpaint-server 2.183.2-0 → 2.184.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.183.2-0",
3
+ "version": "2.184.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",
@@ -66,7 +66,7 @@
66
66
  "@sjcrh/proteinpaint-r": "2.181.0",
67
67
  "@sjcrh/proteinpaint-rust": "2.183.0",
68
68
  "@sjcrh/proteinpaint-shared": "2.183.0",
69
- "@sjcrh/proteinpaint-types": "2.183.1",
69
+ "@sjcrh/proteinpaint-types": "2.184.0",
70
70
  "@types/express": "^5.0.0",
71
71
  "@types/express-session": "^1.18.1",
72
72
  "better-sqlite3": "^12.4.1",
@@ -0,0 +1,114 @@
1
+ import { ProfileScoresPayload } from "#types/checkers";
2
+ import { getData } from "../src/termdb.matrix.js";
3
+ const api = {
4
+ endpoint: "termdb/profileBarchart2Scores",
5
+ methods: {
6
+ get: {
7
+ ...ProfileScoresPayload,
8
+ init
9
+ },
10
+ post: {
11
+ ...ProfileScoresPayload,
12
+ init
13
+ }
14
+ }
15
+ };
16
+ function init({ genomes }) {
17
+ return async (req, res) => {
18
+ try {
19
+ const g = genomes[req.query.genome];
20
+ if (!g) throw "invalid genome name";
21
+ const ds = g.datasets?.[req.query.dslabel];
22
+ const result = await getScores(req.query, ds);
23
+ res.send(result);
24
+ } catch (e) {
25
+ console.log(e);
26
+ res.send({ status: "error", error: e.message || e });
27
+ }
28
+ };
29
+ }
30
+ function derivePrefix(query) {
31
+ const firstScoreId = query.scoreTerms?.[0]?.score?.term?.id;
32
+ if (firstScoreId?.startsWith("F")) return "F";
33
+ if (firstScoreId?.startsWith("A")) return "A";
34
+ for (const entry of query.filter?.lst || []) {
35
+ const id = entry.tvs?.term?.id;
36
+ if (id?.startsWith("F")) return "F";
37
+ if (id?.startsWith("A")) return "A";
38
+ }
39
+ throw "cannot determine cohort prefix from scoreTerms or filter term IDs";
40
+ }
41
+ async function getScores(query, ds) {
42
+ const { activeCohort, clientAuthResult } = query.__protected__;
43
+ const prefix = derivePrefix(query);
44
+ const facilityTermId = `${prefix}UNIT`;
45
+ const facilityTW = { term: { id: facilityTermId }, q: {} };
46
+ const terms = [facilityTW];
47
+ for (const t of query.scoreTerms) {
48
+ terms.push(t.score);
49
+ if (t.maxScore?.term) terms.push(t.maxScore);
50
+ }
51
+ if (!query.filterByUserSites) {
52
+ query.__protected__.ignoredTermIds.push(facilityTermId);
53
+ }
54
+ const cohortAuth = clientAuthResult[activeCohort];
55
+ const isPublic = !cohortAuth?.role || cohortAuth.role === "public";
56
+ const userSites = cohortAuth?.sites;
57
+ const raw = await getData(
58
+ {
59
+ terms,
60
+ filter: query.filter,
61
+ __protected__: query.__protected__
62
+ },
63
+ ds
64
+ );
65
+ if (raw.error) throw raw.error;
66
+ const sampleList = Object.values(raw.samples);
67
+ let sites = sampleList.map((s) => {
68
+ const val = s[facilityTW.$id].value;
69
+ let label = facilityTW.term.values?.[val]?.label || val;
70
+ if (label.length > 50) label = label.slice(0, 47) + "...";
71
+ return { value: val, label };
72
+ });
73
+ if (userSites && query.filterByUserSites) {
74
+ sites = sites.filter((s) => userSites.includes(s.value));
75
+ }
76
+ sites.sort((a, b) => a.label.localeCompare(b.label));
77
+ const samples = Object.values(raw.samples);
78
+ const eligibleSamples = userSites && query.filterByUserSites ? samples.filter((s) => userSites.includes(s[facilityTW.$id].value)) : samples;
79
+ const term2Score = {};
80
+ for (const d of query.scoreTerms) {
81
+ const score = computeMedianPercentage(d, eligibleSamples);
82
+ if (score !== null) term2Score[d.score.term.id] = score;
83
+ }
84
+ return {
85
+ term2Score,
86
+ // Public users see only aggregated scores — do not expose site IDs or names
87
+ sites: isPublic ? [] : sites,
88
+ n: eligibleSamples.length
89
+ };
90
+ }
91
+ function computeMedianPercentage(d, samples) {
92
+ const percentages = [];
93
+ for (const s of samples) {
94
+ const scoreValue = s[d.score.$id]?.value;
95
+ if (scoreValue == null) continue;
96
+ let maxScoreValue = null;
97
+ if (typeof d.maxScore === "number") {
98
+ maxScoreValue = d.maxScore;
99
+ } else {
100
+ maxScoreValue = s[d.maxScore.$id]?.value;
101
+ }
102
+ if (maxScoreValue == null || maxScoreValue === 0) continue;
103
+ const percentage = scoreValue / maxScoreValue * 100;
104
+ percentages.push(percentage);
105
+ }
106
+ if (percentages.length === 0) return null;
107
+ percentages.sort((a, b) => a - b);
108
+ const mid = Math.floor(percentages.length / 2);
109
+ const median = percentages.length % 2 !== 0 ? percentages[mid] : (percentages[mid - 1] + percentages[mid]) / 2;
110
+ return Math.round(median);
111
+ }
112
+ export {
113
+ api
114
+ };
@@ -194,6 +194,9 @@ function addNonDictionaryQueries(c, ds, genome) {
194
194
  }
195
195
  if (q.proteome) {
196
196
  q2.proteome = {};
197
+ if (q.proteome.overlayTerm) {
198
+ q2.proteome.overlayTerm = JSON.parse(JSON.stringify(q.proteome.overlayTerm));
199
+ }
197
200
  if (q.proteome.assays) {
198
201
  q2.proteome.assays = {};
199
202
  for (const assay in q.proteome.assays) {
@@ -203,11 +206,11 @@ function addNonDictionaryQueries(c, ds, genome) {
203
206
  for (const cohort in q.proteome.assays[assay].cohorts) {
204
207
  q2.proteome.assays[assay].cohorts[cohort] = {};
205
208
  const src = q.proteome.assays[assay].cohorts[cohort];
206
- if ("filter" in src) {
207
- q2.proteome.assays[assay].cohorts[cohort].filter = JSON.parse(JSON.stringify(src.filter));
209
+ if ("controlFilter" in src) {
210
+ q2.proteome.assays[assay].cohorts[cohort].controlFilter = JSON.parse(JSON.stringify(src.controlFilter));
208
211
  }
209
- if ("overlayTerm" in src) {
210
- q2.proteome.assays[assay].cohorts[cohort].overlayTerm = JSON.parse(JSON.stringify(src.overlayTerm));
212
+ if ("caseFilter" in src) {
213
+ q2.proteome.assays[assay].cohorts[cohort].caseFilter = JSON.parse(JSON.stringify(src.caseFilter));
211
214
  }
212
215
  }
213
216
  }
@@ -28,16 +28,11 @@ function init({ genomes }) {
28
28
  const cohorts = [];
29
29
  for (const assayName in ds.queries.proteome.assays) {
30
30
  const assay = ds.queries.proteome.assays[assayName];
31
- for (const cohort of assay.cohorts) {
31
+ for (const cohortName in assay.cohorts || {}) {
32
32
  const details = {
33
33
  dbfile: ds.queries.proteome.dbfile,
34
- assayName,
35
- cohortName: cohort.cohortName,
36
- cohortControlFilter: cohort.controlFilter,
37
- cohortCaseFilter: cohort.caseFilter,
38
- PTMType: assay.PTMType,
39
- assayColumnIdx: assay.columnIdx,
40
- assayColumnValue: assay.columnValue
34
+ assay: assayName,
35
+ cohort: cohortName
41
36
  };
42
37
  const tw = {
43
38
  $id: "_",
@@ -214,16 +209,14 @@ async function validate_query_proteome(ds) {
214
209
  }
215
210
  for (const assayName in q.assays) {
216
211
  const assay = q.assays[assayName];
217
- if (!assay.columnIdx) throw `queries.proteome.assays.${assayName}.columnIdx missing`;
218
- if (!assay.columnValue) throw `queries.proteome.assays.${assayName}.columnValue missing`;
212
+ if (assay.columnIdx == null) throw `queries.proteome.assays.${assayName}.columnIdx missing`;
213
+ if (assay.columnValue == null) throw `queries.proteome.assays.${assayName}.columnValue missing`;
219
214
  if (assay.cohorts) {
220
- console.log(`Validating assay "${assayName}" with multiple cohorts`);
221
- for (const cohort of assay.cohorts) {
222
- if (!cohort.cohortName) throw `Missing cohortName in queries.proteome.assays.${assayName}.cohorts`;
215
+ for (const cohortName in assay.cohorts) {
216
+ const cohort = assay.cohorts[cohortName];
223
217
  if (!cohort.controlFilter)
224
- throw `Missing controlFilter in queries.proteome.assays.${assayName}.cohorts.${cohort.cohortName}`;
225
- if (!cohort.caseFilter)
226
- throw `Missing caseFilter in queries.proteome.assays.${assayName}.cohorts.${cohort.cohortName}`;
218
+ throw `Missing controlFilter in queries.proteome.assays.${assayName}.cohorts.${cohortName}`;
219
+ if (!cohort.caseFilter) throw `Missing caseFilter in queries.proteome.assays.${assayName}.cohorts.${cohortName}`;
227
220
  }
228
221
  } else {
229
222
  throw `Invalid assay structure for "${assayName}". Must have .cohorts`;
@@ -232,15 +225,56 @@ async function validate_query_proteome(ds) {
232
225
  q.find = async (arg) => {
233
226
  const proteins = arg?.proteins;
234
227
  if (!Array.isArray(proteins) || proteins.length == 0) throw "queries.proteome.find arg.proteins[] missing";
235
- return findProteinsInCohort(q.db, proteins);
228
+ const matches = /* @__PURE__ */ new Set();
229
+ const details = arg?.proteomeDetails || {};
230
+ const assay = details.assay;
231
+ const cohort = details.cohort;
232
+ const MAX_FIND_RESULTS = 500;
233
+ const filters = [];
234
+ if (Object.keys(details).length) {
235
+ if (!assay || !cohort) throw "queries.proteome.find arg.proteomeDetails.{assay,cohort} missing";
236
+ const assayConfig = q.assays?.[assay];
237
+ if (!assayConfig) throw `queries.proteome.find invalid assay: ${assay}`;
238
+ const cohortConfig = assayConfig?.cohorts?.[cohort];
239
+ if (!cohortConfig) throw `queries.proteome.find invalid cohort: ${cohort}`;
240
+ const assayFilter = [{ columnIdx: assayConfig.columnIdx, columnValue: assayConfig.columnValue }];
241
+ const cohortFilter = (Array.isArray(cohortConfig.caseFilter) ? cohortConfig.caseFilter : []).filter(
242
+ (filter) => !!filter
243
+ );
244
+ if (!cohortFilter.length) throw `queries.proteome.find invalid cohort caseFilter: ${cohort}`;
245
+ filters.push(...assayFilter, ...cohortFilter);
246
+ }
247
+ for (const p of proteins) {
248
+ if (!p) continue;
249
+ const token = String(p).trim();
250
+ if (token.length < 2) continue;
251
+ const upperToken = `${token}\uFFFF`;
252
+ const rawRows = [];
253
+ if (filters?.length) {
254
+ const { conditions, params } = buildFilterClause(filters);
255
+ const sql = `SELECT DISTINCT gene, identifier FROM proteome_abundance WHERE gene >= ? COLLATE NOCASE AND gene < ? COLLATE NOCASE AND ${conditions.join(
256
+ " AND "
257
+ )} LIMIT ${MAX_FIND_RESULTS}`;
258
+ rawRows.push(...q.db.prepare(sql).all(token, upperToken, ...params));
259
+ } else {
260
+ rawRows.push(
261
+ ...q.db.prepare(
262
+ `SELECT DISTINCT gene, identifier FROM proteome_abundance WHERE gene >= ? COLLATE NOCASE AND gene < ? COLLATE NOCASE LIMIT ${MAX_FIND_RESULTS}`
263
+ ).all(token, upperToken)
264
+ );
265
+ }
266
+ for (const row of rawRows) {
267
+ if (!row?.gene || !row?.identifier) continue;
268
+ matches.add(`${row.gene}: ${row.identifier}`);
269
+ }
270
+ }
271
+ return [...matches];
236
272
  };
237
273
  q.get = async (param) => {
238
274
  if (!param?.terms?.length) throw "queries.proteome.get param.terms[] missing";
239
- if (!param.proteomeDetails?.assayName || !param.proteomeDetails?.cohortName)
240
- throw "queries.proteome.get param.proteomeDetails.{assayName,cohortName} missing";
241
- if (!param.proteomeDetails?.cohortControlFilter || !param.proteomeDetails?.cohortCaseFilter || !param.proteomeDetails?.assayColumnIdx || !param.proteomeDetails?.assayColumnValue)
242
- throw "queries.proteome.get param.proteomeDetails.{cohortControlFilter, cohortCaseFilter, assayColumnIdx, assayColumnValue} missing";
243
- return await getProteomeValuesFromCohort(ds, param);
275
+ if (!param.proteomeDetails?.assay || !param.proteomeDetails?.cohort)
276
+ throw "queries.proteome.get param.proteomeDetails.{assay,cohort} missing";
277
+ return await getProteomeValuesFromCohort(ds, param, q);
244
278
  };
245
279
  }
246
280
  const columnIdxToName = {
@@ -267,32 +301,26 @@ function buildFilterClause(filters) {
267
301
  }
268
302
  return { conditions, params };
269
303
  }
270
- function findProteinsInCohort(db, proteins) {
271
- const matches = [];
272
- for (const p of proteins) {
273
- if (!p) continue;
274
- const rows = db.prepare("SELECT DISTINCT gene, identifier FROM proteome_abundance WHERE gene LIKE ? COLLATE NOCASE").all(`%${p}%`);
275
- for (const row of rows) {
276
- if (row.gene.toLowerCase().includes(p.toLowerCase())) {
277
- matches.push(`${row.gene}: ${row.identifier}`);
278
- }
279
- }
280
- }
281
- return matches;
282
- }
283
304
  function queryDbRows(db, matchColumn, matchValue, filters) {
284
- console.log(`Querying DB for ${matchColumn}=${matchValue} with filters:`, filters);
285
305
  const { conditions, params } = buildFilterClause(filters);
286
306
  const allConditions = [`${matchColumn} = ? COLLATE NOCASE`, ...conditions];
287
- const sql = `SELECT identifier, protein_accession, modsite, gene, sample, value
307
+ const sql = `SELECT identifier, protein_accession, isoform, modsite, gene, sample, value
288
308
  FROM proteome_abundance
289
309
  WHERE ${allConditions.join(" AND ")}`;
290
- console.log("Executing SQL:", sql);
291
310
  return db.prepare(sql).all(matchValue, ...params);
292
311
  }
293
- async function getProteomeValuesFromCohort(ds, param) {
312
+ async function getProteomeValuesFromCohort(ds, param, q) {
294
313
  const db = ds.queries.proteome.db;
295
- const { assayName, cohortName, PTMType, cohortControlFilter, cohortCaseFilter, assayColumnIdx, assayColumnValue } = param.proteomeDetails;
314
+ const { assay, cohort } = param.proteomeDetails;
315
+ const assayConfig = q.assays?.[assay];
316
+ if (!assayConfig) throw `queries.proteome.get invalid assay: ${assay}`;
317
+ const PTMType = q.assays[assay].PTMType;
318
+ const assayColumnIdx = assayConfig.columnIdx;
319
+ const assayColumnValue = assayConfig.columnValue;
320
+ const cohortConfig = assayConfig?.cohorts?.[cohort];
321
+ if (!cohortConfig) throw `queries.proteome.get invalid cohort: ${cohort}`;
322
+ const cohortControlFilter = cohortConfig.controlFilter;
323
+ const cohortCaseFilter = cohortConfig.caseFilter;
296
324
  const assayFilter = [{ columnIdx: assayColumnIdx, columnValue: assayColumnValue }];
297
325
  const term2sample2value = /* @__PURE__ */ new Map();
298
326
  const allEntries = [];
@@ -336,11 +364,13 @@ async function getProteomeValuesFromCohort(ds, param) {
336
364
  if (!entryMap.has(row.identifier)) {
337
365
  entryMap.set(row.identifier, {
338
366
  uniqueIdentifier: row.identifier,
339
- assayName,
340
- cohortName,
367
+ assayName: assay,
368
+ cohortName: cohort,
341
369
  PTMType,
342
370
  modSites: PTMType ? row.modsite || void 0 : void 0,
343
371
  proteinAccession: row.protein_accession,
372
+ isoform: row.isoform,
373
+ // refSeq transcript ID mapped from protein_accession
344
374
  geneName: row.gene,
345
375
  s2v: {}
346
376
  });