@sjcrh/proteinpaint-server 2.180.1 → 2.181.1

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.
@@ -60,7 +60,9 @@ function termdb_test_default() {
60
60
  }
61
61
  }
62
62
  ]
63
- }
63
+ },
64
+ /** Test by commenting out cohort.termdb.cohort.termdb.selectCohort */
65
+ html: "<div>Demo HTML Tab Content</div>"
64
66
  }
65
67
  }
66
68
  },
@@ -383,6 +385,10 @@ function termdb_test_default() {
383
385
  file: "files/hg38/TermdbTest/rnaseq/TermdbTest.fpkm.matrix.new.h5",
384
386
  unit: "FPKM"
385
387
  },
388
+ isoformExpression: {
389
+ file: "files/hg38/TermdbTest/rnaseq/TermdbTest.isoform.tpm.h5",
390
+ unit: "TPM"
391
+ },
386
392
  ssGSEA: {
387
393
  file: "files/hg38/TermdbTest/rnaseq/TermdbTest.ssgsea.h5"
388
394
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.180.1",
3
+ "version": "2.181.1",
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",
@@ -61,12 +61,12 @@
61
61
  "typescript": "^5.6.3"
62
62
  },
63
63
  "dependencies": {
64
- "@sjcrh/augen": "2.143.0",
65
- "@sjcrh/proteinpaint-python": "2.179.0",
66
- "@sjcrh/proteinpaint-r": "2.180.1-0",
67
- "@sjcrh/proteinpaint-rust": "2.180.1-0",
68
- "@sjcrh/proteinpaint-shared": "2.180.1",
69
- "@sjcrh/proteinpaint-types": "2.180.1",
64
+ "@sjcrh/augen": "2.181.1",
65
+ "@sjcrh/proteinpaint-python": "2.181.0",
66
+ "@sjcrh/proteinpaint-r": "2.181.0",
67
+ "@sjcrh/proteinpaint-rust": "2.181.0",
68
+ "@sjcrh/proteinpaint-shared": "2.181.0",
69
+ "@sjcrh/proteinpaint-types": "2.181.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,101 @@
1
+ import { ProfileScoresPayload } from "#types/checkers";
2
+ import { getData } from "../src/termdb.matrix.js";
3
+ const api = {
4
+ endpoint: "termdb/profilePolar2Scores",
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 = samples.filter((s) => s[d.score.$id]?.value).map((s) => s[d.score.$id].value / (s[d.maxScore.$id]?.value || d.maxScore) * 100);
93
+ if (percentages.length === 0) return null;
94
+ percentages.sort((a, b) => a - b);
95
+ const mid = Math.floor(percentages.length / 2);
96
+ const median = percentages.length % 2 !== 0 ? percentages[mid] : (percentages[mid - 1] + percentages[mid]) / 2;
97
+ return Math.round(median);
98
+ }
99
+ export {
100
+ api
101
+ };
@@ -7,7 +7,7 @@ import serverconfig from "#src/serverconfig.js";
7
7
  import { gdc_validate_query_geneExpression } from "#src/mds3.gdc.js";
8
8
  import { mayLimitSamples } from "#src/mds3.filter.js";
9
9
  import { clusterMethodLst, distanceMethodLst } from "#shared/clustering.js";
10
- import { TermTypes, PROTEOME_ABUNDANCE } from "#shared/terms.js";
10
+ import { TermTypes, ISOFORM_EXPRESSION, PROTEOME_ABUNDANCE } from "#shared/terms.js";
11
11
  import { termType2label } from "#shared/terms.js";
12
12
  import { formatElapsedTime } from "#shared/time.js";
13
13
  const api = {
@@ -34,7 +34,7 @@ function init({ genomes }) {
34
34
  if (!ds) throw "invalid dataset name";
35
35
  if (ds.label === "GDC" && !ds.__gdc?.doneCaching)
36
36
  throw "The server has not finished caching the case IDs: try again in about 2 minutes.";
37
- if ([TermTypes.GENE_EXPRESSION, TermTypes.METABOLITE_INTENSITY].includes(q.dataType)) {
37
+ if ([TermTypes.GENE_EXPRESSION, ISOFORM_EXPRESSION, TermTypes.METABOLITE_INTENSITY].includes(q.dataType)) {
38
38
  if (!ds.queries?.[q.dataType]) throw `no ${q.dataType} data on this dataset`;
39
39
  if (!q.terms) throw `missing gene list`;
40
40
  if (!Array.isArray(q.terms)) throw `gene list is not an array`;
@@ -81,7 +81,7 @@ async function getResult(q, ds) {
81
81
  for (const [term, obj] of term2sample2value) {
82
82
  if (Object.keys(obj).length === 0) {
83
83
  const tw = q.terms.find((t2) => t2.$id == term);
84
- const termName = !tw ? term : tw.term.type == "geneExpression" ? tw.term.gene : tw.term.name;
84
+ const termName = !tw ? term : tw.term.type == "geneExpression" ? tw.term.gene : tw.term.type == "isoformExpression" ? tw.term.isoform : tw.term.name;
85
85
  noValueTerms.push(termName);
86
86
  term2sample2value.delete(term);
87
87
  delete byTermId[term];
@@ -309,7 +309,115 @@ async function validateNative(q, ds) {
309
309
  return { term2sample2value, byTermId, bySampleId };
310
310
  };
311
311
  }
312
+ async function validateQueryIsoformExpression(ds, _genome) {
313
+ const q = ds.queries.isoformExpression;
314
+ if (!q) return;
315
+ q.geneExpression2bins = {};
316
+ if (typeof q.get == "function") return;
317
+ if (q.file) {
318
+ await validateNativeIsoform(q, ds);
319
+ return;
320
+ }
321
+ throw "isoformExpression requires either .get() or .file";
322
+ }
323
+ async function validateNativeIsoform(q, ds) {
324
+ q.file = path.join(serverconfig.tpmasterdir, q.file);
325
+ q.samples = [];
326
+ try {
327
+ await utils.file_is_readable(q.file);
328
+ const tmp = await run_rust("readH5", JSON.stringify({ hdf5_file: q.file, validate: true, include_items: true }));
329
+ const vr = JSON.parse(tmp);
330
+ if (vr.status !== "success") throw vr.message;
331
+ if (!vr.samples?.length) throw "HDF5 file has no samples, please check file.";
332
+ for (const sn of vr.samples) {
333
+ const si = ds.cohort.termdb.q.sampleName2id(sn);
334
+ if (si == void 0) {
335
+ if (ds.cohort.db) {
336
+ throw `unknown sample ${sn} from HDF5 ${q.file}`;
337
+ } else {
338
+ continue;
339
+ }
340
+ }
341
+ q.samples.push(si);
342
+ }
343
+ q.availableItems = vr.items || [];
344
+ console.log(
345
+ `${ds.label}: isoformExpression HDF5 file validated. Format: ${vr.format}, Samples:`,
346
+ q.samples.length,
347
+ "Items:",
348
+ q.availableItems.length
349
+ );
350
+ } catch (error) {
351
+ throw `${ds.label}: Failed to validate isoformExpression HDF5 file: ${error}`;
352
+ }
353
+ q.get = async (param) => {
354
+ const limitSamples = await mayLimitSamples(param, q.samples, ds);
355
+ if (limitSamples?.size == 0) {
356
+ return { term2sample2value: /* @__PURE__ */ new Map(), byTermId: {}, bySampleId: {} };
357
+ }
358
+ const bySampleId = {};
359
+ const samples = q.samples || [];
360
+ if (limitSamples) {
361
+ for (const sid of limitSamples) {
362
+ if (ds.cohort?.termdb?.q?.id2sampleRefs) {
363
+ bySampleId[sid] = ds.cohort.termdb.q.id2sampleRefs(sid);
364
+ } else {
365
+ bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
366
+ }
367
+ }
368
+ } else {
369
+ for (const sid of samples) {
370
+ if (ds.cohort?.termdb?.q?.id2sampleRefs) {
371
+ bySampleId[sid] = ds.cohort.termdb.q.id2sampleRefs(sid);
372
+ } else {
373
+ bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
374
+ }
375
+ }
376
+ }
377
+ const term2sample2value = /* @__PURE__ */ new Map();
378
+ const byTermId = {};
379
+ const isoformIds = [];
380
+ for (const tw of param.terms) {
381
+ if (tw.term.isoform) {
382
+ isoformIds.push(tw.term.isoform);
383
+ }
384
+ }
385
+ if (isoformIds.length === 0) {
386
+ console.log("No isoforms to query");
387
+ return { term2sample2value, byTermId };
388
+ }
389
+ const time1 = Date.now();
390
+ const isoformData = JSON.parse(await queryHDF5(q.file, isoformIds, null));
391
+ console.log("Time taken to run isoform query:", formatElapsedTime(Date.now() - time1));
392
+ const isoformsData = isoformData.query_output || {};
393
+ if (!isoformsData) throw "No expression data returned from HDF5 query";
394
+ for (const tw of param.terms) {
395
+ if (!tw.term.isoform) continue;
396
+ const isoformResult = isoformsData[tw.term.isoform];
397
+ if (!isoformResult) {
398
+ console.warn(`No data found for isoform ${tw.term.isoform} in the response`);
399
+ continue;
400
+ }
401
+ const samplesData = isoformResult.samples || {};
402
+ const s2v = {};
403
+ for (const sampleName in samplesData) {
404
+ const sampleId = ds.cohort.termdb.q.sampleName2id(sampleName);
405
+ if (!sampleId) continue;
406
+ if (limitSamples && !limitSamples.has(sampleId)) continue;
407
+ s2v[sampleId] = samplesData[sampleName];
408
+ }
409
+ if (Object.keys(s2v).length) {
410
+ term2sample2value.set(tw.$id, s2v);
411
+ }
412
+ }
413
+ if (term2sample2value.size == 0) {
414
+ throw "No data available for the input " + param.terms?.map((tw) => tw.term.isoform).join(", ");
415
+ }
416
+ return { term2sample2value, byTermId, bySampleId };
417
+ };
418
+ }
312
419
  export {
313
420
  api,
421
+ validateQueryIsoformExpression,
314
422
  validate_query_geneExpression
315
423
  };
@@ -5,6 +5,7 @@ import {
5
5
  TermTypeGroups,
6
6
  SINGLECELL_CELLTYPE,
7
7
  GENE_EXPRESSION,
8
+ ISOFORM_EXPRESSION,
8
9
  METABOLITE_INTENSITY,
9
10
  PROTEOME_ABUNDANCE,
10
11
  SINGLECELL_GENE_EXPRESSION,
@@ -162,7 +163,8 @@ function addNonDictionaryQueries(c, ds, genome) {
162
163
  "cnvCutoffsByGene",
163
164
  "absoluteValueRenderMax",
164
165
  "gainColor",
165
- "lossColor"
166
+ "lossColor",
167
+ "requiresHardcodeCnvOnlyFlag"
166
168
  ]) {
167
169
  if (k in q.cnv) q2.cnv[k] = q.cnv[k];
168
170
  }
@@ -187,6 +189,9 @@ function addNonDictionaryQueries(c, ds, genome) {
187
189
  if (q.geneExpression) {
188
190
  q2.geneExpression = { unit: q.geneExpression.unit };
189
191
  }
192
+ if (q.isoformExpression) {
193
+ q2.isoformExpression = { unit: q.isoformExpression.unit };
194
+ }
190
195
  if (q.proteome) {
191
196
  q2.proteome = {};
192
197
  if (q.proteome.assays) {
@@ -197,10 +202,12 @@ function addNonDictionaryQueries(c, ds, genome) {
197
202
  q2.proteome.assays[assay].cohorts = {};
198
203
  for (const cohort in q.proteome.assays[assay].cohorts) {
199
204
  q2.proteome.assays[assay].cohorts[cohort] = {};
200
- if ("filter" in q.proteome.assays[assay].cohorts[cohort]) {
201
- q2.proteome.assays[assay].cohorts[cohort].filter = JSON.parse(
202
- JSON.stringify(q.proteome.assays[assay].cohorts[cohort].filter)
203
- );
205
+ 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));
208
+ }
209
+ if ("overlayTerm" in src) {
210
+ q2.proteome.assays[assay].cohorts[cohort].overlayTerm = JSON.parse(JSON.stringify(src.overlayTerm));
204
211
  }
205
212
  }
206
213
  }
@@ -292,6 +299,7 @@ function getAllowedTermTypes(ds) {
292
299
  for (const t of ds.cohort.termdb.allowedTermTypes) typeSet.add(t);
293
300
  }
294
301
  if (ds.queries?.geneExpression) typeSet.add(GENE_EXPRESSION);
302
+ if (ds.queries?.isoformExpression) typeSet.add(ISOFORM_EXPRESSION);
295
303
  if (ds.queries?.metaboliteIntensity) typeSet.add(METABOLITE_INTENSITY);
296
304
  if (ds.queries?.proteome?.assays) typeSet.add(PROTEOME_ABUNDANCE);
297
305
  if (ds.queries?.ssGSEA) typeSet.add(SSGSEA);
@@ -1,6 +1,5 @@
1
1
  import { diffMethPayload } from "#types/checkers";
2
2
  import { getData } from "../src/termdb.matrix.js";
3
- import { get_ds_tdb } from "../src/termdb.js";
4
3
  import { run_R } from "@sjcrh/proteinpaint-r";
5
4
  import { mayLog } from "#src/helpers.ts";
6
5
  import { formatElapsedTime } from "#shared";
@@ -22,8 +21,9 @@ function init({ genomes }) {
22
21
  try {
23
22
  const q = req.query;
24
23
  const genome = genomes[q.genome];
25
- if (!genome) throw new Error("invalid genome");
26
- const [ds] = get_ds_tdb(genome, q);
24
+ if (!genome) throw "unknown genome";
25
+ const ds = genome.datasets?.[q.dslabel];
26
+ if (!ds) throw "unknown ds";
27
27
  let term_results = [];
28
28
  if (q.tw) {
29
29
  term_results = await getData({ filter: q.filter, filter0: q.filter0, terms: [q.tw] }, ds);
@@ -35,7 +35,12 @@ function init({ genomes }) {
35
35
  if (term_results2.error) throw new Error(term_results2.error);
36
36
  }
37
37
  const results = await run_diffMeth(req.query, ds, term_results, term_results2);
38
- if (!results || !results.data) throw new Error("No data available");
38
+ if (!results || !results.data)
39
+ throw new Error(
40
+ "Differential methylation analysis returned no data. Please verify sample selections and try again."
41
+ );
42
+ if (Array.isArray(results.data) && !results.data.length)
43
+ throw new Error("No promoters passed filtering. Try relaxing group criteria or selecting more samples.");
39
44
  res.send(results);
40
45
  } catch (e) {
41
46
  res.send({ status: "error", error: e.message || e });
@@ -44,12 +49,15 @@ function init({ genomes }) {
44
49
  };
45
50
  }
46
51
  async function run_diffMeth(param, ds, term_results, term_results2) {
47
- if (param.samplelst?.groups?.length != 2) throw new Error(".samplelst.groups.length!=2");
48
- if (param.samplelst.groups[0].values?.length < 1) throw new Error("samplelst.groups[0].values.length<1");
49
- if (param.samplelst.groups[1].values?.length < 1) throw new Error("samplelst.groups[1].values.length<1");
52
+ if (param.samplelst?.groups?.length != 2)
53
+ throw new Error("Exactly 2 sample groups are required for differential methylation analysis.");
54
+ if (param.samplelst.groups[0].values?.length < 1)
55
+ throw new Error("Group 1 has no samples. Please select at least one sample.");
56
+ if (param.samplelst.groups[1].values?.length < 1)
57
+ throw new Error("Group 2 has no samples. Please select at least one sample.");
50
58
  const q = ds.queries.dnaMethylation?.promoter;
51
- if (!q) throw new Error("ds.queries.dnaMethylation.promoter is not configured");
52
- if (!q.file) throw new Error("ds.queries.dnaMethylation.promoter.file is missing");
59
+ if (!q) throw new Error("This dataset does not have promoter-level methylation data configured.");
60
+ if (!q.file) throw new Error("Promoter methylation data file is not configured for this dataset.");
53
61
  const group1names = [];
54
62
  const conf1_group1 = [];
55
63
  const conf2_group1 = [];
@@ -169,10 +177,13 @@ async function run_diffMeth(param, ds, term_results, term_results2) {
169
177
  }
170
178
  function validateGroups(sample_size1, sample_size2, group1names, group2names) {
171
179
  const alerts = [];
172
- if (sample_size1 < 1) alerts.push("sample size of group1 < 1");
173
- if (sample_size2 < 1) alerts.push("sample size of group2 < 1");
180
+ if (sample_size1 < 1) alerts.push("No samples in group 1 have methylation data available.");
181
+ if (sample_size2 < 1) alerts.push("No samples in group 2 have methylation data available.");
174
182
  const commonnames = group1names.filter((x) => group2names.includes(x));
175
- if (commonnames.length) alerts.push(`Common elements found between both groups: ${commonnames.join(", ")}`);
183
+ if (commonnames.length)
184
+ alerts.push(
185
+ `${commonnames.length} sample(s) appear in both groups: ${commonnames.join(", ")}. Please remove duplicates.`
186
+ );
176
187
  return alerts;
177
188
  }
178
189
  export {
@@ -1,7 +1,9 @@
1
1
  import { TermdbDmrPayload } from "#types/checkers";
2
- import { run_python } from "@sjcrh/proteinpaint-python";
2
+ import { run_rust } from "@sjcrh/proteinpaint-rust";
3
+ import { run_R } from "@sjcrh/proteinpaint-r";
3
4
  import { invalidcoord } from "#shared/common.js";
4
5
  import { mayLog } from "#src/helpers.ts";
6
+ import serverconfig from "#src/serverconfig.js";
5
7
  import { formatElapsedTime } from "#shared";
6
8
  const api = {
7
9
  endpoint: "termdb/dmr",
@@ -21,32 +23,64 @@ function init({ genomes }) {
21
23
  try {
22
24
  const q = req.query;
23
25
  const genome = genomes[q.genome];
24
- if (!genome) throw new Error("invalid genome");
26
+ if (!genome) throw "unknown genome";
25
27
  const ds = genome.datasets?.[q.dslabel];
26
- if (!ds) throw new Error("invalid ds");
27
- if (!ds.queries?.dnaMethylation) throw new Error("analysis not supported");
28
- if (!Array.isArray(q.group1) || q.group1.length == 0) throw new Error("group1 not non empty array");
29
- if (!Array.isArray(q.group2) || q.group2.length == 0) throw new Error("group2 not non empty array");
30
- if (invalidcoord(genome, q.chr, q.start, q.stop)) throw new Error("invalid chr/start/stop");
28
+ if (!ds) throw "unknown ds";
29
+ if (!ds.queries?.dnaMethylation) throw new Error("This dataset does not support DNA methylation analysis.");
30
+ if (!Array.isArray(q.group1) || q.group1.length == 0)
31
+ throw new Error("Group 1 has no samples. Please select at least one sample.");
32
+ if (!Array.isArray(q.group2) || q.group2.length == 0)
33
+ throw new Error("Group 2 has no samples. Please select at least one sample.");
34
+ if (invalidcoord(genome, q.chr, q.start, q.stop))
35
+ throw new Error(`Invalid genomic coordinates: ${q.chr}:${q.start}-${q.stop}`);
36
+ const SERVER_MAX_REGION_BP = 1e7;
37
+ const span = q.stop - q.start;
38
+ if (span > SERVER_MAX_REGION_BP)
39
+ throw new Error(
40
+ `Region too large (${(span / 1e6).toFixed(1)} Mb). Server maximum is ${SERVER_MAX_REGION_BP / 1e6} Mb.`
41
+ );
31
42
  const group1 = q.group1.map((s) => s.sample).filter(Boolean);
32
43
  const group2 = q.group2.map((s) => s.sample).filter(Boolean);
33
- if (group1.length < 3) throw new Error(`Need at least 3 samples in group1, got ${group1.length}`);
34
- if (group2.length < 3) throw new Error(`Need at least 3 samples in group2, got ${group2.length}`);
35
- const gpdmInput = {
36
- h5file: ds.queries.dnaMethylation.file,
44
+ if (group1.length < 3)
45
+ throw new Error(`Group 1 needs at least 3 samples with methylation data, got ${group1.length}.`);
46
+ if (group2.length < 3)
47
+ throw new Error(`Group 2 needs at least 3 samples with methylation data, got ${group2.length}.`);
48
+ const useR = q.backend === "r";
49
+ const dmrInput = {
50
+ probe_h5_file: ds.queries.dnaMethylation.file,
51
+ cachedir: serverconfig.cachedir,
52
+ genome: q.genome,
37
53
  chr: q.chr,
38
54
  start: q.start,
39
55
  stop: q.stop,
40
- group1,
41
- group2,
42
- annotations: q.annotations || [],
43
- nan_threshold: q.nan_threshold ?? 0.5
56
+ case: group2.join(","),
57
+ control: group1.join(","),
58
+ fdr_cutoff: q.fdr_cutoff,
59
+ lambda: q.lambda,
60
+ C: q.C,
61
+ blockWidth: q.blockWidth,
62
+ devicePixelRatio: q.devicePixelRatio,
63
+ maxLoessRegion: q.maxLoessRegion,
64
+ colors: q.colors
44
65
  };
45
66
  const time1 = Date.now();
46
- const result = JSON.parse(await run_python("gpdm_analysis.py", JSON.stringify(gpdmInput)));
47
- mayLog("DMR analysis time:", formatElapsedTime(Date.now() - time1));
67
+ const result = useR ? JSON.parse(await run_R("dmrcate_full.R", JSON.stringify(dmrInput))) : JSON.parse(await run_rust("dmrcate", JSON.stringify(dmrInput)));
68
+ mayLog(`DMR analysis (${useR ? "R" : "Rust"}) time:`, formatElapsedTime(Date.now() - time1));
48
69
  if (result.error) throw new Error(result.error);
49
- res.send({ status: "ok", dmrs: result.dmrs });
70
+ if (result.diagnostic?.probes) {
71
+ const p = result.diagnostic.probes;
72
+ mayLog(
73
+ `${useR ? "R" : "Rust"} probes logFC:`,
74
+ p.logFC,
75
+ "fdr:",
76
+ p.fdr?.map((f) => f.toExponential(4))
77
+ );
78
+ }
79
+ res.send({
80
+ status: "ok",
81
+ dmrs: result.dmrs,
82
+ diagnostic: result.diagnostic
83
+ });
50
84
  } catch (e) {
51
85
  const msg = e instanceof Error ? e.message : String(e);
52
86
  res.send({ error: msg });
@@ -0,0 +1,35 @@
1
+ import { TermdbIsoformAvailabilityPayload } from "#types/checkers";
2
+ const api = {
3
+ endpoint: "termdb/isoformAvailability",
4
+ methods: {
5
+ get: {
6
+ ...TermdbIsoformAvailabilityPayload,
7
+ init
8
+ },
9
+ post: {
10
+ ...TermdbIsoformAvailabilityPayload,
11
+ init
12
+ }
13
+ }
14
+ };
15
+ function init({ genomes }) {
16
+ return (req, res) => {
17
+ try {
18
+ const q = req.query;
19
+ const genome = genomes[q.genome];
20
+ if (!genome) throw "invalid genome";
21
+ const ds = genome.datasets?.[q.dslabel];
22
+ if (!ds) throw "invalid dslabel";
23
+ const isoQ = ds.queries?.isoformExpression;
24
+ if (!isoQ) throw "isoformExpression not configured for this dataset";
25
+ const itemSet = new Set(isoQ.availableItems || []);
26
+ const available = (q.isoforms || []).filter((id) => itemSet.has(id));
27
+ res.send({ available });
28
+ } catch (e) {
29
+ res.send({ error: e.message || e });
30
+ }
31
+ };
32
+ }
33
+ export {
34
+ api
35
+ };
@@ -42,10 +42,21 @@ async function getScoresDict(query, ds) {
42
42
  const percents = getSCPercentsDict(d, data.samples);
43
43
  term2Score[d.term.id] = percents;
44
44
  }
45
- const facilityValue = data.sampleData?.[query.facilityTW.$id];
46
- const termValue = query.facilityTW.term.values[facilityValue?.value];
47
- const hospital = termValue?.label || termValue?.key;
48
- return { term2Score, sites: data.sites, hospital, n: data.sampleData ? 1 : data.samples.length };
45
+ const { clientAuthResult, activeCohort } = query.__protected__;
46
+ const isPublic = !clientAuthResult[activeCohort]?.role || clientAuthResult[activeCohort].role === "public";
47
+ let hospital;
48
+ if (!isPublic && data.sampleData) {
49
+ const facilityValue = data.sampleData[query.facilityTW.$id];
50
+ const termValue = query.facilityTW.term.values[facilityValue?.value];
51
+ hospital = termValue?.label || termValue?.key;
52
+ }
53
+ return {
54
+ term2Score,
55
+ // Do not expose individual site IDs, names, or hospital to public-role users
56
+ sites: isPublic ? [] : data.sites,
57
+ hospital,
58
+ n: data.sampleData ? 1 : data.samples.length
59
+ };
49
60
  }
50
61
  function getDict(key, sample) {
51
62
  if (!sample[key]) return null;
@@ -81,7 +81,15 @@ async function getScores(query, ds) {
81
81
  for (const d of query.scoreTerms) {
82
82
  term2Score[d.score.term.id] = getPercentage(d, data.samples, data.sampleData);
83
83
  }
84
- return { term2Score, sites: data.sites, site: data.site, n: data.sampleData ? 1 : data.samples.length };
84
+ const { clientAuthResult, activeCohort } = query.__protected__;
85
+ const isPublic = !clientAuthResult[activeCohort]?.role || clientAuthResult[activeCohort].role === "public";
86
+ return {
87
+ term2Score,
88
+ // Do not expose individual site IDs or names to public-role users
89
+ sites: isPublic ? [] : data.sites,
90
+ site: isPublic ? void 0 : data.site,
91
+ n: data.sampleData ? 1 : data.samples.length
92
+ };
85
93
  }
86
94
  function getPercentage(d, samples, sampleData) {
87
95
  if (!d) return null;
@@ -131,9 +131,21 @@ async function getSingleCellScatter(req, res, ds) {
131
131
  const data = await ds.queries.singleCell.data.get(arg);
132
132
  const plot = data.plots[0];
133
133
  const cells = [...plot.expCells, ...plot.noExpCells];
134
+ const groups = tw.q?.customset?.groups;
135
+ const cat2GrpName = /* @__PURE__ */ new Map();
136
+ if (groups) {
137
+ for (const group of groups) {
138
+ for (const value of Object.values(group.values)) {
139
+ cat2GrpName.set(value.key, group.name);
140
+ }
141
+ }
142
+ }
134
143
  const samples = cells.map((cell) => {
144
+ let category = cell.category;
145
+ const groupName = cat2GrpName.get(category);
146
+ if (groupName !== void 0) category = groupName;
135
147
  const hidden = {
136
- category: tw?.q?.hiddenValues ? cell.category in tw.q.hiddenValues : false
148
+ category: tw?.q?.hiddenValues ? category in tw.q.hiddenValues : false
137
149
  };
138
150
  return {
139
151
  sample: cell.cellId,
@@ -141,7 +153,7 @@ async function getSingleCellScatter(req, res, ds) {
141
153
  x: cell.x,
142
154
  y: cell.y,
143
155
  z: 0,
144
- category: cell.category,
156
+ category,
145
157
  shape: "Ref",
146
158
  hidden,
147
159
  geneExp: cell.geneExp