diff --git a/src/cosmic/README.md b/src/cosmic/README.md index 44cc66c2..7a9e2ffe 100644 --- a/src/cosmic/README.md +++ b/src/cosmic/README.md @@ -10,34 +10,38 @@ First the data must be downloaded. This requires an account AUTH=$( echo "$COSMIC_EMAIL:$COSMIC_PASSWORD" | base64 ) # Download resistance mutations -resp=$( curl -H "Authorization: Basic $AUTH" https://cancer.sanger.ac.uk/cosmic/file_download/GRCh38/cosmic/v92/CosmicResistanceMutations.tsv.gz ); +resp=$( curl -H "Authorization: Basic $AUTH" "https://cancer.sanger.ac.uk/api/mono/products/v1/downloads/scripted?path=grch38/cosmic/v103/Cosmic_ResistanceMutations_Tsv_v103_GRCh38.tar&bucket=downloads" ); echo $resp url=$( node -e "var resp = $resp; console.log(resp.url);" ); -curl "$url" -o CosmicResistanceMutations.tsv.gz -gunzip CosmicResistanceMutations.tsv.gz +curl "$url" -o Cosmic_ResistanceMutations_Tsv_v103_GRCh38.tar +tar -xf Cosmic_ResistanceMutations_Tsv_v103_GRCh38.tar +gunzip Cosmic_ResistanceMutations_v103_GRCh38.tsv.gz # Download disease mappings -resp=$( curl -H "Authorization: Basic $AUTH" https://cancer.sanger.ac.uk/cosmic/file_download/GRCh38/cosmic/v92/classification.csv ); +resp=$( curl -H "Authorization: Basic $AUTH" "https://cancer.sanger.ac.uk/api/mono/products/v1/downloads/scripted?path=grch38/cosmic/v103/Cosmic_Classification_Tsv_v103_GRCh38.tar&bucket=downloads" ); echo $resp url=$( node -e "var resp = $resp; console.log(resp.url);" ); -curl "$url" -o classification.csv +curl "$url" -o Cosmic_Classification_Tsv_v103_GRCh38.tar +tar -xf Cosmic_Classification_Tsv_v103_GRCh38.tar +gunzip Cosmic_Classification_v103_GRCh38.tsv.gz # Download fusion files -resp=$( curl -H "Authorization: Basic $AUTH" https://cancer.sanger.ac.uk/cosmic/file_download/GRCh38/cosmic/v92/CosmicFusionExport.tsv.gz ); +resp=$( curl -H "Authorization: Basic $AUTH" "https://cancer.sanger.ac.uk/api/mono/products/v1/downloads/scripted?path=grch38/cosmic/v103/Cosmic_Fusion_Tsv_v103_GRCh38.tar&bucket=downloads" ); echo $resp url=$( node -e "var resp = $resp; console.log(resp.url);" ); -curl "$url" -o CosmicFusionExport.tsv.gz -gunzip CosmicFusionExport.tsv.gz +curl "$url" -o Cosmic_Fusion_Tsv_v103_GRCh38.tar +tar -xf Cosmic_Fusion_Tsv_v103_GRCh38.tar +gunzip Cosmic_Fusion_v103_GRCh38.tsv.gz ``` Since this loader requires 2 files, it is separate from the other more general loaders ```bash -node bin/load.js cosmic resistance CosmicResistanceMutations.tsv classification.csv +node bin/load.js cosmic resistance Cosmic_ResistanceMutations_v103_GRCh38.tsv Cosmic_Classification_v103_GRCh38.tsv ``` And then to load the fusions (Will create recurrency statements) ```bash -node bin/load.js cosmic fusions CosmicFusionExport.tsv classification.csv +node bin/load.js cosmic fusions Cosmic_Fusion_v103_GRCh38.tsv Cosmic_Classification_v103_GRCh38.tsv ``` diff --git a/src/cosmic/fusions.js b/src/cosmic/fusions.js index 2206482e..eec489f1 100644 --- a/src/cosmic/fusions.js +++ b/src/cosmic/fusions.js @@ -19,18 +19,36 @@ const { logger } = require('../logging'); const { cosmic: SOURCE_DEFN } = require('../sources'); const RECURRENCE_THRESHOLD = 3; +const cache = {}; const HEADER = { - disease: 'HISTOLOGY_SUBTYPE_1', - diseaseFamily: 'PRIMARY_HISTOLOGY', - exon1: '5\'_LAST_OBSERVED_EXON', - exon2: '3\'_FIRST_OBSERVED_EXON', - fusionId: 'FUSION_ID', - fusionName: 'TRANSLOCATION_NAME', - gene1: '5\'_GENE_NAME', - gene2: '3\'_GENE_NAME', + diseaseId: 'COSMIC_PHENOTYPE_ID', + exon1: 'FIVE_PRIME_LAST_OBSERVE_EXON', + exon2: 'THREE_PRIME_FIRST_OBSERVE_EXON', + fusionId: 'COSMIC_FUSION_ID', + fusionName: 'FUSION_SYNTAX', + gene1: 'FIVE_PRIME_CHROMOSOME', + gene2: 'THREE_PRIME_CHROMOSOME', pubmed: 'PUBMED_PMID', - sampleId: 'SAMPLE_ID', + sampleId: 'COSMIC_SAMPLE_ID', +}; + +const fetchChromosome = async (conn, sourceId) => { + if (cache[sourceId]) { + return cache[sourceId]; + } + const record = await conn.getUniqueRecordBy({ + filters: { + AND: [ + { sourceId }, + { biotype: 'chromosome' }, + ], + }, + sort: orderPreferredOntologyTerms, + target: 'Feature', + }); + cache[sourceId] = record; + return record; }; @@ -38,10 +56,19 @@ const processVariants = async ({ conn, record, variantType, exonSpecific, }) => { // fetch the features - const [gene1] = await _gene.fetchAndLoadBySymbol(conn, record.gene1); - const [gene2] = await _gene.fetchAndLoadBySymbol(conn, record.gene2); + let [gene1] = await _gene.fetchAndLoadBySymbol(conn, record.gene1), + [gene2] = await _gene.fetchAndLoadBySymbol(conn, record.gene2); // create the variants + if (!gene1) { + gene1 = await fetchChromosome(conn, record.gene1); + } + if (!gene2) { + gene2 = await fetchChromosome(conn, record.gene2); + } + if (!gene1 || !gene2) { + throw new Error(`unable to find genes for record ${record.id}: ${record.gene1}, ${record.gene2}`); + } const general = await conn.addVariant({ content: { reference1: rid(gene1), @@ -93,7 +120,6 @@ const processRecordGroup = async ({ conn, exonSpecific, record: records[0], variantType, }); - // create the recurrence statement await conn.addRecord({ content: { @@ -138,7 +164,7 @@ const uploadFile = async ({ const variantType = rid(await conn.getVocabularyTerm('fusion')); const cancer = rid(await conn.getUniqueRecordBy({ filters: { name: 'cancer' }, sort: orderPreferredOntologyTerms, target: 'Disease' })); - await _pubmed.fetchAndLoadByIds(conn, records.map(rec => rec.pumbed)); + await _pubmed.fetchAndLoadByIds(conn, records.map(rec => rec.pubmed)); const addPropertyMatch = (histogram, object, properties) => { const hashId = hashRecordToId(object, properties); @@ -163,7 +189,9 @@ const uploadFile = async ({ // pre-process/clean records for (const record of records) { record.id = hashRecordToId(record, ['fusionId', 'sampleId']); - record.ncit = (mapping[record.diseaseFamily] || {})[record.disease]; + record.ncit = mapping[record.diseaseId].ncit || ''; + record.disease = mapping[record.diseaseId].disease || ''; + record.diseaseFamily = mapping[record.diseaseId].diseaseFamily || ''; record.disease = record.disease.toUpperCase() === 'NS' ? '' @@ -183,7 +211,9 @@ const uploadFile = async ({ record.variant = `(${record.gene1},${record.gene2}).fus(e.${record.exon1},e.${record.exon2})`; record.nonSpecificVariant = `(${record.gene1},${record.gene2}).fus(e.?,e.?)`; - recurrentProperties.forEach((plist, index) => addPropertyMatch(recurrenceCounts[index], record, plist)); + recurrentProperties.forEach( + (plist, index) => addPropertyMatch(recurrenceCounts[index], record, plist), + ); } const getSampleCount = group => (new Set(group.map(row => row.sampleId))).size; @@ -192,7 +222,7 @@ const uploadFile = async ({ const processed = new Set(); // disease-specific, exon-specific, fusions - for (let index = 0; index <= recurrenceCounts.length; index++) { + for (let index = 0; index < recurrenceCounts.length; index++) { const recurrencyLevel = recurrenceCounts[index]; for (const [groupId, group] of Object.entries(recurrencyLevel)) { diff --git a/src/cosmic/resistance.js b/src/cosmic/resistance.js index 881b04ec..b6030a22 100644 --- a/src/cosmic/resistance.js +++ b/src/cosmic/resistance.js @@ -24,22 +24,20 @@ const { cosmic: SOURCE_DEFN } = require('../sources'); const HEADER = { cds: 'HGVSC', - disease: 'Histology Subtype 1', - diseaseFamily: 'Histology', - gene: 'Gene Name', + diseaseId: 'COSMIC_PHENOTYPE_ID', + gene: 'GENE_SYMBOL', genomic: 'HGVSG', mutationId: 'LEGACY_MUTATION_ID', protein: 'HGVSP', - pubmed: 'Pubmed Id', - sampleId: 'Sample ID', - sampleName: 'Sample Name', - therapy: 'Drug Name', - transcript: 'Transcript', + pubmed: 'PUBMED_PMID', + sampleName: 'SAMPLE_NAME', + therapy: 'DRUG_NAME', + transcript: 'TRANSCRIPT_ACCESSION', }; /** - * Create and link the variant defuinitions for a single row/record + * Create and link the variant definitions for a single row/record */ const processVariants = async ({ conn, record, source }) => { let protein, @@ -71,43 +69,45 @@ const processVariants = async ({ conn, record, source }) => { throw Error(`failed to find the HGNC gene for ${record.gene}`); } } catch (err) { - logger.error(err); + logger.warn(err); } } - try { - // add the protein variant with its protein translation - const variant = jsonifyVariant(parseVariant(record.protein, false)); - variant.type = rid(await conn.getVocabularyTerm(variant.type)); - - const reference1 = rid(await _ensembl.fetchAndLoadById( - conn, - { biotype: 'protein', sourceId: variant.reference1 }, - )); - protein = rid(await conn.addVariant({ - content: { ...variant, reference1 }, - existsOk: true, - target: 'PositionalVariant', - })); - - if (gene) { - // add the same protein varaint with the gene notation - generalProtein = rid(await conn.addVariant({ - content: { ...variant, reference1: gene }, + if (record.protein && record.protein.trim()) { + try { + // add the protein variant with its protein translation + const variant = jsonifyVariant(parseVariant(record.protein, false)); + variant.type = rid(await conn.getVocabularyTerm(variant.type)); + + const reference1 = rid(await _ensembl.fetchAndLoadById( + conn, + { biotype: 'protein', sourceId: variant.reference1 }, + )); + protein = rid(await conn.addVariant({ + content: { ...variant, reference1 }, existsOk: true, target: 'PositionalVariant', })); - // link the translation version to the gene version - await conn.addRecord({ - content: { in: generalProtein, out: protein }, - existsOk: true, - fetchExisting: false, - target: 'Infers', - }); + if (gene) { + // add the same protein variant with the gene notation + generalProtein = rid(await conn.addVariant({ + content: { ...variant, reference1: gene }, + existsOk: true, + target: 'PositionalVariant', + })); + + // link the translation version to the gene version + await conn.addRecord({ + content: { in: generalProtein, out: protein }, + existsOk: true, + fetchExisting: false, + target: 'Infers', + }); + } + } catch (err) { + logger.error(err); } - } catch (err) { - logger.error(err); } // create the cds variant @@ -299,17 +299,20 @@ const processCosmicRecord = async (conn, record, source) => { * Disease mappings */ const loadClassifications = async (filename) => { - const classifications = await loadDelimToJson(filename, { delim: ',' }); + const classifications = await loadDelimToJson(filename); const mapping = {}; for (const row of classifications) { - const disease = row.HISTOLOGY_COSMIC; - const subdisease = row.HIST_SUBTYPE1_COSMIC; + const diseaseId = row.COSMIC_PHENOTYPE_ID; - if (!mapping[disease]) { - mapping[disease] = {}; + if (!mapping[diseaseId]) { + mapping[diseaseId] = {}; } - mapping[disease][subdisease] = row.NCI_CODE; + mapping[diseaseId] = { + disease: row.HISTOLOGY_SUBTYPE_1, + diseaseFamily: row.PRIMARY_HISTOLOGY, + ncit: row.NCI_CODE, + }; } return mapping; }; @@ -337,7 +340,7 @@ const uploadFile = async ({ filters: [ { source }, { relevance }, - { createdBy: { filters: { name: conn.username }, target: 'User' } }, + { createdBy: { filters: { name: 'graphkb_importer' }, target: 'User' } }, ], returnProperties: ['@rid'], target: 'Statement', @@ -350,6 +353,7 @@ const uploadFile = async ({ const errorList = []; logger.info(`Processing ${jsonList.length} records`); // Upload the list of pubmed IDs + await _pubmed.preLoadCache(conn); await _pubmed.fetchAndLoadByIds(conn, jsonList.map(rec => rec[HEADER.pubmed]), { upsert: true }); for (let index = 0; index < jsonList.length; index++) { @@ -367,7 +371,10 @@ const uploadFile = async ({ } try { - record.ncit = (mapping[record.diseaseFamily] || {})[record.disease]; + const diseaseMapping = mapping[record.diseaseId] || {}; + record.ncit = diseaseMapping.ncit || ''; + record.disease = diseaseMapping.disease || ''; + record.diseaseFamily = diseaseMapping.diseaseFamily || ''; record.publication = rid((await _pubmed.fetchAndLoadByIds(conn, [record.pubmed]))[0]); const statement = await processCosmicRecord(conn, record, source); diff --git a/src/entrez/pubmed.js b/src/entrez/pubmed.js index f1adbbb6..26e461b0 100644 --- a/src/entrez/pubmed.js +++ b/src/entrez/pubmed.js @@ -71,9 +71,17 @@ const createDisplayName = sourceId => `pmid:${sourceId}`; * @param {Array.} idList list of pubmed IDs */ const fetchAndLoadByIds = async (api, idListIn, opt = {}) => { - const pmcIds = idListIn.filter(id => /^pmc\d+$/i.exec(id)).map(id => id.replace(/^pmc/i, '')); + // hardcoded fix for a record that was deleted from pubmed after being cited in a paper + // https://pubmed.ncbi.nlm.nih.gov/21656749/ + const idList = idListIn.map((id) => { + if (id === '21225871') { + return '21656749'; + } + return id; + }); + const pmcIds = idList.filter(id => /^pmc\d+$/i.exec(id)).map(id => id.replace(/^pmc/i, '')); const records = await fetchByIdList( - idListIn.filter(id => !/^pmc\d+$/i.exec(id)), + idList.filter(id => !/^pmc\d+$/i.exec(id)), { cache: CACHE, db: DB_NAME, parser: parseRecord, }, diff --git a/src/entrez/util.js b/src/entrez/util.js index 8dc7bcd4..deb683e0 100644 --- a/src/entrez/util.js +++ b/src/entrez/util.js @@ -91,6 +91,11 @@ const fetchByIdList = async (rawIdList, opt) => { Object.keys(result).filter(k => k !== 'uids').forEach((key) => { const rec = result[key]; + if (rec.error) { + logger.error(`error fetching record for id ${key}: ${rec.error}`); + return; + } + try { records.push(parser(rec)); } catch (err) { diff --git a/src/hgnc/index.js b/src/hgnc/index.js index d1b59a0c..d3eac186 100644 --- a/src/hgnc/index.js +++ b/src/hgnc/index.js @@ -218,6 +218,10 @@ const fetchAndLoadBySymbol = async ({ uri, }); + if (!docs || docs.length === 0) { + throw new Error(`No HGNC record found for ${paramType}: ${symbol}`); + } + for (const record of docs) { checkSpec(validateHgncSpec, record, rec => rec.hgnc_id); } @@ -238,7 +242,9 @@ const fetchAndLoadBySymbol = async ({ filters: { name: ensemblSourceName }, target: 'Source', }); - } catch (err) { } + } catch (err) { + logger.info('Unable to fetch ensembl source for linking records'); + } const result = await uploadRecord({ conn, deprecated: ( diff --git a/test/cosmic.fusions.test.js b/test/cosmic.fusions.test.js new file mode 100644 index 00000000..a374e247 --- /dev/null +++ b/test/cosmic.fusions.test.js @@ -0,0 +1,134 @@ +const path = require('path'); + +jest.mock('../src/util', () => { + const original = jest.requireActual('../src/util'); + return { + ...original, + loadDelimToJson: jest.fn(), + }; +}); + +jest.mock('../src/entrez/pubmed', () => ({ + fetchAndLoadByIds: jest.fn(), +})); + +jest.mock('../src/entrez/gene', () => ({ + fetchAndLoadBySymbol: jest.fn(), +})); + +jest.mock('../src/cosmic/resistance', () => ({ + loadClassifications: jest.fn(), + processDisease: jest.fn(), +})); + +const util = require('../src/util'); +const pubmed = require('../src/entrez/pubmed'); +const gene = require('../src/entrez/gene'); +const resistance = require('../src/cosmic/resistance'); +const fusions = require('../src/cosmic/fusions'); + +const utilActual = jest.requireActual('../src/util'); + +describe('cosmic fusions', () => { + const mockConn = () => ({ + addRecord: jest.fn().mockResolvedValue({ '@rid': '#153:0' }), + addSource: jest.fn().mockResolvedValue({ '@rid': '#40:1' }), + addVariant: jest.fn().mockResolvedValue({ '@rid': '#23:4' }), + getUniqueRecordBy: jest.fn().mockResolvedValue({ '@rid': '#133:8' }), + getVocabularyTerm: jest.fn().mockImplementation(async term => ({ '@rid': `#vocab:${term}` })), + }); + + const loadFixtureTsv = filename => utilActual.loadDelimToJson( + path.join(__dirname, 'data', filename), + ); + + const classificationRowsToMap = rows => rows.reduce((mapping, row) => ({ + ...mapping, + [row.COSMIC_PHENOTYPE_ID]: { + disease: row.HISTOLOGY_SUBTYPE_1, + diseaseFamily: row.PRIMARY_HISTOLOGY, + ncit: row.NCI_CODE, + }, + }), {}); + + let fixtureRows = []; + + beforeEach(async () => { + fixtureRows = await loadFixtureTsv('cosmic_fusion.tsv'); + const classificationRows = await loadFixtureTsv('cosmic_classification.tsv'); + const classificationMap = classificationRowsToMap(classificationRows); + + gene.fetchAndLoadBySymbol.mockImplementation(async (_conn, symbol) => [{ '@rid': `#gene:${symbol}` }]); + pubmed.fetchAndLoadByIds.mockImplementation(async (_conn, ids) => ids.map(id => ({ '@rid': `#pubmed:${id}` }))); + resistance.loadClassifications.mockResolvedValue(classificationMap); + resistance.processDisease.mockResolvedValue({ '@rid': '#133:99' }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('does not create statement below recurrence threshold', async () => { + const conn = mockConn(); + util.loadDelimToJson.mockResolvedValueOnce(fixtureRows); + + await fusions.uploadFile({ classification: 'classification.tsv', conn, filename: 'fusion.tsv' }); + + expect(conn.addSource).toHaveBeenCalledWith(fusions.SOURCE_DEFN); + expect(pubmed.fetchAndLoadByIds).toHaveBeenCalledWith(conn, ['23405175']); + + const statementCalls = conn.addRecord.mock.calls.filter(([arg]) => arg.target === 'Statement'); + expect(statementCalls).toHaveLength(0); + }); + + test('creates recurrence statement when non-specific fusion reaches threshold', async () => { + const conn = mockConn(); + const [baseRow] = fixtureRows; + const rows = [ + { + ...baseRow, + COSMIC_SAMPLE_ID: 'S1', + FIVE_PRIME_LAST_OBSERVE_EXON: '1', + THREE_PRIME_FIRST_OBSERVE_EXON: '2', + }, + { + ...baseRow, + COSMIC_SAMPLE_ID: 'S2', + FIVE_PRIME_LAST_OBSERVE_EXON: '3', + THREE_PRIME_FIRST_OBSERVE_EXON: '4', + }, + { + ...baseRow, + COSMIC_SAMPLE_ID: 'S3', + FIVE_PRIME_LAST_OBSERVE_EXON: '5', + THREE_PRIME_FIRST_OBSERVE_EXON: '6', + }, + ]; + util.loadDelimToJson.mockResolvedValueOnce(rows); + + await fusions.uploadFile({ classification: 'classification.tsv', conn, filename: 'fusion.tsv' }); + + const statementCalls = conn.addRecord.mock.calls.filter(([arg]) => arg.target === 'Statement'); + expect(statementCalls).toHaveLength(1); + expect(statementCalls[0][0]).toEqual(expect.objectContaining({ + content: expect.objectContaining({ + relevance: '#vocab:recurrent', + subject: '#133:99', + }), + target: 'Statement', + })); + + expect(conn.addVariant).toHaveBeenCalledWith({ + content: { + reference1: '#gene:', + reference2: '#gene:', + type: '#vocab:fusion', + }, + existsOk: true, + target: 'CategoryVariant', + }); + expect(pubmed.fetchAndLoadByIds).toHaveBeenCalledWith(conn, ['23405175', '23405175', '23405175']); + expect(pubmed.fetchAndLoadByIds).toHaveBeenCalledWith(conn, ['23405175']); + expect(resistance.processDisease).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/cosmic.resistance.test.js b/test/cosmic.resistance.test.js new file mode 100644 index 00000000..7a58f1dc --- /dev/null +++ b/test/cosmic.resistance.test.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const path = require('path'); + +jest.mock('../src/util', () => { + const original = jest.requireActual('../src/util'); + return { + ...original, + loadDelimToJson: jest.fn(), + }; +}); + +const util = require('../src/util'); +const pubmed = require('../src/entrez/pubmed'); +const resistance = require('../src/cosmic/resistance'); + +const utilActual = jest.requireActual('../src/util'); + +describe('cosmic resistance', () => { + const mockConn = () => ({ + addSource: jest.fn().mockResolvedValue({ '@rid': '#40:1' }), + deleteRecord: jest.fn().mockResolvedValue(null), + getRecords: jest.fn().mockResolvedValue([]), + getUniqueRecordBy: jest.fn().mockResolvedValue({ '@rid': '#133:8' }), + getVocabularyTerm: jest.fn().mockResolvedValue({ '@rid': '#106:3' }), + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const loadFixtureTsv = filename => utilActual.loadDelimToJson( + path.join(__dirname, 'data', filename), + ); + + describe('processDisease', () => { + test('prefers ncit mapped disease', async () => { + const conn = mockConn(); + const record = { + disease: 'acute_myeloid_leukaemia', + diseaseFamily: 'haematopoietic_neoplasm', + ncit: 'C3171', + }; + + const result = await resistance.processDisease(conn, record); + + expect(result).toEqual({ '@rid': '#133:8' }); + expect(conn.getUniqueRecordBy).toHaveBeenCalledTimes(1); + expect(conn.getUniqueRecordBy).toHaveBeenCalledWith(expect.objectContaining({ + target: 'Disease', + })); + }); + + test('falls back to cleaned disease name when ncit lookup fails', async () => { + const conn = mockConn(); + conn.getUniqueRecordBy + .mockRejectedValueOnce(new Error('missing ncit')) + .mockResolvedValueOnce({ '@rid': '#133:9' }); + + const record = { + disease: 'acute_myeloid_leukaemia', + diseaseFamily: 'haematopoietic_tumour', + ncit: 'C3171', + }; + + const result = await resistance.processDisease(conn, record); + + expect(result).toEqual({ '@rid': '#133:9' }); + expect(conn.getUniqueRecordBy).toHaveBeenNthCalledWith(2, { + filters: { name: 'acute myeloid leukemia' }, + sort: expect.any(Function), + target: 'Disease', + }); + }); + + test('falls back to disease family when disease is NS', async () => { + const conn = mockConn(); + conn.getUniqueRecordBy + .mockRejectedValueOnce(new Error('missing ncit')) + .mockResolvedValueOnce({ '@rid': '#133:10' }); + + const record = { + disease: 'NS', + diseaseFamily: 'solid_tumour', + ncit: 'C0000', + }; + + const result = await resistance.processDisease(conn, record); + + expect(result).toEqual({ '@rid': '#133:10' }); + expect(conn.getUniqueRecordBy).toHaveBeenNthCalledWith(2, { + filters: { name: 'solid tumor' }, + sort: expect.any(Function), + target: 'Disease', + }); + }); + }); + + describe('uploadFile', () => { + test('preloads pubmed, deletes stale statements, and writes error json', async () => { + const conn = mockConn(); + conn.getRecords.mockResolvedValue([{ '@rid': '#153:0' }, { '@rid': '#153:1' }]); + const resistanceRows = await loadFixtureTsv('cosmic_resistanceMutations.tsv'); + const classificationRows = await loadFixtureTsv('cosmic_classification.tsv'); + + util.loadDelimToJson + .mockResolvedValueOnce(resistanceRows) + .mockResolvedValueOnce(classificationRows); + const preLoadSpy = jest.spyOn(pubmed, 'preLoadCache').mockResolvedValue(); + const fetchPubmedSpy = jest.spyOn(pubmed, 'fetchAndLoadByIds').mockResolvedValue([]); + const writeSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => null); + + await resistance.uploadFile({ + classification: 'classification.tsv', + conn, + errorLogPrefix: 'errorLog-123', + filename: 'resistance.tsv', + maxRecords: -1, + }); + + expect(util.loadDelimToJson).toHaveBeenCalledTimes(2); + expect(preLoadSpy).toHaveBeenCalledWith(conn); + expect(fetchPubmedSpy).toHaveBeenCalledWith(conn, ['16983123', '16983123', '16983123'], { upsert: true }); + expect(conn.deleteRecord).toHaveBeenCalledTimes(2); + expect(conn.deleteRecord).toHaveBeenCalledWith('Statement', '#153:0'); + expect(conn.deleteRecord).toHaveBeenCalledWith('Statement', '#153:1'); + expect(writeSpy).toHaveBeenCalledWith( + 'errorLog-123-cosmic.json', + expect.stringContaining('"records": []'), + ); + }); + }); +}); diff --git a/test/data/cosmic_classification.tsv b/test/data/cosmic_classification.tsv new file mode 100644 index 00000000..b671deb5 --- /dev/null +++ b/test/data/cosmic_classification.tsv @@ -0,0 +1,2 @@ +COSMIC_PHENOTYPE_ID PRIMARY_SITE SITE_SUBTYPE_1 SITE_SUBTYPE_2 SITE_SUBTYPE_3 PRIMARY_HISTOLOGY HISTOLOGY_SUBTYPE_1 HISTOLOGY_SUBTYPE_2 HISTOLOGY_SUBTYPE_3 NCI_CODE EFO +COSO29974826 lung right_lower_lobe NS NS carcinoma adenocarcinoma NS NS C3512 http://www.ebi.ac.uk/efo/EFO_0000571 diff --git a/test/data/cosmic_fusion.tsv b/test/data/cosmic_fusion.tsv new file mode 100644 index 00000000..f6bfc6c5 --- /dev/null +++ b/test/data/cosmic_fusion.tsv @@ -0,0 +1,2 @@ +COSMIC_SAMPLE_ID SAMPLE_NAME COSMIC_PHENOTYPE_ID COSMIC_FUSION_ID FUSION_SYNTAX FIVE_PRIME_CHROMOSOME FIVE_PRIME_STRAND FIVE_PRIME_TRANSCRIPT_ID FIVE_PRIME_GENE_SYMBOL FIVE_PRIME_LAST_OBSERVE_EXON FIVE_PRIME_GENOME_START_FROM FIVE_PRIME_GENOME_START_TO FIVE_PRIME_GENOME_STOP_FROM FIVE_PRIME_GENOME_STOP_TO THREE_PRIME_CHROMOSOME THREE_PRIME_STRAND THREE_PRIME_TRANSCRIPT_ID THREE_PRIME_GENE_SYMBOL THREE_PRIME_FIRST_OBSERVE_EXON THREE_PRIME_GENOME_START_FROM THREE_PRIME_GENOME_START_TO THREE_PRIME_GENOME_STOP_FROM THREE_PRIME_GENOME_STOP_TO FUSION_TYPE PUBMED_PMID +COSS1910869 P3 COSO29974826 ENST00000300843.8 MARK4 ENST00000391945.9 ERCC2 23405175 diff --git a/test/data/cosmic_resistanceMutations.tsv b/test/data/cosmic_resistanceMutations.tsv new file mode 100644 index 00000000..e58e3027 --- /dev/null +++ b/test/data/cosmic_resistanceMutations.tsv @@ -0,0 +1,4 @@ +SAMPLE_NAME COSMIC_SAMPLE_ID GENE_SYMBOL COSMIC_GENE_ID TRANSCRIPT_ACCESSION CENSUS_GENE DRUG_NAME DRUG_RESPONSE GENOMIC_MUTATION_ID LEGACY_MUTATION_ID MUTATION_ID MUTATION_AA MUTATION_CDS GENOMIC_WT_ALLELE GENOMIC_MUT_ALLELE COSMIC_PHENOTYPE_ID PUBMED_PMID COSMIC_STUDY_ID MUTATION_ZYGOSITY CHROMOSOME GENOME_START GENOME_STOP STRAND HGVSP HGVSC HGVSG MUTATION_SOMATIC_STATUS +1000815 COSS1000815 EGFR COSG73399 ENST00000275493.6 Yes Gefitinib Gefitinib clinical resistant recurrence COSV51765492 COSM6240 178601678 p.T790M c.2369C>T C T COSO29974826 16983123 7 55181378 55181378 + ENSP00000275493.2:p.Thr790Met ENST00000275493.6:c.2369C>T 7:g.55181378C>T Reported in another cancer sample as somatic +1000815 COSS1000815 EGFR COSG73399 ENST00000454757.6 Yes Gefitinib Gefitinib clinical resistant recurrence COSV51765492 COSM6240 138108736 p.T745M c.2234C>T C T COSO29974826 16983123 7 55181378 55181378 + ENSP00000395243.3:p.Thr745Met ENST00000454757.6:c.2234C>T 7:g.55181378C>T Reported in another cancer sample as somatic +1000815 COSS1000815 EGFR COSG73399 ENST00000455089.5 Yes Gefitinib Gefitinib clinical resistant recurrence COSV51765492 COSM6240 138834457 p.T745M c.2234C>T C T COSO29974826 16983123 7 55181378 55181378 + ENSP00000415559.1:p.Thr745Met ENST00000455089.5:c.2234C>T 7:g.55181378C>T Reported in another cancer sample as somatic