@sjcrh/proteinpaint-server 2.192.0 → 2.193.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.
@@ -1,238 +0,0 @@
1
- import ky from "ky";
2
- import { joinUrl } from "#shared/joinUrl.js";
3
- import serverconfig from "#src/serverconfig.js";
4
- import { mayLog } from "#src/helpers.ts";
5
- const mafMaxFileNumber = 1e3, cnvMaxFileNumber = 1e3;
6
- const allowedWorkflowType = "Aliquot Ensemble Somatic Variant Merging and Masking";
7
- const maxFileSizeAllowed = 1e6;
8
- const maxTotalSizeCompressed = serverconfig.features.gdcMafMaxFileSize || 4e8;
9
- function init({ genomes }) {
10
- return async (req, res) => {
11
- try {
12
- const g = genomes.hg38;
13
- if (!g) throw "hg38 missing";
14
- const ds = g.datasets?.GDC;
15
- if (!ds) throw "hg38 GDC missing";
16
- console.log("GRIN2 List Route Received:", JSON.stringify(req.query, null, 2));
17
- const result = {};
18
- if (req.query.mafOptions) {
19
- result.mafFiles = {
20
- files: [],
21
- filesTotal: 0,
22
- maxTotalSizeCompressed: 0,
23
- fileCounts: { maf: 0 }
24
- };
25
- await listMafFiles(req.query, result, ds);
26
- }
27
- if (req.query.cnvOptions) {
28
- result.cnvFiles = {
29
- files: [],
30
- maxTotalSizeCompressed
31
- };
32
- await listCnvFiles(req.query, result, ds);
33
- }
34
- res.send(result);
35
- } catch (e) {
36
- if (e.stack) console.log(e.stack);
37
- res.send({ status: "error", error: e.message || e });
38
- }
39
- };
40
- }
41
- async function listMafFiles(q, result, ds) {
42
- if (!q.mafOptions) return;
43
- if (!result.mafFiles) {
44
- throw new Error("result.mafFiles must be initialized before calling listMafFiles");
45
- }
46
- const filters = {
47
- op: "and",
48
- content: [
49
- { op: "=", content: { field: "data_format", value: "MAF" } },
50
- { op: "=", content: { field: "experimental_strategy", value: q.mafOptions.experimentalStrategy } },
51
- { op: "=", content: { field: "analysis.workflow_type", value: allowedWorkflowType } },
52
- { op: "=", content: { field: "access", value: "open" } }
53
- ]
54
- };
55
- const case_filters = { op: "and", content: [] };
56
- if (q.filter0) {
57
- case_filters.content.push(q.filter0);
58
- }
59
- const { host, headers } = ds.getHostHeaders(q);
60
- const body = {
61
- filters,
62
- size: mafMaxFileNumber,
63
- fields: [
64
- "id",
65
- "file_size",
66
- "cases.project.project_id",
67
- "cases.case_id",
68
- "cases.submitter_id",
69
- "cases.samples.tissue_type",
70
- "cases.samples.tumor_descriptor"
71
- ].join(",")
72
- };
73
- if (case_filters.content.length) body.case_filters = case_filters;
74
- const response = await ky.post(joinUrl(host.rest, "files"), { timeout: false, headers, json: body });
75
- if (!response.ok) throw `HTTP Error: ${response.status} ${response.statusText}`;
76
- const re = await response.json();
77
- if (!Number.isInteger(re.data?.pagination?.total)) throw "re.data.pagination.total is not int";
78
- if (!Array.isArray(re.data?.hits)) throw "re.data.hits[] not array";
79
- const files = [];
80
- const filteredFiles = [];
81
- for (const h of re.data.hits) {
82
- const c = h.cases?.[0];
83
- if (!c) throw "h.cases[0] missing";
84
- if (h.file_size >= maxFileSizeAllowed) {
85
- filteredFiles.push({
86
- fileId: h.id,
87
- fileSize: h.file_size,
88
- reason: `File size (${h.file_size} bytes) exceeds maximum allowed size (${maxFileSizeAllowed} bytes)`
89
- });
90
- mayLog(
91
- `File ${h.id} with a size of ${h.file_size} bytes is larger then the allowed file size. It is excluded from the list.
92
- If you want to include it, please increase the maxFileSizeAllowed in the code.`
93
- );
94
- continue;
95
- }
96
- const file = {
97
- id: h.id,
98
- project_id: c.project.project_id,
99
- file_size: h.file_size,
100
- case_submitter_id: c.submitter_id,
101
- case_uuid: c.case_id,
102
- sample_types: []
103
- };
104
- if (c.samples) {
105
- let normalTypeName;
106
- for (const { tumor_descriptor, tissue_type } of c.samples) {
107
- if (tissue_type == "Normal") {
108
- normalTypeName = (tumor_descriptor == "Not Applicable" ? "" : tumor_descriptor + " ") + tissue_type;
109
- continue;
110
- }
111
- file.sample_types.push(tumor_descriptor + " " + tissue_type);
112
- }
113
- if (normalTypeName) file.sample_types.push(normalTypeName);
114
- }
115
- file.sample_types = [...new Set(file.sample_types)];
116
- files.push(file);
117
- }
118
- const filesByCase = /* @__PURE__ */ new Map();
119
- for (const file of files) {
120
- const caseId = file.case_submitter_id;
121
- if (!filesByCase.has(caseId)) {
122
- filesByCase.set(caseId, []);
123
- }
124
- filesByCase.get(caseId).push(file);
125
- }
126
- const deduplicatedFiles = [];
127
- let duplicatesRemoved = 0;
128
- const caseDetails = [];
129
- for (const [caseId, caseFiles] of filesByCase) {
130
- if (caseFiles.length > 1) {
131
- caseFiles.sort((a, b) => b.file_size - a.file_size);
132
- deduplicatedFiles.push(caseFiles[0]);
133
- duplicatesRemoved += caseFiles.length - 1;
134
- caseDetails.push({
135
- caseName: caseId,
136
- fileCount: caseFiles.length,
137
- keptFileSize: caseFiles[0].file_size
138
- });
139
- mayLog(`Case ${caseId}: Found ${caseFiles.length} MAF files, keeping largest (${caseFiles[0].file_size} bytes)`);
140
- } else {
141
- deduplicatedFiles.push(caseFiles[0]);
142
- }
143
- }
144
- if (duplicatesRemoved > 0) {
145
- mayLog(
146
- `GRIN2 MAF deduplication: Removed ${duplicatesRemoved} duplicate files, kept ${deduplicatedFiles.length} unique cases`
147
- );
148
- }
149
- deduplicatedFiles.sort((a, b) => b.file_size - a.file_size);
150
- result.mafFiles.files.push(...deduplicatedFiles);
151
- result.mafFiles.filesTotal = re.data.pagination.total;
152
- if (result.mafFiles.fileCounts) {
153
- result.mafFiles.fileCounts.maf = files.length;
154
- }
155
- result.mafFiles.deduplicationStats = {
156
- originalFileCount: files.length,
157
- deduplicatedFileCount: deduplicatedFiles.length,
158
- duplicatesRemoved,
159
- caseDetails,
160
- filteredFiles
161
- };
162
- }
163
- async function listCnvFiles(q, result, ds) {
164
- if (!q.cnvOptions) {
165
- console.log("No cnvOptions provided, returning empty cnvFiles");
166
- return;
167
- }
168
- const case_filters = { op: "and", content: [] };
169
- if (q.filter0) {
170
- case_filters.content.push(q.filter0);
171
- }
172
- const body = {
173
- size: cnvMaxFileNumber,
174
- fields: [
175
- "cases.samples.tissue_type",
176
- "cases.project.project_id",
177
- "cases.submitter_id",
178
- "cases.case_id",
179
- "data_type",
180
- "file_id",
181
- "file_size",
182
- "data_format",
183
- "experimental_strategy",
184
- "analysis.workflow_type"
185
- ].join(","),
186
- filters: {
187
- op: "in",
188
- content: {
189
- field: "data_type",
190
- value: ["Copy Number Segment", "Masked Copy Number Segment", "Allele-specific Copy Number Segment"]
191
- }
192
- }
193
- };
194
- if (case_filters.content.length) body.case_filters = case_filters;
195
- const { host, headers } = ds.getHostHeaders(q);
196
- try {
197
- const re = await ky.post(joinUrl(host.rest, "files"), { timeout: false, headers, json: body }).json();
198
- console.log("API Response:", {
199
- hits: re.data?.hits?.length || 0,
200
- firstHit: re.data?.hits?.[0]
201
- });
202
- if (!Array.isArray(re.data.hits)) {
203
- throw new Error("API response data.hits is not an array");
204
- }
205
- const cnvFiles = [];
206
- for (const h of re.data.hits) {
207
- if (h.data_format != "TXT") {
208
- continue;
209
- }
210
- if (!h.analysis?.workflow_type) throw "h.analysis.workflow_type missing";
211
- const c = h.cases?.[0];
212
- if (!c) throw "h.cases[0] missing";
213
- if (h.data_type == "Allele-specific Copy Number Segment") {
214
- } else if (h.data_type == "Masked Copy Number Segment" || h.data_type == "Copy Number Segment" && h.analysis.workflow_type != "DNACopy") {
215
- const file = {
216
- id: h.file_id || h.id,
217
- // Handle both field names
218
- project_id: c.project?.project_id || "unknown",
219
- // Safe access with fallback
220
- file_size: h.file_size,
221
- case_submitter_id: c.submitter_id,
222
- case_uuid: c.case_id,
223
- sample_types: c.samples?.map((s) => s.tissue_type).filter(Boolean) || []
224
- };
225
- cnvFiles.push(file);
226
- }
227
- }
228
- result.cnvFiles = { files: cnvFiles, maxTotalSizeCompressed };
229
- console.log(`Successfully processed ${cnvFiles.length} CNV files`);
230
- } catch (error) {
231
- console.error("Error fetching CNV files:", error);
232
- result.cnvFiles = { files: [], maxTotalSizeCompressed };
233
- }
234
- }
235
- export {
236
- init,
237
- maxTotalSizeCompressed
238
- };
@@ -1,127 +0,0 @@
1
- import { run_rust } from "@sjcrh/proteinpaint-rust";
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
- const MAX_RECORD = 1e5;
7
- function init({ genomes }) {
8
- return async (req, res) => {
9
- try {
10
- await runGrin2(genomes, req, res);
11
- } catch (e) {
12
- console.error("[GRIN2] Error stack:", e.stack);
13
- const errorResponse = {
14
- status: "error",
15
- error: e.message || String(e)
16
- };
17
- res.status(500).send(errorResponse);
18
- }
19
- };
20
- }
21
- async function runGrin2(genomes, req, res) {
22
- const g = genomes.hg38;
23
- if (!g) throw "hg38 missing";
24
- const ds = g.datasets.GDC;
25
- if (!ds) throw "hg38 GDC missing";
26
- const parsedRequest = req.query;
27
- const rustInput = {
28
- caseFiles: parsedRequest.caseFiles,
29
- mafOptions: parsedRequest.mafOptions,
30
- cnvOptions: parsedRequest.cnvOptions,
31
- chromosomes: [],
32
- max_record: MAX_RECORD
33
- };
34
- const pyInput = {
35
- genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
36
- chromosomelist: {},
37
- lesion: "",
38
- devicePixelRatio: parsedRequest.devicePixelRatio || 2,
39
- plot_width: parsedRequest.plot_width || 1e3,
40
- plot_height: parsedRequest.plot_height || 400,
41
- pngDotRadius: parsedRequest.pngDotRadius || 2
42
- };
43
- for (const c in g.majorchr) {
44
- if (ds.queries.singleSampleMutation?.discoPlot?.skipChrM) {
45
- if (c.toLowerCase() == "chrm") continue;
46
- }
47
- rustInput.chromosomes.push(c);
48
- pyInput.chromosomelist[c] = g.majorchr[c];
49
- }
50
- const downloadStartTime = Date.now();
51
- const rustOutput = await run_rust("gdcGRIN2", JSON.stringify(rustInput));
52
- mayLog("[GRIN2] Rust execution completed");
53
- const downloadTime = Date.now() - downloadStartTime;
54
- const downloadTimeToPrint = Math.round(downloadTime / 1e3);
55
- mayLog(`[GRIN2] Rust processing took ${downloadTimeToPrint} seconds`);
56
- const parsedRustResult = parseRustOutput(rustOutput);
57
- if (!parsedRustResult) {
58
- throw new Error("Failed to process files: No result from Rust");
59
- }
60
- if (parsedRustResult.successful_data !== void 0 && parsedRustResult.successful_data !== null) {
61
- pyInput.lesion = parsedRustResult.successful_data;
62
- mayLog(`[GRIN2] Extracted ${parsedRustResult.successful_data.length.toLocaleString()} characters for python script`);
63
- mayLog(
64
- `[GRIN2] Success: ${parsedRustResult.summary.successful_files.toLocaleString()}, Failed: ${parsedRustResult.summary.failed_files.toLocaleString()}`
65
- );
66
- } else {
67
- throw "No successful data returned from Rust processing";
68
- }
69
- const grin2AnalysisStart = Date.now();
70
- const pyResult = await run_python("grin2PpWrapperGDC.py", JSON.stringify(pyInput));
71
- if (pyResult.stderr?.trim()) {
72
- mayLog(`[GRIN2] Python stderr: ${pyResult.stderr}`);
73
- }
74
- const grin2AnalysisTime = Date.now() - grin2AnalysisStart;
75
- const grin2AnalysisTimeToPrint = Math.round(grin2AnalysisTime / 1e3);
76
- mayLog(`[GRIN2] Python processing took ${grin2AnalysisTimeToPrint} seconds`);
77
- const resultData = JSON.parse(pyResult);
78
- const pngImg = resultData.png[0];
79
- const topGeneTable = resultData.topGeneTable || null;
80
- const totalProcessTime = downloadTimeToPrint + grin2AnalysisTimeToPrint;
81
- return res.json({
82
- resultData,
83
- pngImg,
84
- topGeneTable,
85
- rustResult: parsedRustResult,
86
- timing: {
87
- rustProcessingTime: downloadTimeToPrint,
88
- grin2ProcessingTime: grin2AnalysisTimeToPrint,
89
- totalTime: totalProcessTime
90
- },
91
- status: "success"
92
- });
93
- }
94
- function parseRustOutput(rustOutput) {
95
- try {
96
- const rustResult = JSON.parse(rustOutput);
97
- if (!rustResult.grin2lesion || !rustResult.summary) {
98
- throw new Error("Invalid Rust output: missing lesion data or summary");
99
- }
100
- mayLog(`[GRIN2] Parsed Rust output successfully`);
101
- return {
102
- successful_data: rustResult.grin2lesion,
103
- failed_files: rustResult.summary.errors || [],
104
- summary: {
105
- type: "summary",
106
- total_files: rustResult.summary.total_files,
107
- successful_files: rustResult.summary.successful_files,
108
- failed_files: rustResult.summary.failed_files,
109
- errors: rustResult.summary.errors || [],
110
- filtered_records: rustResult.summary.filtered_records || 0,
111
- filtered_maf_records: rustResult.summary.filtered_maf_records || 0,
112
- filtered_cnv_records: rustResult.summary.filtered_cnv_records || 0,
113
- included_maf_records: rustResult.summary.included_maf_records || 0,
114
- included_cnv_records: rustResult.summary.included_cnv_records || 0,
115
- filtered_records_by_case: rustResult.summary.filtered_records_by_case || {},
116
- hyper_mutator_records: rustResult.summary.hyper_mutator_records || {},
117
- excluded_by_max_record: rustResult.summary.excluded_by_max_record || {},
118
- skippedChromosomes: rustResult.summary.skipped_chromosomes || {}
119
- }
120
- };
121
- } catch (error) {
122
- throw new Error(`Failed to parse Rust output: ${error instanceof Error ? error.message : "Unknown error"}`);
123
- }
124
- }
125
- export {
126
- init
127
- };