diff --git a/lapis2-docs/astro.config.mjs b/lapis2-docs/astro.config.mjs index 38212ffb..86e1d669 100644 --- a/lapis2-docs/astro.config.mjs +++ b/lapis2-docs/astro.config.mjs @@ -46,6 +46,10 @@ export default defineConfig({ label: 'Filters', link: '/references/filters/', }, + { + label: 'Additional Request Properties', + link: '/references/additional-request-properties/', + }, { label: 'Open API / Swagger', link: '/references/open-api-definition/', diff --git a/lapis2-docs/src/components/QueryGenerator/OutputFormatSelection.tsx b/lapis2-docs/src/components/QueryGenerator/OutputFormatSelection.tsx index 46fc7671..b5df9de6 100644 --- a/lapis2-docs/src/components/QueryGenerator/OutputFormatSelection.tsx +++ b/lapis2-docs/src/components/QueryGenerator/OutputFormatSelection.tsx @@ -12,7 +12,7 @@ type Props = { export const OutputFormatSelection = ({ queryType, format, onFormatChange }: Props) => { return ( -
+
{queryType.selection === 'nucleotideSequences' || queryType.selection === 'aminoAcidSequences' ? ( <> For sequences, only FASTA is available as output format. diff --git a/lapis2-docs/src/components/QueryGenerator/QueryTypeSelection.tsx b/lapis2-docs/src/components/QueryGenerator/QueryTypeSelection.tsx index 0caa746a..0f8a6e99 100644 --- a/lapis2-docs/src/components/QueryGenerator/QueryTypeSelection.tsx +++ b/lapis2-docs/src/components/QueryGenerator/QueryTypeSelection.tsx @@ -113,17 +113,19 @@ const AggregatedStratified = ({ config, state, onStateChange }: Props) => {
By which field(s) would you like to stratify? - {config.schema.metadata.map((m) => ( - changeAggregatedStratifiedField(m.name)} - /> - ))} + {config.schema.metadata + .filter((metadata) => metadata.type !== 'insertion' && metadata.type !== 'aaInsertion') + .map((metadata) => ( + changeAggregatedStratifiedField(metadata.name)} + /> + ))}
diff --git a/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts b/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts index d11f16e9..c878a112 100644 --- a/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts +++ b/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts @@ -125,5 +125,12 @@ function mapMetadataTypeToResultFieldType(type: MetadataType): ResultFieldType { case 'date': case 'string': return 'string'; + case 'int': + return 'integer'; + case 'float': + return 'float'; + case 'insertion': + case 'aaInsertion': + throw Error('Insertion and aaInsertion are not supported as result field types'); } } diff --git a/lapis2-docs/src/components/QueryGenerator/Result.tsx b/lapis2-docs/src/components/QueryGenerator/Result.tsx index 3ac6d842..d07b654b 100644 --- a/lapis2-docs/src/components/QueryGenerator/Result.tsx +++ b/lapis2-docs/src/components/QueryGenerator/Result.tsx @@ -5,12 +5,26 @@ import { CodeBlock } from '../CodeBlock'; import { Tab, TabsBox } from '../TabsBox/react/TabsBox'; import { generateNonFastaQuery } from '../../utils/code-generators/python/generator'; import type { Config } from '../../config'; -import { useState } from 'react'; +import React, { useState } from 'react'; import type { ResultField } from '../../utils/code-generators/types'; -import { ContainerWrapper, LabelWrapper } from './styled-components'; +import { CheckBoxesWrapper, ContainerWrapper, LabeledCheckBox, LabelWrapper } from './styled-components'; import { getResultFields, MULTI_SEGMENTED, type QueryTypeSelectionState } from './QueryTypeSelectionState.ts'; import type { OrderByLimitOffset } from './OrderLimitOffsetSelection.tsx'; +const compressionOptions = [ + { value: undefined, label: 'No compression' }, + { value: 'gzip', label: 'gzip' }, + { value: 'zstd', label: 'zstd' }, +]; + +type CompressionValues = (typeof compressionOptions)[number]['value']; + +type AdditionalProperties = { + downloadAsFile: boolean; + tabularOutputFormat: TabularOutputFormat; + compression: CompressionValues; +}; + type Props = { queryType: QueryTypeSelectionState; filters: Filters; @@ -19,23 +33,60 @@ type Props = { lapisUrl: string; }; +type TabProps = Props & AdditionalProperties; + export const Result = (props: Props) => { + const [additionalProperties, setAdditionalProperties] = useState({ + downloadAsFile: false, + tabularOutputFormat: 'json', + compression: undefined, + }); + + const tabProps = { ...props, ...additionalProperties }; + const tabs = [ - { name: 'Query URL', content: }, - { name: 'R code', content: }, - { name: 'Python code', content: }, + { name: 'Query URL', content: }, + { name: 'R code', content: }, + { name: 'Python code', content: }, ]; return ( - - {tabs.map((tab) => ( - {tab.content} - ))} - + <> + setAdditionalProperties((prev) => ({ ...prev, downloadAsFile: !prev.downloadAsFile }))} + /> + setAdditionalProperties((prev) => ({ ...prev, tabularOutputFormat: value }))} + /> +
+ Do you want to fetch compressed data? + + {compressionOptions.map(({ value, label }) => ( + setAdditionalProperties((prev) => ({ ...prev, compression: value }))} + /> + ))} + +
+ + {tabs.map((tab) => ( + {tab.content} + ))} + + ); }; -function constructGetQueryUrl(props: Props, tabularOutputFormat: TabularOutputFormat) { +function constructGetQueryUrl(props: TabProps) { const { lapisUrl, endpoint, body } = constructPostQuery(props); const params = new URLSearchParams(); for (let [name, value] of Object.entries(body)) { @@ -51,25 +102,24 @@ function constructGetQueryUrl(props: Props, tabularOutputFormat: TabularOutputFo if ( queryType.selection !== 'nucleotideSequences' && queryType.selection !== 'aminoAcidSequences' && - tabularOutputFormat !== 'json' + props.tabularOutputFormat !== 'json' ) { - params.set('dataFormat', tabularOutputFormat); + params.set('dataFormat', props.tabularOutputFormat); + } + if (props.downloadAsFile) { + params.set('downloadAsFile', 'true'); + } + if (props.compression !== undefined) { + params.set('compression', props.compression); } return `${lapisUrl}${endpoint}?${params}`; } -const QueryUrlTab = (props: Props) => { - const [tabularOutputFormat, setTabularOutputFormat] = useState('json'); - - const queryUrl = constructGetQueryUrl(props, tabularOutputFormat); +const QueryUrlTab = (props: TabProps) => { + const queryUrl = constructGetQueryUrl(props); return ( -
Query URL: { ); }; -const RTab = (props: Props) => { +const RTab = (props: TabProps) => { return TODO R code; }; -const PythonTab = (props: Props) => { +const PythonTab = (props: TabProps) => { if (props.queryType.selection === 'nucleotideSequences' || props.queryType.selection === 'aminoAcidSequences') { return TODO Code for fetching sequences; } - const propsWithJson: Props = { + const propsWithJson: TabProps = { ...props, }; const { lapisUrl, endpoint, body, resultFields } = constructPostQuery(propsWithJson); @@ -103,7 +153,16 @@ const PythonTab = (props: Props) => { return {code}; }; -function constructPostQuery({ queryType, filters, config, lapisUrl, orderByLimitOffset }: Props): { +function constructPostQuery({ + queryType, + filters, + config, + lapisUrl, + orderByLimitOffset, + downloadAsFile, + tabularOutputFormat, + compression, +}: TabProps): { lapisUrl: string; endpoint: string; body: object; @@ -165,5 +224,14 @@ function constructPostQuery({ queryType, filters, config, lapisUrl, orderByLimit if (orderByLimitOffset.offset !== undefined) { body.offset = orderByLimitOffset.offset; } + if (downloadAsFile) { + body.downloadAsFile = true; + } + if (tabularOutputFormat !== 'json') { + body.dataFormat = tabularOutputFormat; + } + if (compression !== undefined) { + body.compression = compression; + } return { lapisUrl, endpoint, body, resultFields }; } diff --git a/lapis2-docs/src/components/TabsBox/react/TabsBox.tsx b/lapis2-docs/src/components/TabsBox/react/TabsBox.tsx index 574e7ca8..577be1e0 100644 --- a/lapis2-docs/src/components/TabsBox/react/TabsBox.tsx +++ b/lapis2-docs/src/components/TabsBox/react/TabsBox.tsx @@ -18,7 +18,7 @@ export const TabsBox = ({ children }: Props) => { const [activeTab, setActiveTab] = useState(0); return ( -
+
{tabs.map((tab, index) => ( `. +This will prompt browsers to download the data as file. + +```http +GET /sample/aggregated?downloadAsFile=true +``` + +## Compression + +LAPIS supports gzip and Zstd compression. +You can request compressed data via the `Accept-Encoding` header. + +```http +GET /sample/aggregated +Accept-Encoding: gzip +``` + +```http +POST /sample/aggregated +Accept-Encoding: zstd +``` + +LAPIS will set the `Content-Encoding` header in the response to indicate the compression used. + +:::note +Alternatively, you can use the `compression` property in the request. +Refer to the Swagger UI for allowed values. + +```http +GET /sample/aggregated?compression=gzip +``` + +::: diff --git a/lapis2-docs/src/content/docs/references/filters.mdx b/lapis2-docs/src/content/docs/references/filters.mdx index ec4822c2..e8619d7b 100644 --- a/lapis2-docs/src/content/docs/references/filters.mdx +++ b/lapis2-docs/src/content/docs/references/filters.mdx @@ -5,6 +5,6 @@ description: Filters import FiltersTable from '../../../components/FiltersTable/FiltersTable.astro'; -This instance of LAPIS supports the following filters: +This instance of LAPIS supports the following sequence filters: diff --git a/lapis2-docs/public/images/docs/concepts/example_nucleotide_mutation_response_200.png b/lapis2-docs/src/images/concepts/example_nucleotide_mutation_response_200.png similarity index 100% rename from lapis2-docs/public/images/docs/concepts/example_nucleotide_mutation_response_200.png rename to lapis2-docs/src/images/concepts/example_nucleotide_mutation_response_200.png diff --git a/lapis2-docs/public/images/docs/concepts/example_nucleotide_mutation_schema.png b/lapis2-docs/src/images/concepts/example_nucleotide_mutation_schema.png similarity index 100% rename from lapis2-docs/public/images/docs/concepts/example_nucleotide_mutation_schema.png rename to lapis2-docs/src/images/concepts/example_nucleotide_mutation_schema.png diff --git a/lapis2-docs/src/images/references/media_type.png b/lapis2-docs/src/images/references/media_type.png new file mode 100644 index 00000000..83510b42 Binary files /dev/null and b/lapis2-docs/src/images/references/media_type.png differ diff --git a/lapis2-docs/tests/docs.spec.ts b/lapis2-docs/tests/docs.spec.ts index 0f44b1a2..7010773e 100644 --- a/lapis2-docs/tests/docs.spec.ts +++ b/lapis2-docs/tests/docs.spec.ts @@ -30,6 +30,7 @@ const referencesPages = prependToRelativeUrl( { title: 'Introduction', relativeUrl: '/introduction' }, { title: 'Fields', relativeUrl: '/fields' }, { title: 'Filters', relativeUrl: '/filters' }, + { title: 'Additional Request Properties', relativeUrl: '/additional-request-properties' }, { title: 'Open API / Swagger', relativeUrl: '/open-api-definition' }, { title: 'Database Config', relativeUrl: '/database-config' }, { title: 'Reference Genomes', relativeUrl: '/reference-genomes' }, diff --git a/lapis2-docs/tests/queryGenerator.page.ts b/lapis2-docs/tests/queryGenerator.page.ts index 4fbfe7ba..dfadfd48 100644 --- a/lapis2-docs/tests/queryGenerator.page.ts +++ b/lapis2-docs/tests/queryGenerator.page.ts @@ -69,6 +69,14 @@ export class QueryGeneratorPage { await this.page.getByLabel(format).check(); } + public async selectCompressionFormat(format: 'gzip' | 'zstd') { + await this.page.getByLabel(format).check(); + } + + public async checkDownloadAsFile() { + await this.page.getByLabel('Download as file').check(); + } + public async expectQueryUrlContains(expected: string) { await expect(this.page.getByRole('textbox', {})).toHaveValue( new RegExp(`^http://localhost:8080/sample.*${expected}`), diff --git a/lapis2-docs/tests/queryGenerator.spec.ts b/lapis2-docs/tests/queryGenerator.spec.ts index 2b29c11a..f73a17f8 100644 --- a/lapis2-docs/tests/queryGenerator.spec.ts +++ b/lapis2-docs/tests/queryGenerator.spec.ts @@ -20,6 +20,12 @@ test.describe('The query generator wizard', () => { await queryGeneratorPage.selectOutputFormat('CSV'); await queryGeneratorPage.expectQueryUrlContains('dataFormat=csv'); + await queryGeneratorPage.selectCompressionFormat('zstd'); + await queryGeneratorPage.expectQueryUrlContains('compression=zstd'); + + await queryGeneratorPage.checkDownloadAsFile(); + await queryGeneratorPage.expectQueryUrlContains('downloadAsFile=true'); + await queryGeneratorPage.viewPythonCode(); await queryGeneratorPage.expectCodeContains( '@dataclass class DataEntry: count: int primaryKey: Optional[str] date: Optional[str]', diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt index 172e30fb..a08140b0 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt @@ -49,11 +49,10 @@ const val LIMIT_DESCRIPTION = """The maximum number of entries to return in the const val OFFSET_DESCRIPTION = """The offset of the first entry to return in the response. This is useful for pagination in combination with \"limit\".""" -const val FORMAT_DESCRIPTION = - """The data format of the response. - Alternatively, the data format can be specified by setting the \"Accept\"-header. - You can include the parameter to return the CSV/TSV without headers: "$TEXT_CSV_WITHOUT_HEADERS_HEADER". - When both are specified, this parameter takes precedence.""" +const val FORMAT_DESCRIPTION = """The data format of the response. +Alternatively, the data format can be specified by setting the \"Accept\"-header. +You can include the parameter to return the CSV/TSV without headers: "$TEXT_CSV_WITHOUT_HEADERS_HEADER". +When both are specified, the request parameter takes precedence over the header.""" private const val MAYBE_DESCRIPTION = """ A mutation can be wrapped in a maybe expression "MAYBE(\)"