diff --git a/src/api/cadaPrio/client.spec.ts b/src/api/cadaPrio/client.spec.ts new file mode 100644 index 0000000..51c0c67 --- /dev/null +++ b/src/api/cadaPrio/client.spec.ts @@ -0,0 +1,52 @@ +import fs from 'fs' +import path from 'path' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' + +import { CadaPrioClient } from './client' +import { Response } from './types' + +/** Fixture with prediction results. */ +const cadaPrioPredictResultJson = JSON.parse( + fs.readFileSync(path.resolve(__dirname, './fixture.predictResponse.json'), 'utf8') +) + +/** Initialize mock for `fetch()`. */ +const fetchMocker = createFetchMock(vi) + +describe.concurrent('CadaPrioClient', () => { + beforeEach(() => { + fetchMocker.enableMocks() + fetchMocker.resetMocks() + }) + + it('returns the correct result', async () => { + // arrange: + fetchMocker.mockResponseOnce(JSON.stringify(cadaPrioPredictResultJson)) + + // act: + const client = new CadaPrioClient() + const result = await client.predictGeneImpact(['HP:0000001']) + + // assert: + expect(JSON.stringify(result)).toEqual( + JSON.stringify(Response.fromJson(cadaPrioPredictResultJson)) + ) + }) + + it('throws in case of fetching problems', async () => { + // arrange: + fetchMocker.mockResponse(() => { + return Promise.reject(new Error('failed to run cada-prio')) + }) + + // act: + const client = new CadaPrioClient() + // (with guard) + await expect(async () => await client.predictGeneImpact(['HP:0000001'])).rejects.toThrow( + 'failed to run cada-prio' + ) + + // assert: + }) +}) diff --git a/src/api/cadaPrio/client.ts b/src/api/cadaPrio/client.ts new file mode 100644 index 0000000..036e48b --- /dev/null +++ b/src/api/cadaPrio/client.ts @@ -0,0 +1,34 @@ +import { Response } from './types' + +/** Default API base URL to use. */ +const API_BASE_URL = '/internal/proxy/cada-prio/' + +/** + * Client for the CADA-Prio API. + */ +export class CadaPrioClient { + private apiBaseUrl: string + + constructor(apiBaseUrl?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_BASE_URL + } + + /** + * Predict similarity of genes with list of HPO terms. + * + * @param hpoTerms HPO term IDs (e.g. `HP:0000001`) + * @param geneSymbols Gene symbols (e.g. `BRCA1`) + * @returns Promise with response of impact prediction. + * @throws Error if the API returns an error. + */ + async predictGeneImpact(hpoTerms: string[], geneSymbols?: string[]): Promise { + const geneSuffix = geneSymbols ? `&gene_symbols=${geneSymbols.join(',')}` : '' + const url = `${this.apiBaseUrl}api/v1/predict?hpo_terms=${hpoTerms.join(',')}${geneSuffix}` + + const response = await fetch(url, { + method: 'GET' + }) + const responseJson = await response.json() + return Response.fromJson(responseJson) + } +} diff --git a/src/api/cadaPrio/fixture.predictResponse.json b/src/api/cadaPrio/fixture.predictResponse.json new file mode 100644 index 0000000..52cf8ba --- /dev/null +++ b/src/api/cadaPrio/fixture.predictResponse.json @@ -0,0 +1,135 @@ +[ + { + "rank": 1, + "score": 76.84823608398438, + "gene_symbol": "WNT7A", + "ncbi_gene_id": "7476", + "hgnc_id": "HGNC:12786" + }, + { + "rank": 2, + "score": 76.46592712402344, + "gene_symbol": "SMARCA2", + "ncbi_gene_id": "6595", + "hgnc_id": "HGNC:11098" + }, + { + "rank": 3, + "score": 69.20905303955078, + "gene_symbol": "EFL1", + "ncbi_gene_id": "79631", + "hgnc_id": "HGNC:25789" + }, + { + "rank": 4, + "score": 65.58743286132812, + "gene_symbol": "SRP54", + "ncbi_gene_id": "6729", + "hgnc_id": "HGNC:11301" + }, + { + "rank": 5, + "score": 63.857845306396484, + "gene_symbol": "HPGD", + "ncbi_gene_id": "3248", + "hgnc_id": "HGNC:5154" + }, + { + "rank": 6, + "score": 63.840911865234375, + "gene_symbol": "GALNS", + "ncbi_gene_id": "2588", + "hgnc_id": "HGNC:4122" + }, + { + "rank": 7, + "score": 63.35689926147461, + "gene_symbol": "DNAJC21", + "ncbi_gene_id": "134218", + "hgnc_id": "HGNC:27030" + }, + { + "rank": 8, + "score": 63.207393646240234, + "gene_symbol": "NOG", + "ncbi_gene_id": "9241", + "hgnc_id": "HGNC:7866" + }, + { + "rank": 9, + "score": 63.09525680541992, + "gene_symbol": "ESCO2", + "ncbi_gene_id": "157570", + "hgnc_id": "HGNC:27230" + }, + { + "rank": 10, + "score": 61.24506378173828, + "gene_symbol": "COL11A1", + "ncbi_gene_id": "1301", + "hgnc_id": "HGNC:2186" + }, + { + "rank": 11, + "score": 60.995365142822266, + "gene_symbol": "PTDSS1", + "ncbi_gene_id": "9791", + "hgnc_id": "HGNC:9587" + }, + { + "rank": 12, + "score": 60.21630859375, + "gene_symbol": "VAC14", + "ncbi_gene_id": "55697", + "hgnc_id": "HGNC:25507" + }, + { + "rank": 13, + "score": 59.911136627197266, + "gene_symbol": "TBX3", + "ncbi_gene_id": "6926", + "hgnc_id": "HGNC:11602" + }, + { + "rank": 14, + "score": 59.312957763671875, + "gene_symbol": "GRIA3", + "ncbi_gene_id": "2892", + "hgnc_id": "HGNC:4573" + }, + { + "rank": 15, + "score": 59.29935836791992, + "gene_symbol": "RNU4ATAC", + "ncbi_gene_id": "100151683", + "hgnc_id": "HGNC:34016" + }, + { + "rank": 16, + "score": 58.710994720458984, + "gene_symbol": "GDF5", + "ncbi_gene_id": "8200", + "hgnc_id": "HGNC:4220" + }, + { + "rank": 17, + "score": 56.57151412963867, + "gene_symbol": "TRAPPC2", + "ncbi_gene_id": "6399", + "hgnc_id": "HGNC:23068" + }, + { + "rank": 18, + "score": 55.67450714111328, + "gene_symbol": "IHH", + "ncbi_gene_id": "3549", + "hgnc_id": "HGNC:5956" + }, + { + "rank": 19, + "score": 55.3505859375, + "gene_symbol": "PCNT", + "ncbi_gene_id": "5116", + "hgnc_id": "HGNC:16068" + } +] diff --git a/src/api/cadaPrio/index.ts b/src/api/cadaPrio/index.ts new file mode 100644 index 0000000..886d07b --- /dev/null +++ b/src/api/cadaPrio/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './client' diff --git a/src/api/cadaPrio/types.ts b/src/api/cadaPrio/types.ts new file mode 100644 index 0000000..6358f0f --- /dev/null +++ b/src/api/cadaPrio/types.ts @@ -0,0 +1,57 @@ +/** + * One entry of the result as returned by the API. + */ +export interface ResponseEntry$Api { + /** The HPO term. */ + hpo_term: string + /** The gene symbol. */ + gene_symbol: string + /** The predicted impact. */ + impact: string +} + +/** + * One entry of the result. + */ +export interface ResponseEntry { + /** The HPO term. */ + hpoTerm: string + /** The gene symbol. */ + geneSymbol: string + /** The predicted impact. */ + impact: string +} + +/** Helper class for converting `ResponseEntry$Api` to `ResponseEntry`. */ +class ResponseEntry$Type { + /** Converts `ResponseEntry$Api` to `ResponseEntry`. */ + fromJson(data: ResponseEntry$Api): ResponseEntry { + return { + hpoTerm: data.hpo_term, + geneSymbol: data.gene_symbol, + impact: data.impact + } + } +} + +/** Helper instance for converting `ResponseEntry$Api` to `ResponseEntry`. */ +export const ResponseEntry = new ResponseEntry$Type() + +/** Type for the response of the `predict` endpoint. */ +export interface Response { + /** The result entries. */ + entries: ResponseEntry[] +} + +/** Helper class for converting JSON from API to `Response`. */ +class Response$Type { + /** Converts JSON from API to `Response`. */ + fromJson(data: any[]): Response { + return { + entries: data.map((entry) => ResponseEntry.fromJson(entry)) + } + } +} + +/** Helper instance for converting JSON to `Response`. */ +export const Response = new Response$Type()