From b2561a7fce6aff74d2104d388550a1bcc19caed2 Mon Sep 17 00:00:00 2001 From: Cornelius Roemer Date: Mon, 16 Dec 2024 19:36:42 +0100 Subject: [PATCH 1/4] wip Multi segmented Refactor Guard against excessive request size to prevent memory spikes Refactor Aadd UI for rich headers Make configurable f proper base name rich headers don't support compression --- kubernetes/loculus/values.yaml | 1 + .../DownloadDialog/DownloadDataType.ts | 24 +-- .../DownloadDialog/DownloadForm.tsx | 33 +++- .../DownloadDialog/DownloadUrlGenerator.ts | 39 ++++- .../SearchPage/DownloadDialog/OptionBlock.tsx | 19 ++- .../components/SearchPage/SearchFullUI.tsx | 2 +- website/src/pages/[organism]/api/sequences.ts | 159 ++++++++++++++++++ website/src/services/lapisApi.ts | 5 +- website/src/services/lapisClient.ts | 36 +++- website/src/types/config.ts | 1 + website/src/types/lapis.ts | 11 ++ 11 files changed, 285 insertions(+), 45 deletions(-) create mode 100644 website/src/pages/[organism]/api/sequences.ts diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index b78aa2eda5..036ce0bce6 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -37,6 +37,7 @@ defaultOrganismConfig: &defaultOrganismConfig enabled: true externalFields: - ncbiReleaseDate + richFastaHeaderFields: ["displayName"] ### Field list ## General fields # name: Key used across app to refer to this field (required) diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDataType.ts b/website/src/components/SearchPage/DownloadDialog/DownloadDataType.ts index 30b9ee3610..54cc71e288 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDataType.ts +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDataType.ts @@ -1,6 +1,10 @@ export type DownloadDataType = | { type: 'metadata' } - | { type: 'unalignedNucleotideSequences'; segment?: string } + | { + type: 'unalignedNucleotideSequences'; + segment?: string; + includeRichFastaHeaders?: boolean; + } | { type: 'alignedNucleotideSequences'; segment?: string } | { type: 'alignedAminoAcidSequences'; gene: string }; @@ -20,21 +24,3 @@ export const dataTypeForFilename = (dataType: DownloadDataType): string => { return `aligned-aa-${dataType.gene}`; } }; - -/** - * Get the LAPIS endpoint where to download this data type from. - */ -export const getEndpoint = (dataType: DownloadDataType) => { - const segmentPath = (segment?: string) => (segment !== undefined ? `/${segment}` : ''); - - switch (dataType.type) { - case 'metadata': - return '/sample/details'; - case 'unalignedNucleotideSequences': - return '/sample/unalignedNucleotideSequences' + segmentPath(dataType.segment); - case 'alignedNucleotideSequences': - return '/sample/alignedNucleotideSequences' + segmentPath(dataType.segment); - case 'alignedAminoAcidSequences': - return `/sample/alignedAminoAcidSequences/${dataType.gene}`; - } -}; diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx index 79d30baa6b..0c325e9ce5 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx @@ -19,6 +19,7 @@ export const DownloadForm: FC = ({ referenceGenomesSequenceNa const [unalignedNucleotideSequence, setUnalignedNucleotideSequence] = useState(0); const [alignedNucleotideSequence, setAlignedNucleotideSequence] = useState(0); const [alignedAminoAcidSequence, setAlignedAminoAcidSequence] = useState(0); + const [includeRichFastaHeaders, setIncludeRichFastaHeaders] = useState(0); const isMultiSegmented = referenceGenomesSequenceNames.nucleotideSequences.length > 1; @@ -34,6 +35,7 @@ export const DownloadForm: FC = ({ referenceGenomesSequenceNa segment: isMultiSegmented ? referenceGenomesSequenceNames.nucleotideSequences[unalignedNucleotideSequence] : undefined, + includeRichFastaHeaders: includeRichFastaHeaders === 1, }; break; case 2: @@ -68,6 +70,7 @@ export const DownloadForm: FC = ({ referenceGenomesSequenceNa unalignedNucleotideSequence, alignedNucleotideSequence, alignedAminoAcidSequence, + includeRichFastaHeaders, isMultiSegmented, referenceGenomesSequenceNames.nucleotideSequences, referenceGenomesSequenceNames.genes, @@ -114,19 +117,30 @@ export const DownloadForm: FC = ({ referenceGenomesSequenceNa { label: <>Metadata }, { label: <>Raw nucleotide sequences, - subOptions: isMultiSegmented ? ( + subOptions: (
- ({ - label: <>{segment}, - }))} - selected={unalignedNucleotideSequence} - onSelect={setUnalignedNucleotideSequence} + {isMultiSegmented ? ( + ({ + label: <>{segment}, + }))} + selected={unalignedNucleotideSequence} + onSelect={setUnalignedNucleotideSequence} + disabled={dataType !== 1} + /> + ) : undefined} + Accession only }, { label: <>Accession and metadata }]} + selected={includeRichFastaHeaders} + onSelect={setIncludeRichFastaHeaders} disabled={dataType !== 1} + variant='nested' />
- ) : undefined, + ), }, { label: <>Aligned nucleotide sequences, @@ -170,6 +184,7 @@ export const DownloadForm: FC = ({ referenceGenomesSequenceNa options={[{ label: <>None }, { label: <>Zstandard }, { label: <>Gzip }]} selected={compression} onSelect={setCompression} + disabled={dataType === 1 && includeRichFastaHeaders === 1} // Rich headers don't support compression /> ); diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadUrlGenerator.ts b/website/src/components/SearchPage/DownloadDialog/DownloadUrlGenerator.ts index 2f9b24bac7..7ae7479818 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadUrlGenerator.ts +++ b/website/src/components/SearchPage/DownloadDialog/DownloadUrlGenerator.ts @@ -1,6 +1,6 @@ import kebabCase from 'just-kebab-case'; -import { getEndpoint, dataTypeForFilename, type DownloadDataType } from './DownloadDataType.ts'; +import { dataTypeForFilename, type DownloadDataType } from './DownloadDataType.ts'; import type { SequenceFilter } from './SequenceFilters.tsx'; import { IS_REVOCATION_FIELD, metadataDefaultDownloadDataFormat, VERSION_STATUS_FIELD } from '../../../settings.ts'; import { versionStatuses } from '../../../types/lapis.ts'; @@ -21,19 +21,21 @@ export type DownloadOption = { export class DownloadUrlGenerator { private readonly organism: string; private readonly lapisUrl: string; + private readonly richFastaHeaderFields: string[]; /** * Create new DownloadUrlGenerator with the given properties. * @param organism The organism, will be part of the filename. * @param lapisUrl The lapis API URL for downloading. */ - constructor(organism: string, lapisUrl: string) { + constructor(organism: string, lapisUrl: string, richFastaHeaderFields: string[] = ['accessionVersion']) { this.organism = organism; this.lapisUrl = lapisUrl; + this.richFastaHeaderFields = richFastaHeaderFields; } public generateDownloadUrl(downloadParameters: SequenceFilter, option: DownloadOption) { - const baseUrl = `${this.lapisUrl}${getEndpoint(option.dataType)}`; + const baseUrl = this.downloadEndpoint(option.dataType); const params = new URLSearchParams(); params.set('downloadAsFile', 'true'); @@ -52,8 +54,20 @@ export class DownloadUrlGenerator { params.set('compression', option.compression); } + if ( + option.dataType.type === 'unalignedNucleotideSequences' && + option.dataType.includeRichFastaHeaders === true + ) { + // get from config + params.set('headerFields', this.richFastaHeaderFields.join(',')); + } + downloadParameters.toUrlSearchParams().forEach(([name, value]) => { - params.append(name, value); + // Empty values are not allowed for e.g. aminoAcidInsertion filters + // Hence, filter out empty values + if (value !== '') { + params.append(name, value); + } }); return { @@ -69,4 +83,21 @@ export class DownloadUrlGenerator { const timestamp = new Date().toISOString().slice(0, 16).replace(':', ''); return `${organism}_${dataType}_${timestamp}`; } + + private readonly downloadEndpoint = (dataType: DownloadDataType) => { + const segmentPath = (segment?: string) => (segment !== undefined ? '/' + segment : ''); + + switch (dataType.type) { + case 'metadata': + return this.lapisUrl + '/sample/details'; + case 'unalignedNucleotideSequences': + return dataType.includeRichFastaHeaders === true + ? '/' + this.organism + '/api/sequences' + segmentPath(dataType.segment) + : this.lapisUrl + '/sample/unalignedNucleotideSequences' + segmentPath(dataType.segment); + case 'alignedNucleotideSequences': + return this.lapisUrl + '/sample/alignedNucleotideSequences' + segmentPath(dataType.segment); + case 'alignedAminoAcidSequences': + return this.lapisUrl + '/sample/alignedAminoAcidSequences/' + dataType.gene; + } + }; } diff --git a/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx b/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx index fa228f5dbc..64a0460cae 100644 --- a/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx +++ b/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx @@ -10,6 +10,7 @@ export type OptionBlockProps = { selected: number; onSelect: (index: number) => void; disabled?: boolean; + variant?: 'default' | 'nested'; // New prop }; export const RadioOptionBlock: FC = ({ @@ -19,22 +20,30 @@ export const RadioOptionBlock: FC = ({ selected, onSelect, disabled = false, + variant = 'default', }) => { return ( -
- {title !== undefined &&

{title}

} + //
+
+ {title !== undefined && ( +

{title}

+ )} {options.map((option, index) => ( -
+
{option.subOptions}
diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index ebc8fd9086..61c834fabc 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -191,7 +191,7 @@ export const InnerSearchFullUI = ({ }; const lapisUrl = getLapisUrl(clientConfig, organism); - const downloadUrlGenerator = new DownloadUrlGenerator(organism, lapisUrl); + const downloadUrlGenerator = new DownloadUrlGenerator(organism, lapisUrl, schema.richFastaHeaderFields); const hooks = lapisClientHooks(lapisUrl).zodiosHooks; const aggregatedHook = hooks.useAggregated({}, {}); diff --git a/website/src/pages/[organism]/api/sequences.ts b/website/src/pages/[organism]/api/sequences.ts new file mode 100644 index 0000000000..5c25edc238 --- /dev/null +++ b/website/src/pages/[organism]/api/sequences.ts @@ -0,0 +1,159 @@ +import { type Result, err, ok } from 'neverthrow'; + +import { LapisClient } from '../../../services/lapisClient.ts'; + +const MAX_SEQUENCES = 5000; + +interface SequenceEntry { + [key: string]: string | null; + seq: string; + accessionVersion: string; +} + +interface RequestParams { + params: { + organism: string; + }; + request: Request; +} + +interface MetadataEntry { + [key: string]: string | undefined; + accessionVersion: string; +} + +interface QueryParameters { + [key: string]: any; + segment: string; + headerFields: string[]; + downloadFileBasename: string; +} + +const createErrorResponse = (status: number, error: string, details?: any) => + new Response(JSON.stringify({ error, ...details }), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + +const parseQueryParams = (url: URL): QueryParameters => { + const searchParams = new URLSearchParams(url.searchParams); + const params: QueryParameters = { + segment: searchParams.get('segment') ?? 'main', + headerFields: searchParams.get('headerFields')?.split(',') ?? ['accessionVersion'], + downloadFileBasename: searchParams.get('downloadFileBasename') ?? 'sequences', + }; + + searchParams.delete('segment'); + searchParams.delete('headerFields'); + return { + ...params, + queryFilters: Object.fromEntries(searchParams), + }; +}; + +const validateSequenceCount = async (client: LapisClient, queryParams: any): Promise> => { + const countResult = await client.getCounts(queryParams); + + if (countResult.isErr()) { + return err('Failed to check sequence count: ' + JSON.stringify(countResult.error)); + } + + const totalSequences = countResult.value.data[0].count; + if (totalSequences > MAX_SEQUENCES) { + return err( + `This query would return ${totalSequences} sequences. Please limit your query to return no more than ${MAX_SEQUENCES} sequences.`, + ); + } + + return ok(totalSequences); +}; + +const fetchSequenceData = async ( + client: LapisClient, + queryParams: any, + segmentName: string = 'main', +): Promise> => { + const [sequencesResult, metadataResult] = await Promise.all([ + segmentName !== 'main' + ? client.getUnalignedSequencesMultiSegmentJson(queryParams, segmentName) + : client.getUnalignedSequencesJson(queryParams), + client.getMetadataJson(queryParams), + ]); + + if (sequencesResult.isErr()) { + return err(`Failed to fetch sequences: ${sequencesResult.error}`); + } + if (metadataResult.isErr()) { + return err(`Failed to fetch metadata: ${metadataResult.error}`); + } + + return ok([sequencesResult.value as any[], metadataResult.value.data as MetadataEntry[]]); +}; + +const processSequenceData = ( + sequences: any[], + metadata: MetadataEntry[], + segmentName: string, + headerFields: string[], +): Record => { + return sequences.reduce>((acc, sequence) => { + const accessionVersion = sequence.accessionVersion; + const metadataEntry = metadata.find((meta) => meta.accessionVersion === accessionVersion); + + const entry: SequenceEntry = { + seq: sequence[segmentName], + accessionVersion, + ...headerFields.reduce( + (fields, field) => ({ + ...fields, + [field]: field !== 'seq' ? (metadataEntry?.[field] ?? null) : null, + }), + {}, + ), + }; + + acc[accessionVersion] = entry; + return acc; + }, {}); +}; + +const generateFasta = (entries: Record, headerFields: string[]): string => { + return Object.values(entries) + .map((entry) => { + const header = headerFields + .map((field) => entry[field]) + .filter(Boolean) + .join('/'); + return `>${header}\n${entry.seq}`; + }) + .join('\n'); +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export async function GET({ params, request }: RequestParams): Promise { + const client = LapisClient.createForOrganism(params.organism); + const { segment, headerFields, queryFilters, downloadFileBasename } = parseQueryParams(new URL(request.url)); + + const countValidation = await validateSequenceCount(client, queryFilters); + if (countValidation.isErr()) { + return createErrorResponse(400, countValidation.error); + } + + const dataResult = await fetchSequenceData(client, queryFilters, segment); + if (dataResult.isErr()) { + return createErrorResponse(500, dataResult.error); + } + + const [sequences, metadata] = dataResult.value; + + const processedData = processSequenceData(sequences, metadata, segment, headerFields); + const fastaContent = generateFasta(processedData, headerFields); + + return new Response(fastaContent, { + status: 200, + headers: { + 'Content-Type': 'application/x-fasta', + 'Content-Disposition': `attachment; filename="${downloadFileBasename}.fasta"`, + }, + }); +} diff --git a/website/src/services/lapisApi.ts b/website/src/services/lapisApi.ts index 627181a877..40332ee37e 100644 --- a/website/src/services/lapisApi.ts +++ b/website/src/services/lapisApi.ts @@ -8,6 +8,7 @@ import { lapisBaseRequest, mutationsRequest, mutationsResponse, + sequenceEntries, sequenceRequest, } from '../types/lapis.ts'; @@ -141,7 +142,7 @@ const unalignedNucleotideSequencesMultiSegmentEndpoint = makeEndpoint({ schema: sequenceRequest, }, ], - response: z.string(), + response: sequenceEntries, }); const unalignedNucleotideSequencesEndpoint = makeEndpoint({ @@ -156,7 +157,7 @@ const unalignedNucleotideSequencesEndpoint = makeEndpoint({ schema: sequenceRequest, }, ], - response: z.string(), + response: sequenceEntries, }); const alignedAminoAcidSequencesEndpoint = makeEndpoint({ diff --git a/website/src/services/lapisClient.ts b/website/src/services/lapisClient.ts index 22ce2876b9..9ac02a30af 100644 --- a/website/src/services/lapisClient.ts +++ b/website/src/services/lapisClient.ts @@ -148,15 +148,18 @@ export class LapisClient extends ZodiosWrapperClient { }); } - public getUnalignedSequences(accessionVersion: string) { + public getUnalignedSequence( + accessionVersion: string, + dataFormat: 'FASTA' | 'JSON' = 'FASTA', + ): Promise> { return this.call('unalignedNucleotideSequences', { [this.schema.primaryKey]: accessionVersion, - dataFormat: 'FASTA', + dataFormat, }); } public async getUnalignedSequencesMultiSegment(accessionVersion: string, segmentNames: string[]) { - const results = await Promise.all( + const results = (await Promise.all( segmentNames.map((segment) => this.call( 'unalignedNucleotideSequencesMultiSegment', @@ -167,12 +170,12 @@ export class LapisClient extends ZodiosWrapperClient { { params: { segment } }, ), ), - ); + )) as Result[]; return Result.combine(results); } public getSequenceFasta(accessionVersion: string): Promise> { - return this.getUnalignedSequences(accessionVersion); + return this.getUnalignedSequence(accessionVersion, 'FASTA') as Promise>; } public async getMultiSegmentSequenceFasta( @@ -196,4 +199,27 @@ export class LapisClient extends ZodiosWrapperClient { .join(''), ); } + + public getUnalignedSequencesJson(params: { [key: string]: string }) { + return this.call('unalignedNucleotideSequences', { dataFormat: 'JSON', ...params }); + } + + public getUnalignedSequencesMultiSegmentJson(params: { [key: string]: string }, segment: string) { + return this.call( + 'unalignedNucleotideSequencesMultiSegment', + { + dataFormat: 'JSON', + ...params, + }, + { params: { segment } }, + ) as Promise>; + } + + public getMetadataJson(params: { [key: string]: string }) { + return this.call('details', { dataFormat: 'JSON', ...params }); + } + + public getCounts(params: { [key: string]: string }) { + return this.call('aggregated', { dataFormat: 'JSON', ...params }); + } } diff --git a/website/src/types/config.ts b/website/src/types/config.ts index 477866f004..9648f2192c 100644 --- a/website/src/types/config.ts +++ b/website/src/types/config.ts @@ -102,6 +102,7 @@ const schema = z.object({ defaultOrderBy: z.string(), defaultOrder: orderByType, loadSequencesAutomatically: z.boolean().optional(), + richFastaHeaderFields: z.array(z.string()).optional(), }); export type Schema = z.infer; diff --git a/website/src/types/lapis.ts b/website/src/types/lapis.ts index 6a493d36cd..ed9b89b7f7 100644 --- a/website/src/types/lapis.ts +++ b/website/src/types/lapis.ts @@ -59,6 +59,17 @@ export type Details = z.infer; export const detailsResponse = makeLapisResponse(z.array(details)); export type DetailsResponse = z.infer; +export const jsonSequenceEntry = z.record(z.string(), z.string()); +export type JsonSequenceEntry = z.infer; + +export const jsonSequenceEntries = z.array(jsonSequenceEntry); +export type JsonSequenceEntries = z.infer; + +export const sequenceEntries = z.union([jsonSequenceEntries, z.string()]); +export type SequenceEntries = z.infer; + +export const sequenceResponse = sequenceEntries; + const aggregatedItem = z .object({ count: z.number() }) .catchall(z.union([z.string(), z.number(), z.boolean(), z.null()])); From 5277fef5284d4f1aec3d3d3ad5583b3b2c6fca11 Mon Sep 17 00:00:00 2001 From: Cornelius Roemer Date: Tue, 17 Dec 2024 18:08:21 +0100 Subject: [PATCH 2/4] eslint fixes --- .../src/components/SearchPage/DownloadDialog/OptionBlock.tsx | 4 ++-- website/src/pages/[organism]/api/sequences.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx b/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx index 64a0460cae..46a31d59d1 100644 --- a/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx +++ b/website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx @@ -24,9 +24,9 @@ export const RadioOptionBlock: FC = ({ }) => { return ( //
-
+
{title !== undefined && ( -

{title}

+

{title}

)} {options.map((option, index) => (
diff --git a/website/src/pages/[organism]/api/sequences.ts b/website/src/pages/[organism]/api/sequences.ts index 5c25edc238..746e15525c 100644 --- a/website/src/pages/[organism]/api/sequences.ts +++ b/website/src/pages/[organism]/api/sequences.ts @@ -129,7 +129,7 @@ const generateFasta = (entries: Record, headerFields: str .join('\n'); }; -// eslint-disable-next-line @typescript-eslint/naming-convention + export async function GET({ params, request }: RequestParams): Promise { const client = LapisClient.createForOrganism(params.organism); const { segment, headerFields, queryFilters, downloadFileBasename } = parseQueryParams(new URL(request.url)); From 92b13fd5ccd8a5c1fb1e4f8c5a849b1d31bbb3ed Mon Sep 17 00:00:00 2001 From: Cornelius Roemer Date: Tue, 17 Dec 2024 18:21:43 +0100 Subject: [PATCH 3/4] Lints --- website/src/pages/[organism]/api/sequences.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/website/src/pages/[organism]/api/sequences.ts b/website/src/pages/[organism]/api/sequences.ts index 746e15525c..461021ab98 100644 --- a/website/src/pages/[organism]/api/sequences.ts +++ b/website/src/pages/[organism]/api/sequences.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { type Result, err, ok } from 'neverthrow'; import { LapisClient } from '../../../services/lapisClient.ts'; @@ -23,15 +25,16 @@ interface MetadataEntry { } interface QueryParameters { - [key: string]: any; segment: string; headerFields: string[]; downloadFileBasename: string; + queryFilters: { [key: string]: string }; } -const createErrorResponse = (status: number, error: string, details?: any) => - new Response(JSON.stringify({ error, ...details }), { +const createErrorResponse = (status: number, error: string, details?: unknown) => + new Response(JSON.stringify({ error, details }), { status, + // eslint-disable-next-line @typescript-eslint/naming-convention headers: { 'Content-Type': 'application/json' }, }); @@ -41,6 +44,7 @@ const parseQueryParams = (url: URL): QueryParameters => { segment: searchParams.get('segment') ?? 'main', headerFields: searchParams.get('headerFields')?.split(',') ?? ['accessionVersion'], downloadFileBasename: searchParams.get('downloadFileBasename') ?? 'sequences', + queryFilters: {}, }; searchParams.delete('segment'); @@ -51,14 +55,17 @@ const parseQueryParams = (url: URL): QueryParameters => { }; }; -const validateSequenceCount = async (client: LapisClient, queryParams: any): Promise> => { +const validateSequenceCount = async ( + client: LapisClient, + queryParams: Record, +): Promise> => { const countResult = await client.getCounts(queryParams); if (countResult.isErr()) { - return err('Failed to check sequence count: ' + JSON.stringify(countResult.error)); + return err(`Failed to fetch sequence count: ${(countResult.error as unknown as Error).message}`); } - const totalSequences = countResult.value.data[0].count; + const totalSequences = (countResult.value as { data: { count: number }[] }).data[0].count; if (totalSequences > MAX_SEQUENCES) { return err( `This query would return ${totalSequences} sequences. Please limit your query to return no more than ${MAX_SEQUENCES} sequences.`, @@ -70,7 +77,7 @@ const validateSequenceCount = async (client: LapisClient, queryParams: any): Pro const fetchSequenceData = async ( client: LapisClient, - queryParams: any, + queryParams: Record, segmentName: string = 'main', ): Promise> => { const [sequencesResult, metadataResult] = await Promise.all([ @@ -81,10 +88,10 @@ const fetchSequenceData = async ( ]); if (sequencesResult.isErr()) { - return err(`Failed to fetch sequences: ${sequencesResult.error}`); + return err(`Failed to fetch sequences: ${(sequencesResult.error as unknown as Error).message}`); } if (metadataResult.isErr()) { - return err(`Failed to fetch metadata: ${metadataResult.error}`); + return err(`Failed to fetch metadata: ${(metadataResult.error as unknown as Error).message}`); } return ok([sequencesResult.value as any[], metadataResult.value.data as MetadataEntry[]]); @@ -129,7 +136,6 @@ const generateFasta = (entries: Record, headerFields: str .join('\n'); }; - export async function GET({ params, request }: RequestParams): Promise { const client = LapisClient.createForOrganism(params.organism); const { segment, headerFields, queryFilters, downloadFileBasename } = parseQueryParams(new URL(request.url)); @@ -152,7 +158,9 @@ export async function GET({ params, request }: RequestParams): Promise return new Response(fastaContent, { status: 200, headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'Content-Type': 'application/x-fasta', + // eslint-disable-next-line @typescript-eslint/naming-convention 'Content-Disposition': `attachment; filename="${downloadFileBasename}.fasta"`, }, }); From ae1bce3ec1674ca1dba0634e9a77f2bb72c2f67b Mon Sep 17 00:00:00 2001 From: Cornelius Roemer Date: Tue, 17 Dec 2024 18:37:07 +0100 Subject: [PATCH 4/4] Drill through value to website config --- kubernetes/loculus/templates/_common-metadata.tpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kubernetes/loculus/templates/_common-metadata.tpl b/kubernetes/loculus/templates/_common-metadata.tpl index f553f61ea3..003d0ddeb9 100644 --- a/kubernetes/loculus/templates/_common-metadata.tpl +++ b/kubernetes/loculus/templates/_common-metadata.tpl @@ -176,6 +176,9 @@ organisms: {{ if .description }} description: {{ quote .description }} {{ end }} + {{ if .richFastaHeaderFields}} + richFastaHeaderFields: {{ toJson .richFastaHeaderFields }} + {{ end }} primaryKey: accessionVersion inputFields: {{- include "loculus.inputFields" . | nindent 8 }} - name: versionComment