Skip to content

Commit cbfcadb

Browse files
authored
Merge pull request #466 from bcgsc/feat/DEVSU-2745-improve-rapid-report-variants-tests
cleanup variant fetching code and add tests
2 parents 7fbfc06 + db7c593 commit cbfcadb

3 files changed

Lines changed: 801 additions & 326 deletions

File tree

app/libs/createReport.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ const createReportGenes = async (report, content, options = {}) => {
207207
for (const gene of genes) {
208208
geneDefns[gene.name] = gene.id;
209209
}
210-
211210
return geneDefns;
212211
} catch (error) {
213212
throw new Error(`Unable to create report genes ${error.message || error}`);
@@ -234,7 +233,10 @@ const updateKbMatchesInputFormat = (content) => {
234233
if (!('kbMatchedStatements' in content)) {
235234
content.kbMatchedStatements = [];
236235
}
237-
content.kbMatchedStatements.push(statement);
236+
const stmtIds = content.kbMatchedStatements.map((s) => {return s.kbStatementId;});
237+
if (!stmtIds.includes(statement.kbStatementId)) {
238+
content.kbMatchedStatements.push(statement);
239+
}
238240

239241
const conditionSet = {
240242
kbStatementId: item.kbStatementId,
@@ -246,7 +248,17 @@ const updateKbMatchesInputFormat = (content) => {
246248
if (!('kbStatementMatchedConditions' in content)) {
247249
content.kbStatementMatchedConditions = [];
248250
}
249-
content.kbStatementMatchedConditions.push(conditionSet);
251+
const exists = content.kbStatementMatchedConditions.some((cs) => {
252+
return cs.kbStatementId === conditionSet.kbStatementId
253+
&& cs.matchedConditions.length === conditionSet.matchedConditions.length
254+
&& cs.matchedConditions.every((mc, index) => {
255+
return mc.observedVariantKey === conditionSet.matchedConditions[index].observedVariantKey
256+
&& mc.kbVariantId === conditionSet.matchedConditions[index].kbVariantId;
257+
});
258+
});
259+
if (!exists) {
260+
content.kbStatementMatchedConditions.push(conditionSet);
261+
}
250262
}
251263
});
252264
}
@@ -312,6 +324,7 @@ const createReportVariantsSection = async (reportId, genesRecordsByName, modelNa
312324
const createReportVariantSections = async (report, content, transaction) => {
313325
// create the genes first since they will need to be linked to the variant records
314326
const geneDefns = await createReportGenes(report, content, {transaction});
327+
315328
// create the variants and create a mapping from their input 'key' value to the new records
316329
const variantMapping = {};
317330
const variantPromises = Object.keys(KB_PIVOT_MAPPING).map(async (variantType) => {
@@ -321,8 +334,8 @@ const createReportVariantSections = async (report, content, transaction) => {
321334
if (!Array.isArray(content[variantModel]) && typeof content[variantModel] === 'object') {
322335
content[variantModel] = [content[variantModel]];
323336
}
324-
const mapping = await createReportVariantsSection(report.id, geneDefns, variantModel, content[variantModel] || [], {transaction});
325337

338+
const mapping = await createReportVariantsSection(report.id, geneDefns, variantModel, content[variantModel] || [], {transaction});
326339
variantMapping[variantType] = mapping;
327340
});
328341
// create the probe results (linked to gene but not to kbMatches)

app/routes/report/variants.js

Lines changed: 102 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const express = require('express');
33

44
const router = express.Router({mergeParams: true});
55

6-
const {Op, literal} = require('sequelize');
6+
const {literal} = require('sequelize');
77
const db = require('../../models');
88
const logger = require('../../log');
99

@@ -13,6 +13,7 @@ const KBMATCHEXCLUDE = ['id', 'reportId', 'variantId', 'deletedAt', 'updatedBy']
1313
const OBSVARANNOTEXCLUDE = KBMATCHEXCLUDE;
1414
const STATEMENTEXCLUDE = ['id', 'reportId', 'deletedAt', 'updatedBy'];
1515
const MUTATION_REGEX = '^([^\\s]+)(\\s)(mutation[s]?)?(missense)?$';
16+
const MSI_CUTOFF = 20;
1617

1718
const getVariants = async (tableName, variantType, reportId) => {
1819
return db.models[tableName].scope('extended').findAll({
@@ -38,87 +39,66 @@ const getVariants = async (tableName, variantType, reportId) => {
3839
};
3940

4041
const unknownSignificanceIncludes = ['mut'];
41-
const signatureVariant = ['tmb', 'msi', 'sigv'];
42-
43-
const unknownSignificanceGeneFilter = {
44-
[Op.or]: [{oncogene: true}, {tumourSuppressor: true}, {cancerGeneListMatch: true}],
45-
};
42+
const cancerRelevanceOmits = ['exp'];
43+
const geneLinkedVariantTypes = ['mut', 'cnv', 'exp'];
4644

4745
const getRapidReportVariants = async (tableName, variantType, reportId, rapidTable) => {
4846
let allKbMatches;
49-
if (
50-
unknownSignificanceIncludes.includes(variantType)
51-
&& !signatureVariant.includes(variantType)
52-
) {
53-
allKbMatches = await db.models[tableName].scope('extended').findAll({
54-
order: [['id', 'ASC']],
55-
attributes: {
56-
include: [[literal(`'${variantType}'`), 'variantType']],
57-
},
58-
where: {
59-
reportId,
60-
},
61-
include: [
62-
{
63-
model: db.models.kbMatches,
64-
attributes: {exclude: KBMATCHEXCLUDE},
65-
include: [
66-
{
67-
model: db.models.kbMatchedStatements,
68-
as: 'kbMatchedStatements',
69-
attributes: {
70-
exclude: STATEMENTEXCLUDE,
71-
},
72-
// where: {
73-
// ...therapeuticAssociationFilterStatement,
74-
// },
75-
through: {attributes: ['flags']},
47+
48+
const query = {
49+
order: [['id', 'ASC']],
50+
attributes: {
51+
include: [[literal(`'${variantType}'`), 'variantType']],
52+
},
53+
where: {
54+
reportId,
55+
},
56+
include: [
57+
{
58+
model: db.models.kbMatches,
59+
attributes: {exclude: KBMATCHEXCLUDE},
60+
include: [
61+
{
62+
model: db.models.kbMatchedStatements,
63+
as: 'kbMatchedStatements',
64+
attributes: {
65+
exclude: STATEMENTEXCLUDE,
7666
},
77-
],
78-
},
79-
{
80-
model: db.models.observedVariantAnnotations,
81-
attributes: {exclude: OBSVARANNOTEXCLUDE},
82-
as: 'observedVariantAnnotation',
83-
},
84-
{
85-
model: db.models.genes.scope('minimal'),
86-
as: 'gene',
87-
where: unknownSignificanceGeneFilter, // TODO does including this remove results that would otherwise be in allKbMatches
88-
},
89-
],
90-
});
91-
} else {
92-
allKbMatches = await db.models[tableName].scope('extended').findAll({
93-
order: [['id', 'ASC']],
94-
attributes: {
95-
include: [[literal(`'${variantType}'`), 'variantType']],
67+
through: {attributes: ['flags']},
68+
},
69+
],
9670
},
97-
where: {
98-
reportId,
71+
{
72+
model: db.models.observedVariantAnnotations,
73+
attributes: {exclude: OBSVARANNOTEXCLUDE},
74+
as: 'observedVariantAnnotation',
9975
},
100-
include: [
101-
{
102-
model: db.models.kbMatches,
103-
attributes: {exclude: KBMATCHEXCLUDE},
104-
include: [
105-
{
106-
model: db.models.kbMatchedStatements,
107-
as: 'kbMatchedStatements',
108-
attributes: {
109-
exclude: STATEMENTEXCLUDE,
110-
},
111-
through: {attributes: ['flags']},
112-
},
113-
],
114-
},
115-
{
116-
model: db.models.observedVariantAnnotations,
117-
attributes: {exclude: OBSVARANNOTEXCLUDE},
118-
as: 'observedVariantAnnotation',
119-
},
120-
],
121-
});
76+
],
77+
};
78+
if (geneLinkedVariantTypes.includes(variantType)) {
79+
const geneSubquery = {
80+
model: db.models.genes.scope('minimal'),
81+
as: 'gene',
82+
};
83+
query.include.push(geneSubquery);
84+
}
85+
86+
// if the table is unknownSignificance, only get subset of variant types
87+
if (rapidTable === 'unknownSignificance') {
88+
if (unknownSignificanceIncludes.includes(variantType)) {
89+
allKbMatches = await db.models[tableName].scope('extended').findAll(query);
90+
}
91+
} else if (rapidTable === 'cancerRelevance') {
92+
// exclude expression variants from cancer relevance table
93+
if (!cancerRelevanceOmits.includes(variantType)) {
94+
allKbMatches = await db.models[tableName].scope('extended').findAll(query);
95+
}
96+
} else {
97+
allKbMatches = await db.models[tableName].scope('extended').findAll(query);
98+
}
99+
100+
if (!allKbMatches) {
101+
return [];
122102
}
123103

124104
// do initial filtering based on contents of annotations - these should override defaults
@@ -159,6 +139,7 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
159139
const variantRegexMatch = item.kbVariant.match(MUTATION_REGEX);
160140
return !(variantRegexMatch);
161141
});
142+
162143
return variant;
163144
});
164145

@@ -181,6 +162,7 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
181162
kbmatch.kbMatchedStatements = statements;
182163
return kbmatch;
183164
});
165+
184166
return variant;
185167
});
186168

@@ -191,12 +173,14 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
191173
.filter((item) => {return item !== null;});
192174
return kbmatch.kbMatchedStatements.length > 0;
193175
});
176+
194177
return variant;
195178
});
196179

197180
// remove variants which have no matches
198181
therapeuticAssociationResults = therapeuticAssociationResults.filter((variant) => {
199182
const kbmatches = variant.kbMatches.filter((item) => {return item !== null;});
183+
200184
return kbmatches.length > 0;
201185
});
202186
}
@@ -209,35 +193,37 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
209193

210194
let cancerRelevanceResults = [];
211195
const cancerRelevanceResultsFiltered = [];
212-
if (!(variantType === 'exp')) { // omit expression variants from this table
213-
cancerRelevanceResults = JSON.parse(JSON.stringify(allKbMatches));
196+
cancerRelevanceResults = JSON.parse(JSON.stringify(allKbMatches));
214197

215-
// remove nonmatching kbmatches
216-
cancerRelevanceResults = cancerRelevanceResults.map((variant) => {
217-
const kbmatches = variant.kbMatches.filter((item) => {
218-
const msiMatch = (item.variantType === 'msi' && item.score >= 20);
219-
if (msiMatch) {
220-
return true;
221-
}
222-
const variantRegexMatch = item.kbVariant.match(MUTATION_REGEX);
223-
return (!variantRegexMatch);
224-
});
225-
variant.kbMatches = kbmatches;
226-
return variant;
227-
});
198+
// remove msi variants below cutoff
199+
cancerRelevanceResults = cancerRelevanceResults.filter((variant) => {
200+
if (variantType === 'msi') {
201+
return (variant.score >= MSI_CUTOFF);
202+
}
203+
return true;
204+
});
228205

229-
// remove variants which now have no matches
230-
cancerRelevanceResults = cancerRelevanceResults.filter((variant) => {
231-
const kbmatches = variant.kbMatches.filter((item) => {return item !== null;});
232-
return kbmatches.length > 0;
206+
// remove kbmatches that fail the regex
207+
cancerRelevanceResults = cancerRelevanceResults.map((variant) => {
208+
const kbmatches = variant.kbMatches.filter((item) => {
209+
const variantRegexMatch = item.kbVariant.match(MUTATION_REGEX);
210+
return (!variantRegexMatch);
233211
});
212+
variant.kbMatches = kbmatches;
213+
return variant;
214+
});
234215

235-
for (const row of cancerRelevanceResults) {
236-
if (!(therapeuticAssociationResults.find(
237-
(e) => {return e.ident === row.ident;},
238-
))) {
239-
cancerRelevanceResultsFiltered.push(row);
240-
}
216+
// remove variants which now have no matches
217+
cancerRelevanceResults = cancerRelevanceResults.filter((variant) => {
218+
const kbmatches = variant.kbMatches.filter((item) => {return item !== null;});
219+
return kbmatches.length > 0;
220+
});
221+
222+
for (const row of cancerRelevanceResults) {
223+
if (!(therapeuticAssociationResults.find(
224+
(e) => {return e.ident === row.ident;},
225+
))) {
226+
cancerRelevanceResultsFiltered.push(row);
241227
}
242228
}
243229

@@ -254,9 +240,17 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
254240
unknownSignificanceResults = JSON.parse(JSON.stringify(allKbMatches));
255241
}
256242

257-
// refine unknownSignificance results to only include therapeuticAssoc-qualifying matches
243+
// refine unknownSignificance results to only include variants that EITHER:
244+
// a - have a linked gene that is oncogene, cancerGeneList or tumourSuppressor true, OR
245+
// b - therapeuticAssoc-qualifying matches
258246
// (unless they are tagged - add the tagged results back after filtering)
259247
unknownSignificanceResults = unknownSignificanceResults.map((variant) => {
248+
if (
249+
// this is safe to check because variantType is smallMutation which always has a gene
250+
variant.gene.oncogene || variant.gene.cancerGeneListMatch || variant.gene.tumourSuppressor
251+
) {
252+
return variant;
253+
}
260254
variant.kbMatches = variant.kbMatches.map((kbmatch) => {
261255
const statements = kbmatch.kbMatchedStatements.filter((stmt) => {
262256
if ((stmt.category === 'therapeutic')
@@ -276,7 +270,7 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
276270
return variant;
277271
});
278272

279-
// remove matches which have no matching statements
273+
// remove matches which have no matching statements...
280274
unknownSignificanceResults = unknownSignificanceResults.map((variant) => {
281275
variant.kbMatches = variant.kbMatches.filter((kbmatch) => {
282276
kbmatch.kbMatchedStatements = kbmatch.kbMatchedStatements
@@ -286,6 +280,11 @@ const getRapidReportVariants = async (tableName, variantType, reportId, rapidTab
286280
return variant;
287281
});
288282

283+
// remove variants which have no matches
284+
unknownSignificanceResults = unknownSignificanceResults.filter((variant) => {
285+
return variant.kbMatches.length > 0;
286+
});
287+
289288
// remove variants already included in a different section
290289
for (const row of unknownSignificanceResults) {
291290
if (!(therapeuticAssociationResults.find(

0 commit comments

Comments
 (0)