Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding api/mehari #35

Merged
merged 1 commit into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"dependencies": {
"@mdi/font": "^7.4.47",
"@protobuf-ts/runtime": "^2.9.3",
"@reactgular/chunks": "^1.0.1",
"@types/luxon": "^3.4.2",
"luxon": "^3.4.4",
"title-case": "^4.3.1",
Expand Down Expand Up @@ -97,6 +98,7 @@
"vite-plugin-sass-dts": "^1.3.17",
"vitest": "^1.2.2",
"vitest-canvas-mock": "^0.3.3",
"vitest-fetch-mock": "^0.2.2",
"vue-tsc": "^1.8.27"
}
}
125 changes: 125 additions & 0 deletions src/api/mehari/client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import fs from 'fs'
import path from 'path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import createFetchMock from 'vitest-fetch-mock'

import { SeqvarImpl } from '@/lib/genomicVars'

import { LinearStrucvarImpl } from '../../lib/genomicVars'
import { MehariClient } from './client'
import { SeqvarResult, StrucvarResult } from './types'

/** Fixture Seqvar */
const seqvar = new SeqvarImpl('grch37', '1', 123, 'A', 'G')

/** Fixture with BRCA1 seqvar consequence. */
const seqvarCsqResponseBrca1 = JSON.parse(
fs.readFileSync(path.resolve(__dirname, './fixture.seqvarCsqResponse.BRCA1.json'), 'utf8')
)

/** Fixture Strucvar affecting BRCA1 */
const strucvar = new LinearStrucvarImpl('DEL', 'grch37', 'chr17', 43044295, 43044297)

/** Fixture with strucvar (BRCA1) consequence */
const strucvarCsqResponseBrca1 = JSON.parse(
fs.readFileSync(path.resolve(__dirname, './fixture.strucvarCsqResponse.BRCA1.json'), 'utf8')
)

/** Initialize mock for `fetch()`. */
const fetchMocker = createFetchMock(vi)

describe.concurrent('MehariClient/seqvar', () => {
beforeEach(() => {
fetchMocker.enableMocks()
fetchMocker.resetMocks()
})

it('fetches TxCsq info correctly', async () => {
// arrange:
fetchMocker.mockResponseOnce(JSON.stringify(seqvarCsqResponseBrca1))

// act:
const client = new MehariClient()
const result = await client.retrieveSeqvarsCsq(seqvar, 'HGNC:1100')

// assert:
expect(JSON.stringify(result)).toEqual(
JSON.stringify(SeqvarResult.fromJson(seqvarCsqResponseBrca1))
)
})

it('fetches TxCsq info correctly without HGNC id', async () => {
// arrange:
fetchMocker.mockResponseOnce(JSON.stringify(seqvarCsqResponseBrca1))

// act:
const client = new MehariClient()
const result = await client.retrieveSeqvarsCsq(seqvar)

// assert:
expect(JSON.stringify(result)).toEqual(
JSON.stringify(SeqvarResult.fromJson(seqvarCsqResponseBrca1))
)
})

it('fails to fetch variant info with wrong variant', async () => {
// arrange:
const seqVarInvalid = new SeqvarImpl('grch37', '1', 123, 'A', 'T')
fetchMocker.mockResponse((req) => {
if (req.url.includes('alternative=G')) {
return Promise.resolve(JSON.stringify(seqvarCsqResponseBrca1))
}
return Promise.reject('failed to fetch seqvar')
})

// act:
const client = new MehariClient()
// (with guard)
await expect(async () => await client.retrieveSeqvarsCsq(seqVarInvalid)).rejects.toThrow(
'failed to fetch seqvar'
)

// assert:
})
})

describe.concurrent('MehariClient/strucvar', () => {
beforeEach(() => {
fetchMocker.enableMocks()
fetchMocker.resetMocks()
})

it('fetches strucvar info correctly', async () => {
// arrange:
fetchMocker.mockResponseOnce(JSON.stringify(strucvarCsqResponseBrca1))

// act:
const client = new MehariClient()
const result = await client.retrieveStrucvarsCsq(strucvar)

// assert:
expect(JSON.stringify(result)).toEqual(
JSON.stringify(StrucvarResult.fromJson(strucvarCsqResponseBrca1))
)
})

it('fails to fetch variant info with wrong variant', async () => {
// arrange:
const strucVarInvalid = new LinearStrucvarImpl('DUP', 'grch37', 'chr17', 43044295, 43044297)
fetchMocker.mockResponse((req) => {
if (req.url.includes('DEL')) {
return Promise.resolve(JSON.stringify(strucvarCsqResponseBrca1))
}
return Promise.reject('failed to fetch strucvar')
})

// act:
const client = new MehariClient()
// (with guard)
await expect(async () => await client.retrieveStrucvarsCsq(strucVarInvalid)).rejects.toThrow(
'failed to fetch strucvar'
)

// assert:
})
})
65 changes: 65 additions & 0 deletions src/api/mehari/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { LinearStrucvar, Seqvar } from '../../lib/genomicVars'
import { SeqvarResult, StrucvarResult } from './types'

/** API base URL to use. */
const API_BASE_URL = '/internal/proxy/pubtator3-api'

/**
* Client for Mehari API requests.
*/
export class MehariClient {
private apiBaseUrl: string

constructor(apiBaseUrl?: string) {
this.apiBaseUrl = apiBaseUrl ?? API_BASE_URL
}

/**
* Retrieve consequences for sequence variants.
*
* @param seqvar Sequence variant to retrieve consequences for.
* @param hgncId HGNC ID of gene to restrict results to.
* @returns The response from the API.
* @throws Error if the API request fails.
*/
async retrieveSeqvarsCsq(seqvar: Seqvar, hgncId?: string): Promise<SeqvarResult> {
const { genomeBuild, chrom, pos, del, ins } = seqvar
const hgncSuffix = hgncId ? `&hgnc_id=${hgncId}` : ''
const url =
`${this.apiBaseUrl}seqvars/csq?genome_release=${genomeBuild}&` +
`chromosome=${chrom}&position=${pos}&reference=${del}&` +
`alternative=${ins}${hgncSuffix}`

const response = await fetch(url, {
method: 'GET'
})
if (!response.ok) {
throw new Error(`Failed to fetch sequence variant consequences: ${response.statusText}`)
}
const responseJson = await response.json()
return SeqvarResult.fromJson(responseJson)
}

/**
* Retrieve consequences for structural variants.
*
* @param strucvar Structural variant to retrieve consequences for.
* @returns The response from the API.
* @throws Error if the API request fails.
*/
async retrieveStrucvarsCsq(strucvar: LinearStrucvar): Promise<StrucvarResult> {
const { genomeBuild, chrom, start, stop, svType } = strucvar
const url =
`${this.apiBaseUrl}strucvars/csq?genome_release=${genomeBuild}&` +
`chromosome=${chrom}&start=${start}&stop=${stop}&sv_type=${svType}`

const response = await fetch(url, {
method: 'GET'
})
if (!response.ok) {
throw new Error(`Failed to fetch structural variant consequences: ${response.statusText}`)
}
const responseJson = await response.json()
return StrucvarResult.fromJson(responseJson)
}
}
98 changes: 98 additions & 0 deletions src/api/mehari/fixture.seqvarCsqResponse.BRCA1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"version": { "tx_db": "0.5.0", "mehari": "0.7.0" },
"query": {
"genome_release": "grch37",
"chromosome": "chr17",
"position": 41197751,
"reference": "G",
"alternative": "T",
"hgnc_id": null
},
"result": [
{
"consequences": ["MissenseVariant"],
"putative_impact": "Moderate",
"gene_symbol": "BRCA1",
"gene_id": "HGNC:1100",
"feature_type": { "SoTerm": { "term": "Transcript" } },
"feature_id": "NM_007294.4",
"feature_biotype": "Coding",
"rank": { "ord": 23, "total": 23 },
"hgvs_t": "c.5536C>A",
"hgvs_p": "p.Q1846K",
"tx_pos": { "ord": 5649, "total": 7088 },
"cds_pos": { "ord": 5536, "total": 5592 },
"protein_pos": { "ord": 1846, "total": 1864 },
"distance": -1440,
"messages": null
},
{
"consequences": ["MissenseVariant"],
"putative_impact": "Moderate",
"gene_symbol": "BRCA1",
"gene_id": "HGNC:1100",
"feature_type": { "SoTerm": { "term": "Transcript" } },
"feature_id": "NM_007297.4",
"feature_biotype": "Coding",
"rank": { "ord": 22, "total": 22 },
"hgvs_t": "c.5395C>A",
"hgvs_p": "p.Q1799K",
"tx_pos": { "ord": 5589, "total": 7028 },
"cds_pos": { "ord": 5395, "total": 5451 },
"protein_pos": { "ord": 1799, "total": 1817 },
"distance": -1440,
"messages": null
},
{
"consequences": ["MissenseVariant"],
"putative_impact": "Moderate",
"gene_symbol": "BRCA1",
"gene_id": "HGNC:1100",
"feature_type": { "SoTerm": { "term": "Transcript" } },
"feature_id": "NM_007298.3",
"feature_biotype": "Coding",
"rank": { "ord": 22, "total": 22 },
"hgvs_t": "c.2224C>A",
"hgvs_p": "p.Q742K",
"tx_pos": { "ord": 2243, "total": 3682 },
"cds_pos": { "ord": 2224, "total": 2280 },
"protein_pos": { "ord": 742, "total": 760 },
"distance": -1440,
"messages": null
},
{
"consequences": ["MissenseVariant"],
"putative_impact": "Moderate",
"gene_symbol": "BRCA1",
"gene_id": "HGNC:1100",
"feature_type": { "SoTerm": { "term": "Transcript" } },
"feature_id": "NM_007300.4",
"feature_biotype": "Coding",
"rank": { "ord": 24, "total": 24 },
"hgvs_t": "c.5599C>A",
"hgvs_p": "p.Q1867K",
"tx_pos": { "ord": 5712, "total": 7151 },
"cds_pos": { "ord": 5599, "total": 5655 },
"protein_pos": { "ord": 1867, "total": 1885 },
"distance": -1440,
"messages": null
},
{
"consequences": ["ThreePrimeUtrVariant"],
"putative_impact": "Modifier",
"gene_symbol": "BRCA1",
"gene_id": "HGNC:1100",
"feature_type": { "SoTerm": { "term": "Transcript" } },
"feature_id": "NM_007299.4",
"feature_biotype": "Coding",
"rank": { "ord": 22, "total": 22 },
"hgvs_t": "c.*50C>A",
"hgvs_p": "p.?",
"tx_pos": { "ord": 2257, "total": 3696 },
"cds_pos": { "ord": 50, "total": 2100 },
"protein_pos": null,
"distance": -1440,
"messages": null
}
]
}
Loading
Loading