Skip to content

Commit

Permalink
feat: use lapisV2 as backend
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKellerer committed Dec 19, 2023
1 parent 00c6be7 commit 0e7b1de
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 51 deletions.
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*

# NPM
npm-debug.log*
Expand Down
2 changes: 1 addition & 1 deletion src/components/VariantHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { formatVariantDisplayName, getPangoLineage, VariantSelector } from '../data/VariantSelector';
import { fetchPangoLineageRecombinant } from '../data/api';
import { usePangoLineageFullName } from '../services/pangoLineageAlias';
import { usePangoLineageFullName } from '../services/pangoLineageWithAlias';
import { useQuery } from '../helpers/query-hook';

export interface Props {
Expand Down
15 changes: 7 additions & 8 deletions src/components/VariantLineages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { VariantSelector } from '../data/VariantSelector';
import { LapisSelector } from '../data/LapisSelector';
import { useQuery } from '../helpers/query-hook';
import { _fetchAggSamples } from '../data/api-lapis';
import { fetchPangoLineageAliases } from '../data/api';
import { pangoLineageWithAlias } from '../services/pangoLineageWithAlias';

export interface Props {
selector: LapisSelector;
Expand All @@ -19,22 +21,19 @@ const LineageEntry = styled.li`

export const VariantLineages = ({ selector, onVariantSelect, type }: Props) => {
const { data } = useQuery(signal => _fetchAggSamples(selector, [type], signal), [selector, type]);
const { data: pangoLineageAlias } = useQuery(fetchPangoLineageAliases, []);

const distribution:
| {
lineage: string | null;
proportion: number;
}[]
| undefined = useMemo(() => {
const distribution = useMemo(() => {
if (!data) {
return undefined;
}

const total = data.reduce((prev, curr) => prev + curr.count, 0);
return data.map(e => ({
lineage: e[type],
lineage: pangoLineageWithAlias(e[type], pangoLineageAlias ?? []),
proportion: e.count / total,
}));
}, [data, type]);
}, [data, pangoLineageAlias, type]);

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions src/components/__tests__/VariantHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { render, screen } from '@testing-library/react';
import { VariantHeader } from '../VariantHeader';
import { useQuery } from '../../helpers/query-hook';
import { usePangoLineageFullName } from '../../services/pangoLineageAlias';
import { usePangoLineageFullName } from '../../services/pangoLineageWithAlias';

jest.mock('../../services/pangoLineageAlias');
jest.mock('../../services/pangoLineageWithAlias');
const usePangoLineageFullNameMock = usePangoLineageFullName as jest.Mock;

jest.mock('../../helpers/query-hook');
Expand Down
2 changes: 1 addition & 1 deletion src/data/LapisResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export type LapisInformation = {

export type LapisResponse<T> = {
info: LapisInformation;
errors: any[];
errors?: any[];
data: T;
};
3 changes: 2 additions & 1 deletion src/data/VariantSelector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as zod from 'zod';
import jsonRefData from './refData.json';
import { mapFilterToLapisV2 } from './api-lapis';

export const VariantSelectorEncodedSchema = zod.object({
pangoLineage: zod.string().optional(),
Expand Down Expand Up @@ -53,7 +54,7 @@ export function addVariantSelectorToUrlSearchParams(
const arr = selector[k];
if (arr?.length) {
const key = index && index > 0 ? `${k}${index}` : k;
params.set(key, arr.join(','));
params.set(mapFilterToLapisV2(key), arr.join(','));
}
}
for (const k of variantStringFields) {
Expand Down
136 changes: 104 additions & 32 deletions src/data/api-lapis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,45 @@ import { HostCountSampleEntry } from './sample/HostCountSampleEntry';
import { InsertionCountEntry } from './InsertionCountEntry';
import { NextcladeDatasetInfo } from './NextcladeDatasetInfo';

const HOST = process.env.REACT_APP_LAPIS_HOST;
// const HOST = process.env.REACT_APP_LAPIS_HOST;
// TODO: remove this, before merging to master
const HOST = 'http://s1.int.genspectrum.org/gisaid';
const ACCESS_KEY = process.env.REACT_APP_LAPIS_ACCESS_KEY;

let currentLapisDataVersion: number | undefined = undefined;

export const get = async (endpoint: string, signal?: AbortSignal, omitDataVersion = false) => {
let url = HOST + endpoint;
if (currentLapisDataVersion !== undefined && !omitDataVersion) {
url += '&dataVersion=' + currentLapisDataVersion;
}
const res = await fetch(url, {
method: 'GET',
signal,
});
let url = `${HOST}/sample${endpoint}`;
// if (currentLapisDataVersion !== undefined && !omitDataVersion) {
// url += '&dataVersion=' + currentLapisDataVersion;
// }

const requestInit =
signal === undefined
? {
method: 'GET',
}
: {
method: 'GET',
signal: signal,
};
const res = await fetch(url, requestInit);
if (res.status === 410) {
window.location.reload();
}
return res;
};

export async function fetchLapisDataVersion(signal?: AbortSignal): Promise<string> {
let url = '/sample/info';
let url = '/info';
if (ACCESS_KEY) {
url += '?accessKey=' + ACCESS_KEY;
}
const res = await get(url, signal);
if (!res.ok) {
const response = await get(url, signal);
if (!response.ok) {
throw new Error('Error fetching info');
}
const info = (await res.json()) as LapisInformation;
const info = (await response.json()) as LapisInformation;
currentLapisDataVersion = info.dataVersion;
return dayjs.unix(currentLapisDataVersion).locale('en').calendar();
}
Expand All @@ -69,16 +78,23 @@ export function getCurrentLapisDataVersionDate(): Date | undefined {
}

export async function fetchNextcladeDatasetInfo(signal?: AbortSignal): Promise<NextcladeDatasetInfo> {
let url = '/info/nextclade-dataset';
const res = await get(url, signal, true);
if (!res.ok) {
let url = '/aggregated?fields=nextcladeDatasetVersion';
if (ACCESS_KEY) {
url += '&accessKey=' + ACCESS_KEY;
}
const response = await get(url, signal, true);
if (!response.ok) {
throw new Error('Error fetching Nextclade dataset info');
}
return (await res.json()) as NextcladeDatasetInfo;
const nexcladeDatasetInfo = (await response.json()) as LapisResponse<{ nextcladeDatasetVersion: string }[]>;
return {
name: 'nextclade-dataset',
tag: nexcladeDatasetInfo.data[0].nextcladeDatasetVersion,
};
}

export async function fetchAllHosts(): Promise<string[]> {
let url = '/sample/aggregated?fields=host';
let url = '/aggregated?fields=host';
if (ACCESS_KEY) {
url += '&accessKey=' + ACCESS_KEY;
}
Expand Down Expand Up @@ -167,7 +183,7 @@ export async function fetchMutationProportions(
minProportion = 0.001
): Promise<MutationProportionEntry[]> {
const url = await getLinkTo(
`${sequenceType}-mutations`,
getMutationEndpoint(sequenceType),
selector,
undefined,
undefined,
Expand All @@ -183,12 +199,30 @@ export async function fetchMutationProportions(
return _extractLapisData(body);
}

function getMutationEndpoint(sequenceType: SequenceType): string {
switch (sequenceType) {
case 'nuc':
return 'nucleotideMutations';
case 'aa':
return 'aminoAcidMutations';
default:
throw new Error(`Unknown mutation type: ${sequenceType}`);
}
}

export async function fetchInsertionCounts(
selector: LapisSelector,
sequenceType: SequenceType,
signal?: AbortSignal
): Promise<InsertionCountEntry[]> {
const url = await getLinkTo(`${sequenceType}-insertions`, selector, undefined, undefined, undefined, true);
const url = await getLinkTo(
getInsertionEndpoint(sequenceType),
selector,
undefined,
undefined,
undefined,
true
);
const res = await get(url, signal);
if (!res.ok) {
throw new Error('Error fetching new samples data');
Expand All @@ -197,6 +231,17 @@ export async function fetchInsertionCounts(
return _extractLapisData(body);
}

function getInsertionEndpoint(sequenceType: SequenceType): string {
switch (sequenceType) {
case 'nuc':
return 'nucleotideInsertions';
case 'aa':
return 'aminoAcidInsertions';
default:
throw new Error(`Unknown mutation type: ${sequenceType}`);
}
}

export async function getLinkToStrainNames(
selector: LapisSelector,
orderAndLimit?: OrderAndLimitConfig
Expand Down Expand Up @@ -224,7 +269,12 @@ export async function getLinkToFasta(
selector: LapisSelector,
orderAndLimit?: OrderAndLimitConfig
): Promise<string> {
return getLinkTo(aligned ? 'fasta-aligned' : 'fasta', selector, orderAndLimit, true);
return getLinkTo(
aligned ? 'alignedNucleotideSequences' : 'unalignedNucleotideSequences',
selector,
orderAndLimit,
true
);
}

export async function getLinkTo(
Expand Down Expand Up @@ -270,9 +320,9 @@ export async function getLinkTo(
params.set('accessKey', ACCESS_KEY);
}
if (omitHost) {
return `/sample/${endpoint}?${params.toString()}`;
return `/${endpoint}?${params.toString()}`;
} else {
return `${HOST}/sample/${endpoint}?${params.toString()}`;
return `${HOST}/${endpoint}?${params.toString()}`;
}
}

Expand All @@ -284,18 +334,20 @@ export async function _fetchAggSamples(
): Promise<FullSampleAggEntry[]> {
const linkPrefix = await getLinkTo('aggregated', selector, undefined, undefined, undefined, true);
const _additionalParams = new URLSearchParams(additionalParams);
_additionalParams.set('fields', fields.join(','));
const res = await get(`${linkPrefix}&${_additionalParams}`, signal);
if (!res.ok) {
if (res.body !== null) {
const errors = (await res.json()).errors as { message: string }[];
if (errors.length > 0) {
throw new Error(errors.map(e => e.message).join(' '));
_additionalParams.set('fields', mapFiltersToLapisV2(fields).join(','));
const response = await get(`${linkPrefix}&${_additionalParams}`, signal);
if (!response.ok) {
if (response.body !== null) {
if ((await response.json()).errors !== undefined) {
const errors = (await response.json()).errors as { message: string }[];
if (errors.length > 0) {
throw new Error(errors.map(e => e.message).join(' '));
}
}
}
throw new Error();
}
const body = (await res.json()) as LapisResponse<FullSampleAggEntryRaw[]>;
const body = (await response.json()) as LapisResponse<FullSampleAggEntryRaw[]>;

const parsed = _extractLapisData(body).map(raw => parseFullSampleAggEntry(raw));
if (fields.includes('country')) {
Expand All @@ -309,6 +361,26 @@ export async function _fetchAggSamples(
return parsed;
}

function mapFiltersToLapisV2(filters: string[]) {
return filters.map(filter => {
return mapFilterToLapisV2(filter);
});
}

export function mapFilterToLapisV2(filter: string) {
switch (filter) {
case 'aaMutations':
return 'aminoAcidMutations';
case 'nucMutations':
return 'nucleotideMutations';
case 'aaInsertions':
return 'aminoAcidInsertions';
case 'nucInsertions':
return 'nucleotideInsertions';
}
return filter;
}

function _addOrderAndLimitToSearchParams(params: URLSearchParams, orderAndLimitConfig?: OrderAndLimitConfig) {
if (orderAndLimitConfig) {
const { orderBy, limit } = orderAndLimitConfig;
Expand All @@ -322,7 +394,7 @@ function _addOrderAndLimitToSearchParams(params: URLSearchParams, orderAndLimitC
}

function _extractLapisData<T>(response: LapisResponse<T>): T {
if (response.errors.length > 0) {
if (response.errors !== undefined) {
throw new Error('LAPIS returned an error: ' + JSON.stringify(response.errors));
}
if (currentLapisDataVersion === undefined) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/__tests__/pangoLineageAlias.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from '../../helpers/query-hook';
import { renderHook } from '@testing-library/react';
import { usePangoLineageFullName, usePangoLineageWithAlias } from '../pangoLineageAlias';
import { usePangoLineageFullName, usePangoLineageWithAlias } from '../pangoLineageWithAlias';

jest.mock('../../helpers/query-hook');
const useQueryMock = useQuery as jest.Mock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export function usePangoLineageWithAlias(pangoLineage: string) {
return pangoLineage;
}

const alias: PangoLineageAlias | undefined = data
return pangoLineageWithAlias(pangoLineage, data);
}

export function pangoLineageWithAlias(pangoLineage: string | null, pangoLineageAlias: PangoLineageAlias[]) {
if (pangoLineage === null) {
return '';
}
const alias = pangoLineageAlias
.filter(lineageAlias => pangoLineage.includes(lineageAlias.fullName + '.'))
.sort((a, b) => b.fullName.length - a.fullName.length)[0];
if (!alias) {
Expand Down

0 comments on commit 0e7b1de

Please sign in to comment.