Skip to content

Commit

Permalink
feat(website): add direct fasta download of a sequence (#807)
Browse files Browse the repository at this point in the history
  • Loading branch information
chaoran-chen committed Jan 26, 2024
1 parent 4cdda5d commit 4fa930e
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 2 deletions.
102 changes: 102 additions & 0 deletions website/src/pages/[organism]/seq/[accessionVersion].fa/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { APIRoute } from 'astro';
import { err, type Result } from 'neverthrow';

import { getReferenceGenomes } from '../../../../config.ts';
import { routes } from '../../../../routes.ts';
import { LapisClient } from '../../../../services/lapisClient.ts';
import type { ProblemDetail } from '../../../../types/backend.ts';
import { parseAccessionVersionFromString } from '../../../../utils/extractAccessionVersion.ts';
import { fastaEntryToString, parseFasta } from '../../../../utils/parseFasta.ts';

export const GET: APIRoute = async ({ params, redirect }) => {
const accessionVersion = params.accessionVersion!;
const organism = params.organism!;

const result = await getSequenceDetailsUnalignedFasta(accessionVersion, organism);
if (!result.isOk()) {
return new Response(undefined, {
status: 404,
});
}

if (result.value.type === ResultType.REDIRECT) {
return redirect(result.value.redirectUrl);
}

return new Response(result.value.fasta, {
headers: {
'Content-Type': 'text/x-fasta',
},
});
};

enum ResultType {
DATA = 'data',
REDIRECT = 'redirect',
}

type Data = {
type: ResultType.DATA;
fasta: string;
};

type Redirect = {
type: ResultType.REDIRECT;
redirectUrl: string;
};

const getSequenceDetailsUnalignedFasta = async (
accessionVersion: string,
organism: string,
): Promise<Result<Data | Redirect, ProblemDetail>> => {
const { accession, version } = parseAccessionVersionFromString(accessionVersion);

const lapisClient = LapisClient.createForOrganism(organism);

if (version === undefined) {
const latestVersionResult = await lapisClient.getLatestAccessionVersion(accession);
return latestVersionResult.map((latestVersion) => ({
type: ResultType.REDIRECT,
redirectUrl: routes.sequencesFastaPage(organism, latestVersion),
}));
}

const referenceGenomes = getReferenceGenomes(organism);
const segmentNames = referenceGenomes.nucleotideSequences.map((s) => s.name);
const isMultiSegmented = segmentNames.length > 1;

const fastaResult: Result<string, ProblemDetail> = !isMultiSegmented
? await lapisClient.getUnalignedSequences(accessionVersion)
: (await lapisClient.getUnalignedSequencesMultiSegment(accessionVersion, segmentNames)).map((segmentFastas) =>
segmentFastas
.map((fasta, i) => {
const parsed = parseFasta(fasta);
if (parsed.length === 0) {
return '';
}
const withSegmentSuffix = {
name: `${parsed[0].name}_${segmentNames[i]}`,
sequence: parsed[0].sequence,
};
return fastaEntryToString([withSegmentSuffix]);
})
.join('\n'),
);
if (fastaResult.isOk()) {
if (fastaResult.value.trim().length === 0) {
return err({
type: 'about:blank',
title: 'Not Found',
status: 0,
detail: 'No data found for accession version ' + accessionVersion,
instance: '/seq/' + accessionVersion + '.fa',
});
}
}
const withNewLineTermination = fastaResult.map((fasta) => `${fasta}\n`);

return withNewLineTermination.map((fasta) => ({
type: ResultType.DATA,
fasta,
}));
};
2 changes: 2 additions & 0 deletions website/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const routes = {
`/${organism}/seq/${getAccessionVersionString(accessionVersion)}`,
sequencesVersionsPage: (organism: string, accessionVersion: AccessionVersion | string) =>
`/${organism}/seq/${getAccessionVersionString(accessionVersion)}/versions`,
sequencesFastaPage: (organism: string, accessionVersion: AccessionVersion | string) =>
`${routes.sequencesDetailsPage(organism, accessionVersion)}.fa`,
submitPage: (organism: string) => withOrganism(organism, '/submit'),
revisePage: (organism: string) => withOrganism(organism, '/revise'),
editPage: (organism: string, accessionVersion: AccessionVersion) =>
Expand Down
23 changes: 22 additions & 1 deletion website/src/services/lapisClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Narrow } from '@zodios/core/lib/utils.types';
import { err, ok, type Result } from 'neverthrow';
import { err, ok, Result } from 'neverthrow';

import { lapisApi } from './lapisApi.ts';
import { ZodiosWrapperClient } from './zodiosWrapperClient.ts';
Expand Down Expand Up @@ -134,4 +134,25 @@ export class LapisClient extends ZodiosWrapperClient<typeof lapisApi> {
[this.schema.primaryKey]: accessionVersion,
});
}

public getUnalignedSequences(accessionVersion: string) {
return this.call('unalignedNucleotideSequences', {
[this.schema.primaryKey]: accessionVersion,
});
}

public async getUnalignedSequencesMultiSegment(accessionVersion: string, segmentNames: string[]) {
const results = await Promise.all(
segmentNames.map((segment) =>
this.call(
'unalignedNucleotideSequencesMultiSegment',
{
[this.schema.primaryKey]: accessionVersion,
},
{ params: { segment } },
),
),
);
return Result.combine(results);
}
}
1 change: 1 addition & 0 deletions website/tests/e2e.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const testSequenceEntry = {
name: '1.1',
accession: '1',
version: 1,
unaligned: 'A'.repeat(123),
orf1a: 'QRFEINSA',
};
export const revokedSequenceEntry = {
Expand Down
10 changes: 10 additions & 0 deletions website/tests/pages/sequences/accession.fa.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { routes } from '../../../src/routes.ts';
import { baseUrl, dummyOrganism, expect, test, testSequenceEntry } from '../../e2e.fixture';

test.describe('The sequence.fa page', () => {
test('can load and show fasta file', async () => {
const url = `${baseUrl}${routes.sequencesFastaPage(dummyOrganism.key, testSequenceEntry)}`;
const content = await (await fetch(url)).text();
expect(content).toBe(`>${testSequenceEntry.name}\n${testSequenceEntry.unaligned}\n`);
});
});
2 changes: 1 addition & 1 deletion website/tests/util/preprocessingPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const handleError = (error: unknown): Error => {

const sequenceData = {
unalignedNucleotideSequences: {
main: 'A'.repeat(123),
main: testSequenceEntry.unaligned,
},
alignedNucleotideSequences: {
main: 'N' + 'A'.repeat(29902),
Expand Down

0 comments on commit 4fa930e

Please sign in to comment.