@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.
- package/dataset/termdb.test.js +3 -0
- package/package.json +4 -4
- package/src/app.js +5188 -4980
- package/routes/gdc.grin2.list.js +0 -238
- package/routes/gdc.grin2.run.js +0 -127
package/routes/gdc.grin2.list.js
DELETED
|
@@ -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
|
-
};
|
package/routes/gdc.grin2.run.js
DELETED
|
@@ -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
|
-
};
|