@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 +2 -2
- package/routes/profile.barchart2.js +114 -0
- package/routes/termdb.config.js +7 -4
- package/routes/termdb.proteome.js +72 -42
- package/src/app.js +288 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
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
|
+
};
|
package/routes/termdb.config.js
CHANGED
|
@@ -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 ("
|
|
207
|
-
q2.proteome.assays[assay].cohorts[cohort].
|
|
209
|
+
if ("controlFilter" in src) {
|
|
210
|
+
q2.proteome.assays[assay].cohorts[cohort].controlFilter = JSON.parse(JSON.stringify(src.controlFilter));
|
|
208
211
|
}
|
|
209
|
-
if ("
|
|
210
|
-
q2.proteome.assays[assay].cohorts[cohort].
|
|
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
|
|
31
|
+
for (const cohortName in assay.cohorts || {}) {
|
|
32
32
|
const details = {
|
|
33
33
|
dbfile: ds.queries.proteome.dbfile,
|
|
34
|
-
assayName,
|
|
35
|
-
|
|
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 (
|
|
218
|
-
if (
|
|
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
|
-
|
|
221
|
-
|
|
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.${
|
|
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
|
-
|
|
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?.
|
|
240
|
-
throw "queries.proteome.get param.proteomeDetails.{
|
|
241
|
-
|
|
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 {
|
|
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
|
});
|