@sjcrh/proteinpaint-server 2.135.3-1 → 2.137.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.
@@ -111,6 +111,7 @@ function termdb_test_default() {
111
111
  plotConfigByCohort: {
112
112
  default: {
113
113
  report: {
114
+ filterTWs: [{ id: "diaggrp" }],
114
115
  sections: [
115
116
  {
116
117
  name: "Demographics",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.135.3-1",
3
+ "version": "2.137.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",
@@ -47,7 +47,6 @@
47
47
  "babel-loader": "^8.2.2",
48
48
  "c8": "^10.1.3",
49
49
  "esbuild": "^0.19.12",
50
- "glob": "^10.4.5",
51
50
  "monocart-coverage-reports": "^2.12.1",
52
51
  "node-notifier": "^9.0.1",
53
52
  "node-watch": "^0.7.1",
@@ -60,12 +59,12 @@
60
59
  "typescript": "^5.6.3"
61
60
  },
62
61
  "dependencies": {
63
- "@sjcrh/augen": "2.121.0",
62
+ "@sjcrh/augen": "2.136.0",
64
63
  "@sjcrh/proteinpaint-python": "2.135.2-0",
65
64
  "@sjcrh/proteinpaint-r": "2.130.0",
66
- "@sjcrh/proteinpaint-rust": "2.135.3-1",
67
- "@sjcrh/proteinpaint-shared": "2.135.2-0",
68
- "@sjcrh/proteinpaint-types": "2.135.2-0",
65
+ "@sjcrh/proteinpaint-rust": "2.136.0",
66
+ "@sjcrh/proteinpaint-shared": "2.136.0",
67
+ "@sjcrh/proteinpaint-types": "2.137.0",
69
68
  "@types/express": "^5.0.0",
70
69
  "@types/express-session": "^1.18.1",
71
70
  "better-sqlite3": "^9.4.1",
@@ -35,7 +35,8 @@ function getList(samplesPerFilter, filtersData, tw) {
35
35
  for (const sample of twSamples) {
36
36
  data.push(filtersData.samples[sample]);
37
37
  }
38
- const sampleValues = Array.from(new Set(data.map((sample) => sample[tw.$id]?.value)));
38
+ const annotations = data.filter((s) => s != void 0).map((sample) => sample[tw.$id]?.value);
39
+ const sampleValues = Array.from(new Set(annotations));
39
40
  for (const value of values) {
40
41
  value.value = value.label;
41
42
  const label = value.label;
@@ -0,0 +1,245 @@
1
+ import { GRIN2Payload } from "#types/checkers";
2
+ import serverconfig from "#src/serverconfig.js";
3
+ import path from "path";
4
+ import { run_python } from "@sjcrh/proteinpaint-python";
5
+ import { mayLog } from "#src/helpers.ts";
6
+ import { get_samples } from "#src/termdb.sql.js";
7
+ import { read_file, file_is_readable } from "#src/utils.js";
8
+ import { dtsnvindel, dtcnv, dtfusionrna } from "#shared/common.js";
9
+ const api = {
10
+ endpoint: "grin2",
11
+ methods: {
12
+ get: {
13
+ ...GRIN2Payload,
14
+ init
15
+ },
16
+ post: {
17
+ ...GRIN2Payload,
18
+ init
19
+ }
20
+ }
21
+ };
22
+ function init({ genomes }) {
23
+ return async (req, res) => {
24
+ try {
25
+ const request = req.query;
26
+ const g = genomes[request.genome];
27
+ if (!g)
28
+ throw new Error("genome missing");
29
+ const ds = g.datasets?.[request.dslabel];
30
+ if (!ds)
31
+ throw new Error("ds missing");
32
+ if (!ds.queries?.singleSampleMutation)
33
+ throw new Error("singleSampleMutation query missing from dataset");
34
+ const result = await runGrin2(g, ds, request);
35
+ res.json(result);
36
+ } catch (e) {
37
+ console.error("[GRIN2] Error stack:", e.stack);
38
+ const errorResponse = {
39
+ status: "error",
40
+ error: e.message || String(e)
41
+ };
42
+ res.status(500).send(errorResponse);
43
+ }
44
+ };
45
+ }
46
+ async function runGrin2(g, ds, request) {
47
+ const startTime = Date.now();
48
+ mayLog("[GRIN2] Getting samples from cohort filter...");
49
+ const samples = await get_samples(
50
+ request.filter,
51
+ ds,
52
+ true
53
+ // must set to true to return sample name to be able to access file. FIXME this can let names revealed to grin2 client, may need to apply access control
54
+ );
55
+ const cohortTime = Date.now() - startTime;
56
+ mayLog(`[GRIN2] Retrieved ${samples.length.toLocaleString()} samples in ${Math.round(cohortTime / 1e3)} seconds`);
57
+ if (samples.length === 0) {
58
+ throw new Error("No samples found matching the provided filter criteria");
59
+ }
60
+ mayLog("[GRIN2] Processing sample data...");
61
+ const processingStartTime = Date.now();
62
+ const { lesionData, processingSummary } = await processSampleData(samples, ds, request);
63
+ const processingTime = Date.now() - processingStartTime;
64
+ const processingTimeToPrint = Math.round(processingTime / 1e3);
65
+ mayLog(`[GRIN2] Data processing took ${processingTimeToPrint} seconds`);
66
+ mayLog(
67
+ `[GRIN2] Processing summary: ${processingSummary?.successfulSamples ?? 0}/${processingSummary?.totalSamples ?? samples.length} samples processed successfully`
68
+ );
69
+ if (processingSummary && processingSummary.failedSamples > 0) {
70
+ mayLog(`[GRIN2] Warning: ${processingSummary.failedSamples} samples failed to process`);
71
+ }
72
+ if (lesionData.length === 0) {
73
+ throw new Error("No lesions found after processing all samples. Check filter criteria and input data.");
74
+ }
75
+ const pyInput = {
76
+ genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
77
+ chromosomelist: {},
78
+ lesion: JSON.stringify(lesionData)
79
+ };
80
+ for (const c in g.majorchr) {
81
+ if (ds.queries.singleSampleMutation.discoPlot?.skipChrM) {
82
+ if (c.toLowerCase() == "chrm")
83
+ continue;
84
+ }
85
+ pyInput.chromosomelist[c] = g.majorchr[c];
86
+ }
87
+ mayLog(`[GRIN2] Prepared ${lesionData.length.toLocaleString()} lesions for analysis`);
88
+ const grin2AnalysisStart = Date.now();
89
+ const pyResult = await run_python("grin2PpWrapper.py", JSON.stringify(pyInput));
90
+ if (pyResult.stderr?.trim()) {
91
+ mayLog(`[GRIN2] Python stderr: ${pyResult.stderr}`);
92
+ if (pyResult.stderr.includes("ERROR:")) {
93
+ throw new Error(`Python script error: ${pyResult.stderr}`);
94
+ }
95
+ }
96
+ const grin2AnalysisTime = Date.now() - grin2AnalysisStart;
97
+ const grin2AnalysisTimeToPrint = Math.round(grin2AnalysisTime / 1e3);
98
+ mayLog(`[GRIN2] Python processing took ${grin2AnalysisTimeToPrint} seconds`);
99
+ const resultData = JSON.parse(pyResult.stdout);
100
+ if (!resultData?.png?.[0]) {
101
+ throw new Error("Invalid Python output: missing PNG data");
102
+ }
103
+ const totalTime = Math.round((Date.now() - startTime) / 1e3);
104
+ const response = {
105
+ status: "success",
106
+ pngImg: resultData.png[0],
107
+ topGeneTable: resultData.topGeneTable,
108
+ totalGenes: resultData.totalGenes,
109
+ showingTop: resultData.showingTop,
110
+ timing: {
111
+ processingTime: processingTimeToPrint,
112
+ grin2Time: grin2AnalysisTimeToPrint,
113
+ totalTime
114
+ },
115
+ processingSummary
116
+ };
117
+ return response;
118
+ }
119
+ async function processSampleData(samples, ds, request) {
120
+ const lesions = [];
121
+ let lesionId = 1;
122
+ const processingSummary = {
123
+ totalSamples: samples.length,
124
+ successfulSamples: 0,
125
+ failedSamples: 0,
126
+ failedFiles: []
127
+ };
128
+ mayLog(`[GRIN2] Processing JSON files for ${samples.length.toLocaleString()} samples`);
129
+ for (const sample of samples) {
130
+ try {
131
+ const filepath = path.join(serverconfig.tpmasterdir, ds.queries.singleSampleMutation.folder, sample.name);
132
+ await file_is_readable(filepath);
133
+ const mlst = JSON.parse(await read_file(filepath));
134
+ const sampleLesions = await processSampleMlst(sample.name, mlst, lesionId, request);
135
+ lesions.push(...sampleLesions);
136
+ lesionId += sampleLesions.length;
137
+ } catch (error) {
138
+ mayLog(
139
+ `[GRIN2] Error processing sample ${sample.name}: ${typeof error === "object" && error !== null && "message" in error ? error.message : String(error)}`
140
+ );
141
+ }
142
+ }
143
+ mayLog(`[GRIN2] Total lesions processed: ${lesions.length.toLocaleString()}`);
144
+ return {
145
+ lesionData: lesions,
146
+ processingSummary
147
+ };
148
+ }
149
+ async function processSampleMlst(sampleName, mlst, startId, request) {
150
+ const lesions = [];
151
+ for (const m of mlst) {
152
+ switch (m.dt) {
153
+ case dtsnvindel: {
154
+ if (!request.snvindelOptions)
155
+ break;
156
+ const snvIndelLesion = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
157
+ if (snvIndelLesion)
158
+ lesions.push(snvIndelLesion);
159
+ break;
160
+ }
161
+ case dtcnv: {
162
+ if (!request.cnvOptions)
163
+ break;
164
+ const cnvLesion = filterAndConvertCnv(sampleName, m, request.cnvOptions);
165
+ if (cnvLesion)
166
+ lesions.push(cnvLesion);
167
+ break;
168
+ }
169
+ case dtfusionrna: {
170
+ if (!request.fusionOptions)
171
+ break;
172
+ const fusionLesion = filterAndConvertFusion(sampleName, m, request.fusionOptions);
173
+ if (fusionLesion)
174
+ lesions.push(fusionLesion);
175
+ break;
176
+ }
177
+ default:
178
+ mayLog(`[GRIN2] Unknown data type "${m.dt}" in sample ${sampleName}, skipping entry`);
179
+ break;
180
+ }
181
+ }
182
+ return lesions;
183
+ }
184
+ function filterAndConvertSnvIndel(sampleName, entry, options) {
185
+ const opts = {
186
+ minTotalDepth: options?.minTotalDepth ?? 10,
187
+ minAltAlleleCount: options?.minAltAlleleCount ?? 2,
188
+ consequences: options?.consequences ?? [],
189
+ hyperMutator: options?.hyperMutator ?? 1e3
190
+ };
191
+ if (opts.consequences.length > 0 && entry.consequence && !opts.consequences.includes(entry.consequence)) {
192
+ return null;
193
+ }
194
+ return [
195
+ sampleName,
196
+ normalizeChromosome(entry.chromosome || entry.chr),
197
+ String(entry.start || entry.position),
198
+ String(entry.end || entry.position),
199
+ "mutation"
200
+ ];
201
+ }
202
+ function filterAndConvertCnv(sampleName, entry, options) {
203
+ const opts = {
204
+ lossThreshold: options?.lossThreshold ?? -0.4,
205
+ gainThreshold: options?.gainThreshold ?? 0.3,
206
+ maxSegLength: options?.maxSegLength ?? 0,
207
+ minSegLength: options?.minSegLength ?? 0,
208
+ hyperMutator: options?.hyperMutator ?? 500
209
+ };
210
+ const isGain = entry.log2Ratio >= opts.gainThreshold;
211
+ const isLoss = entry.log2Ratio <= opts.lossThreshold;
212
+ if (!isGain && !isLoss)
213
+ return null;
214
+ const lesionType = entry.log2Ratio >= opts.gainThreshold ? "gain" : "loss";
215
+ return [
216
+ sampleName,
217
+ normalizeChromosome(entry.chromosome || entry.chr),
218
+ String(entry.start || entry.begin),
219
+ String(entry.end || entry.stop),
220
+ lesionType
221
+ ];
222
+ }
223
+ function filterAndConvertFusion(sampleName, entry, options) {
224
+ const opts = {
225
+ fusionTypes: options?.fusionTypes ?? ["gene-gene", "gene-intergenic", "readthrough"],
226
+ minConfidence: options?.minConfidence ?? 0.7
227
+ };
228
+ if (entry.fusionType && !opts.fusionTypes.includes(entry.fusionType))
229
+ return null;
230
+ if (entry.confidence && entry.confidence < opts.minConfidence)
231
+ return null;
232
+ return [
233
+ sampleName,
234
+ normalizeChromosome(entry.chromosome || entry.chr),
235
+ String(entry.start || entry.position),
236
+ String(entry.end || entry.position),
237
+ "fusion"
238
+ ];
239
+ }
240
+ function normalizeChromosome(chrom) {
241
+ return chrom.startsWith("chr") ? chrom : `chr${chrom}`;
242
+ }
243
+ export {
244
+ api
245
+ };
@@ -51,12 +51,20 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
51
51
  const data = await getData(arg, ds, genome);
52
52
  if (data.error)
53
53
  throw data.error;
54
+ const [lst, orderedLabels] = getCategories(data, q, ds, $id);
55
+ res.send({
56
+ lst,
57
+ orderedLabels
58
+ });
59
+ }
60
+ function getCategories(data, q, ds, $id) {
54
61
  const lst = [];
55
62
  if (q.tw.term.type == "geneVariant" && q.tw.q.type != "predefined-groupset" && q.tw.q.type != "custom-groupset") {
56
63
  const samples = data.samples;
57
64
  const dtClassMap = /* @__PURE__ */ new Map();
58
65
  if (ds.assayAvailability?.byDt) {
59
- for (const [dtType, dtValue] of Object.entries(ds.assayAvailability.byDt)) {
66
+ for (const [dtType, _dtValue] of Object.entries(ds.assayAvailability.byDt)) {
67
+ const dtValue = _dtValue;
60
68
  if (dtValue.byOrigin) {
61
69
  dtClassMap.set(parseInt(dtType), { byOrigin: { germline: {}, somatic: {} } });
62
70
  }
@@ -65,6 +73,8 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
65
73
  const sampleCountedFor = /* @__PURE__ */ new Set();
66
74
  for (const sampleData of Object.values(samples)) {
67
75
  const key = $id;
76
+ if (!Object.keys(sampleData).includes(key))
77
+ continue;
68
78
  const values = sampleData[key].values;
69
79
  sampleCountedFor.clear();
70
80
  for (const value of values) {
@@ -126,11 +136,9 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
126
136
  if (orderedLabels.length) {
127
137
  lst.sort((a, b) => orderedLabels.indexOf(a.label) - orderedLabels.indexOf(b.label));
128
138
  }
129
- res.send({
130
- lst,
131
- orderedLabels
132
- });
139
+ return [lst, orderedLabels];
133
140
  }
134
141
  export {
135
- api
142
+ api,
143
+ getCategories
136
144
  };
@@ -51,9 +51,14 @@ async function validate_query_singleCell(ds, genome) {
51
51
  const q = ds.queries.singleCell;
52
52
  if (!q)
53
53
  return;
54
- if (typeof q.samples.get != "function") {
54
+ if (typeof q.samples != "object")
55
+ throw "singleCell.samples{} not object";
56
+ if (typeof q.samples.get == "function") {
57
+ } else {
55
58
  validateSamplesNative(q.samples, q.data, ds);
56
59
  }
60
+ if (typeof q.data != "object")
61
+ throw "singleCell.data{} not object";
57
62
  if (q.data.src == "gdcapi") {
58
63
  gdc_validate_query_singleCell_data(ds, genome);
59
64
  } else if (q.data.src == "native") {
@@ -62,6 +67,8 @@ async function validate_query_singleCell(ds, genome) {
62
67
  throw "unknown singleCell.data.src";
63
68
  }
64
69
  if (q.geneExpression) {
70
+ if (typeof q.geneExpression != "object")
71
+ throw "singleCell.geneExpression not object";
65
72
  if (q.geneExpression.src == "native") {
66
73
  validateGeneExpressionNative(q.geneExpression);
67
74
  } else if (q.geneExpression.src == "gdcapi") {
@@ -71,9 +78,13 @@ async function validate_query_singleCell(ds, genome) {
71
78
  }
72
79
  }
73
80
  if (q.DEgenes) {
81
+ if (typeof q.DEgenes != "object")
82
+ throw "singleCell.DEgenes not object";
74
83
  validate_query_singleCell_DEgenes(ds);
75
84
  }
76
85
  if (q.images) {
86
+ if (typeof q.images != "object")
87
+ throw "singleCell.images not object";
77
88
  validateImages(q.images);
78
89
  }
79
90
  }
@@ -127,68 +138,69 @@ function validateDataNative(D, ds) {
127
138
  if (nameSet.has(plot.name))
128
139
  throw "duplicate plot.name";
129
140
  nameSet.add(plot.name);
141
+ if (!plot.folder)
142
+ throw "plot.folder missing";
130
143
  }
144
+ const file2Lines = {};
131
145
  D.get = async (q) => {
132
- try {
133
- const plots = [];
134
- let geneExpMap;
135
- if (ds.queries.singleCell.geneExpression && q.gene) {
136
- geneExpMap = await ds.queries.singleCell.geneExpression.get({ sample: q.sample, gene: q.gene });
137
- }
138
- const file2Lines = {};
139
- for (const plot of D.plots) {
140
- if (!q.plots.includes(plot.name))
141
- continue;
142
- const tsvfile = path.join(
143
- serverconfig.tpmasterdir,
144
- plot.folder,
145
- (q.sample.eID || q.sample.sID) + plot.fileSuffix
146
- );
147
- if (!file2Lines[tsvfile]) {
148
- await file_is_readable(tsvfile);
149
- file2Lines[tsvfile] = (await read_file(tsvfile)).trim().split("\n");
150
- }
151
- const colorColumn = plot.colorColumns.find((c) => c.name == q.colorBy?.[plot.name]) || plot.colorColumns[0];
152
- const lines = file2Lines[tsvfile];
153
- const expCells = [];
154
- const noExpCells = [];
155
- for (let i = 1; i < lines.length; i++) {
156
- const l = lines[i].split(" ");
157
- const cellId = lines.length > 3 ? l[0] : void 0, x = Number(l[plot.coordsColumns.x]), y = Number(l[plot.coordsColumns.y]);
158
- const category = l[colorColumn?.index] || "";
159
- if (!cellId)
160
- throw "cell id missing";
161
- if (!Number.isFinite(x) || !Number.isFinite(y))
162
- throw "x/y not number";
163
- const cell = { cellId, x, y, category };
164
- if (geneExpMap) {
165
- if (geneExpMap[cellId] !== void 0) {
166
- cell.geneExp = geneExpMap[cellId];
167
- expCells.push(cell);
168
- } else {
169
- noExpCells.push(cell);
170
- }
171
- } else
172
- noExpCells.push(cell);
146
+ const plots = [];
147
+ let geneExpMap;
148
+ if (ds.queries.singleCell.geneExpression && q.gene) {
149
+ geneExpMap = await ds.queries.singleCell.geneExpression.get({ sample: q.sample, gene: q.gene });
150
+ }
151
+ for (const plot of D.plots) {
152
+ if (!q.plots.includes(plot.name))
153
+ continue;
154
+ const tsvfile = path.join(serverconfig.tpmasterdir, plot.folder, (q.sample.eID || q.sample.sID) + plot.fileSuffix);
155
+ if (!file2Lines[tsvfile]) {
156
+ await file_is_readable(tsvfile);
157
+ const text = await read_file(tsvfile);
158
+ const lines = text.trim().split("\n");
159
+ let first = true;
160
+ const lines2 = [];
161
+ for (const line of lines) {
162
+ if (first) {
163
+ first = false;
164
+ continue;
165
+ }
166
+ lines2.push(line.split(" "));
173
167
  }
174
- plots.push({
175
- name: plot.name,
176
- expCells,
177
- noExpCells,
178
- colorColumns: plot.colorColumns.map((c) => c.name),
179
- colorBy: colorColumn?.name,
180
- colorMap: colorColumn?.colorMap
181
- });
168
+ file2Lines[tsvfile] = lines2;
182
169
  }
183
- if (plots.length == 0) {
184
- return { nodata: true };
170
+ const colorColumn = plot.colorColumns.find((c) => c.name == q.colorBy?.[plot.name]) || plot.colorColumns[0];
171
+ const expCells = [];
172
+ const noExpCells = [];
173
+ for (const l of file2Lines[tsvfile]) {
174
+ const cellId = l[0], x = Number(l[plot.coordsColumns.x]), y = Number(l[plot.coordsColumns.y]);
175
+ const category = l[colorColumn?.index] || "";
176
+ if (!cellId)
177
+ throw "cell id missing";
178
+ if (!Number.isFinite(x) || !Number.isFinite(y))
179
+ throw "x/y not number";
180
+ const cell = { cellId, x, y, category };
181
+ if (geneExpMap) {
182
+ if (geneExpMap[cellId] !== void 0) {
183
+ cell.geneExp = geneExpMap[cellId];
184
+ expCells.push(cell);
185
+ } else {
186
+ noExpCells.push(cell);
187
+ }
188
+ } else
189
+ noExpCells.push(cell);
185
190
  }
186
- return { plots };
187
- } catch (e) {
188
- if (e.stack)
189
- console.log(e.stack);
190
- return { error: e.message || e };
191
+ plots.push({
192
+ name: plot.name,
193
+ expCells,
194
+ noExpCells,
195
+ colorColumns: plot.colorColumns.map((c) => c.name),
196
+ colorBy: colorColumn?.name,
197
+ colorMap: colorColumn?.colorMap
198
+ });
199
+ }
200
+ if (plots.length == 0) {
201
+ return { nodata: true };
191
202
  }
203
+ return { plots };
192
204
  };
193
205
  }
194
206
  function validateGeneExpressionNative(G) {