diff --git a/backend/app/api/internal/endpoints/proxy.py b/backend/app/api/internal/endpoints/proxy.py index 3d9e8fd5..ab61c275 100644 --- a/backend/app/api/internal/endpoints/proxy.py +++ b/backend/app/api/internal/endpoints/proxy.py @@ -31,6 +31,8 @@ async def reverse_proxy(request: Request) -> Response: ) elif url.path.startswith(f"{settings.INTERNAL_STR}/proxy/nginx"): backend_url = settings.BACKEND_PREFIX_NGINX + url.path.replace("/internal/proxy/nginx", "") + elif url.path.startswith(f"{settings.INTERNAL_STR}/proxy/dotty"): + backend_url = settings.BACKEND_PREFIX_DOTTY + url.path.replace("/internal/proxy/dotty", "") if backend_url: client = httpx.AsyncClient() diff --git a/backend/app/core/config.py b/backend/app/core/config.py index c95cca6d..21fd1bb8 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -86,6 +86,8 @@ def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: # pragma BACKEND_PREFIX_VIGUNO: str = "http://viguno:8080" #: Prefix for the backend of nginx service. BACKEND_PREFIX_NGINX: str = "http://nginx:80" + #: Prefix for the backend of dotty service. + BACKEND_PREFIX_DOTTY: str = "http://dotty:8080" #: URL to REDIS service. REDIS_URL: str = "redis://redis:6379" diff --git a/backend/tests/api/internal/test_proxy.py b/backend/tests/api/internal/test_proxy.py index 399dd1cb..4ccb72a1 100644 --- a/backend/tests/api/internal/test_proxy.py +++ b/backend/tests/api/internal/test_proxy.py @@ -73,6 +73,21 @@ async def test_proxy_nginx(monkeypatch: MonkeyPatch, httpx_mock: HTTPXMock, clie assert response.text == "Mocked response" +@pytest.mark.asyncio +async def test_proxy_dotty(monkeypatch: MonkeyPatch, httpx_mock: HTTPXMock, client: TestClient): + """Test proxying to dotty backend.""" + monkeypatch.setattr(settings, "BACKEND_PREFIX_DOTTY", f"http://{MOCKED_BACKEND_HOST}") + httpx_mock.add_response( + url=f"http://{MOCKED_BACKEND_HOST}/{MOCKED_URL_TOKEN}", + method="GET", + text="Mocked response", + ) + + response = client.get(f"/internal/proxy/dotty/{MOCKED_URL_TOKEN}") + assert response.status_code == 200 + assert response.text == "Mocked response" + + @pytest.mark.asyncio async def test_invalid_proxy_route(client: TestClient): """Test invalid proxy route.""" diff --git a/frontend/src/api/common.ts b/frontend/src/api/common.ts index f1f65bf8..a1083955 100644 --- a/frontend/src/api/common.ts +++ b/frontend/src/api/common.ts @@ -1,20 +1,6 @@ export const API_INTERNAL_BASE_PREFIX = '/internal/' -// import.meta.env.MODE == 'development' ? '//localhost:8080/internal/' : '/internal/' - export const API_INTERNAL_BASE_PREFIX_ANNONARS = '/internal/proxy/annonars' -// import.meta.env.MODE == 'development' -// ? `//localhost:8080/internal/proxy/annonars` -// : '/internal/proxy/annonars' - export const API_INTERNAL_BASE_PREFIX_MEHARI = '/internal/proxy/mehari' -// import.meta.env.MODE == 'development' -// ? '//localhost:8080/internal/proxy/mehari' -// : '/internal/proxy/mehari' - export const API_INTERNAL_BASE_PREFIX_NGINX = '/internal/proxy/nginx' -// import.meta.env.MODE == 'development' -// ? '//localhost:8080/internal/proxy/proxy/nginx' -// : '/internal/proxy/nginx' - +export const API_INTERNAL_BASE_PREFIX_DOTTY = '/internal/proxy/dotty' export const API_V1_BASE_PREFIX = '/api/v1/' -// import.meta.env.MODE == 'development' ? '//localhost:8080/api/v1/' : '/api/v1/' diff --git a/frontend/src/api/dotty.ts b/frontend/src/api/dotty.ts new file mode 100644 index 00000000..3b7bf6ed --- /dev/null +++ b/frontend/src/api/dotty.ts @@ -0,0 +1,45 @@ +import { API_INTERNAL_BASE_PREFIX_DOTTY } from '@/api/common' + +const API_BASE_URL = `${API_INTERNAL_BASE_PREFIX_DOTTY}/` + +/** SPDI representation as returned by dotty. */ +export interface Spdi { + /** Assembly version. */ + assembly: 'GRCh37' | 'GRCh38' + /** Contig. */ + contig: string + /** Position. */ + pos: number + /** Deleted sequence. */ + reference_deleted: string + /** Inserted sequence. */ + alternate_inserted: string +} + +/** Response of a dotto query */ +export interface DottyResponse { + /** SPDI returned by dotty. */ + spdi: Spdi +} + +export class DottyClient { + private apiBaseUrl: string + private csrfToken: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_BASE_URL + this.csrfToken = csrfToken ?? null + } + + async toSpdi(q: String, assembly: 'GRCh37' | 'GRCh38' = 'GRCh38'): Promise { + const url = `${API_INTERNAL_BASE_PREFIX_DOTTY}/api/v1/to-spdi?q=${q}&assembly=${assembly}` + const response = await fetch(url, { + method: 'GET' + }) + if (response.status == 200) { + return await response.json() + } else { + return null + } + } +} diff --git a/frontend/src/components/HeaderDetailPage.vue b/frontend/src/components/HeaderDetailPage.vue index bc6de4be..1c24fa87 100644 --- a/frontend/src/components/HeaderDetailPage.vue +++ b/frontend/src/components/HeaderDetailPage.vue @@ -28,7 +28,7 @@ const genomeReleaseRef = ref(props.genomeRelease) * Otherwise log an error. */ const performSearch = async () => { - const routeLocation: any = search(searchTermRef.value, genomeReleaseRef.value) + const routeLocation: any = await search(searchTermRef.value, genomeReleaseRef.value) if (routeLocation) { router.push(routeLocation) } else { diff --git a/frontend/src/components/VariantDetails/VariationLandscape.vue b/frontend/src/components/VariantDetails/VariationLandscape.vue index a55ad8d9..3b4e079d 100644 --- a/frontend/src/components/VariantDetails/VariationLandscape.vue +++ b/frontend/src/components/VariantDetails/VariationLandscape.vue @@ -47,10 +47,10 @@ const convertClinvarSignificance = (input: number): number => { const vegaData = computed(() => { let clinvarInfo = [] - if (props.genomeRelease == 'grch37') { - clinvarInfo = props.clinvar.variants[0].variants - } else if (props.genomeRelease == 'grch38') { - clinvarInfo = props.clinvar.variants[1] + for (const item of props.clinvar.variants ?? []) { + if (item.genome_release.toLowerCase() == props.genomeRelease) { + clinvarInfo = item.variants + } } return clinvarInfo.map((variant: ClinvarVariant) => ({ @@ -291,13 +291,15 @@ const vegaLayer = [
Variation Landscape of {{ props.geneSymbol }}
- +
+ +
diff --git a/frontend/src/lib/__tests__/utils.spec.ts b/frontend/src/lib/__tests__/utils.spec.ts index 24eae998..2aae8ad3 100644 --- a/frontend/src/lib/__tests__/utils.spec.ts +++ b/frontend/src/lib/__tests__/utils.spec.ts @@ -110,8 +110,8 @@ describe.concurrent('isVariantMtHomopolymer method', () => { }) describe.concurrent('search method', () => { - it('should return "gene" route location for HGNC queries', () => { - const result = search('HGNC:1100', 'ghcr37') + it('should return "gene" route location for HGNC queries', async () => { + const result = await search('HGNC:1100', 'ghcr37') expect(result).toEqual({ name: 'gene', params: { @@ -121,8 +121,8 @@ describe.concurrent('search method', () => { }) }) - it('should return "variant" route location for Variant queries', () => { - const result = search('chr37:12345:A:G', 'ghcr37') + it('should return "variant" route location for Variant queries', async () => { + const result = await search('chr37:12345:A:G', 'ghcr37') expect(result).toEqual({ name: 'variant', params: { @@ -132,8 +132,8 @@ describe.concurrent('search method', () => { }) }) - it('should return "genes" route location for general queries', () => { - const result = search('TP53', 'ghcr37') + it('should return "genes" route location for general queries', async () => { + const result = await search('TP53', 'ghcr37') expect(result).toEqual({ name: 'genes', query: { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 2e0e8f92..55cce2f7 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,3 +1,5 @@ +import { DottyClient } from '@/api/dotty' + /** * Round `value` to `digits` and return an `` tag that has the original value * as the `@title` and the rounded value as the inner text. Optionally add a `label` @@ -87,7 +89,26 @@ export const isVariantMtHomopolymer = (smallVar: any): boolean => { * @param searchTerm The search term to use. * @param genomeRelease The genome release to use. */ -export const search = (searchTerm: string, genomeRelease: string) => { +export const search = async (searchTerm: string, genomeRelease: string) => { + // Remove leading/trailing whitespace. + searchTerm = searchTerm.trim() + if (!searchTerm) { + return // no query ;-) + } + + // First, attempt to resolve using dotty. + const dottyClient = new DottyClient() + const result = await dottyClient.toSpdi(searchTerm) + if (result) { + const spdi = result.spdi + searchTerm = `${spdi.contig}:${spdi.pos}:${spdi.reference_deleted}:${spdi.alternate_inserted}` + if (!searchTerm.startsWith('chr')) { + searchTerm = `chr${searchTerm}` + } + console.log(searchTerm) + genomeRelease = spdi.assembly.toLowerCase() + } + interface RouteLocationFragment { name: string params?: any @@ -110,7 +131,7 @@ export const search = (searchTerm: string, genomeRelease: string) => { }) ], [ - /^chr\d+:\d+:[A-Z]:[A-Z]$/, + /^chr\d+:\d+:[A-Z]+:[A-Z]+$/, (): RouteLocationFragment => ({ name: 'variant', params: { @@ -142,11 +163,6 @@ export const search = (searchTerm: string, genomeRelease: string) => { ] for (const [regexp, getRoute] of SEARCH_REGEXPS) { - // Remove trailing whitespace - searchTerm = searchTerm.trim() - if (!searchTerm) { - return null - } if (regexp.test(searchTerm)) { const routeLocation = getRoute() // console.log(`term ${searchTerm} matched ${regexp}, route is`, routeLocation) diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 2000efe2..de800f6e 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -21,8 +21,7 @@ const examples = [ 'BRCA1', 'EMP', 'TP53', - 'HGNC:1100', - 'HGNC:777', + 'NM_007294.4(BRCA1):c.5123C>A', 'chr17:41197708:T:G', 'chr17:41197751:G:T', 'DEL:chr17:41176312:41277500', @@ -40,7 +39,7 @@ const useExample = (example: string) => { * Otherwise log an error. */ const performSearch = async () => { - const routeLocation: any = search(searchTerm.value, genomeRelease.value) + const routeLocation: any = await search(searchTerm.value, genomeRelease.value) if (routeLocation) { router.push(routeLocation) } else {