From 6058b9923f399c89f99dbc7fbc57eac6c5d231af Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer Date: Tue, 19 Dec 2023 16:07:19 +0100 Subject: [PATCH] feat(docs): generate correct URLs for nucleotide sequences endpoints in multi-segmented case #521 --- lapis2-docs/.env.example | 3 +- lapis2-docs/Dockerfile | 1 + lapis2-docs/README.md | 2 +- lapis2-docs/package-lock.json | 3 +- lapis2-docs/package.json | 3 +- .../QueryGenerator/QueryGenerator.tsx | 15 ++++-- .../QueryGenerator/QueryTypeSelection.tsx | 51 ++++++++++++++++--- .../QueryGenerator/QueryTypeSelectionState.ts | 21 ++++++-- .../src/components/QueryGenerator/Result.tsx | 17 ++++--- .../getting-started/generate-your-request.mdx | 4 +- lapis2-docs/src/reference_genomes.ts | 31 +++++++++++ lapis2-docs/test-docker-compose.yml | 1 + .../lapis/controller/LapisController.kt | 9 +++- .../MultiSegmentedSequenceController.kt | 9 +++- .../genspectrum/lapis/openApi/OpenApiDocs.kt | 7 ++- .../org/genspectrum/lapis/openApi/Schemas.kt | 16 ++++++ 16 files changed, 162 insertions(+), 31 deletions(-) create mode 100644 lapis2-docs/src/reference_genomes.ts diff --git a/lapis2-docs/.env.example b/lapis2-docs/.env.example index 3546f118..9959f6eb 100644 --- a/lapis2-docs/.env.example +++ b/lapis2-docs/.env.example @@ -1,3 +1,4 @@ # Create a .env file with this input might NOT work. You might need to properly set CONFIG_FILE as an # environment variable. -CONFIG_FILE=../lapis2/src/test/resources/config/testDatabaseConfig.yaml +CONFIG_FILE=../siloLapisTests/testData/testDatabaseConfig.yaml +REFERENCE_GENOMES_FILE=../siloLapisTests/testData/reference_genomes.json diff --git a/lapis2-docs/Dockerfile b/lapis2-docs/Dockerfile index 63c5fe8e..c1dc725a 100644 --- a/lapis2-docs/Dockerfile +++ b/lapis2-docs/Dockerfile @@ -6,6 +6,7 @@ RUN apk update && apk add curl COPY . . ENV CONFIG_FILE=/config/database_config.yaml +ENV REFERENCE_GENOMES_FILE=/config/reference_genomes.json EXPOSE 3000 VOLUME /config diff --git a/lapis2-docs/README.md b/lapis2-docs/README.md index 2bd8c21e..b8faf05a 100644 --- a/lapis2-docs/README.md +++ b/lapis2-docs/README.md @@ -17,7 +17,7 @@ This documentation is a website built with For running and building the website, the environment variables `LAPIS_URL` and `CONFIG_FILE` must be set, e.g.: ```shell -CONFIG_FILE=../siloLapisTests/testData/testDatabaseConfig.yaml LAPIS_URL=http://localhost:8080 npm run dev +CONFIG_FILE=../siloLapisTests/testData/testDatabaseConfig.yaml REFERENCE_GENOMES_FILE=../siloLapisTests/testData/reference_genomes.json LAPIS_URL=http://localhost:8080 npm run dev ``` ## Deploying diff --git a/lapis2-docs/package-lock.json b/lapis2-docs/package-lock.json index a3380bcd..3b35f8d8 100644 --- a/lapis2-docs/package-lock.json +++ b/lapis2-docs/package-lock.json @@ -22,7 +22,8 @@ "swagger-ui": "^5.10.5", "swagger-ui-react": "^5.10.5", "tailwindcss": "^3.3.7", - "yaml": "^2.3.4" + "yaml": "^2.3.4", + "zod": "^3.22.4" }, "devDependencies": { "@astrojs/check": "^0.3.2", diff --git a/lapis2-docs/package.json b/lapis2-docs/package.json index 5ce005cf..0bec5da4 100644 --- a/lapis2-docs/package.json +++ b/lapis2-docs/package.json @@ -27,7 +27,8 @@ "swagger-ui": "^5.10.5", "swagger-ui-react": "^5.10.5", "tailwindcss": "^3.3.7", - "yaml": "^2.3.4" + "yaml": "^2.3.4", + "zod": "^3.22.4" }, "devDependencies": { "@astrojs/check": "^0.3.2", diff --git a/lapis2-docs/src/components/QueryGenerator/QueryGenerator.tsx b/lapis2-docs/src/components/QueryGenerator/QueryGenerator.tsx index 02890200..eb213591 100644 --- a/lapis2-docs/src/components/QueryGenerator/QueryGenerator.tsx +++ b/lapis2-docs/src/components/QueryGenerator/QueryGenerator.tsx @@ -5,15 +5,17 @@ import { Result } from './Result'; import { type Config } from '../../config'; import { type OrderByLimitOffset, OrderLimitOffsetSelection } from './OrderLimitOffsetSelection.tsx'; import { getInitialQueryState, type QueryTypeSelectionState } from './QueryTypeSelectionState.ts'; +import { type ReferenceGenomes } from '../../reference_genomes.ts'; type Props = { config: Config; + referenceGenomes: ReferenceGenomes; lapisUrl: string; }; -export const QueryGenerator = ({ config, lapisUrl }: Props) => { +export const QueryGenerator = ({ config, referenceGenomes, lapisUrl }: Props) => { const [step, setStep] = useState(0); - const [queryType, setQueryType] = useState(getInitialQueryState()); + const [queryType, setQueryType] = useState(getInitialQueryState(referenceGenomes)); const [filters, setFilters] = useState(new Map()); const [orderByLimitOffset, setOrderByLimitOffset] = useState({ orderBy: [], @@ -33,7 +35,14 @@ export const QueryGenerator = ({ config, lapisUrl }: Props) => {
- {step === 0 && } + {step === 0 && ( + + )} {step === 1 && } {step === 2 && ( void; + onStateChange: Dispatch>; }; export const QueryTypeSelection = (props: Props) => { @@ -274,7 +277,7 @@ const Insertions = ({ state, onStateChange }: Props) => { ); }; -const NucleotideSequences = ({ state, onStateChange }: Props) => { +const NucleotideSequences = ({ referenceGenomes, state, onStateChange }: Props) => { const changeType = (type: AlignmentType) => { onStateChange({ ...state, @@ -282,6 +285,18 @@ const NucleotideSequences = ({ state, onStateChange }: Props) => { }); }; + const changeSegment = (segmentName: string) => + onStateChange((prev) => ({ + ...prev, + nucleotideSequences: { + ...prev.nucleotideSequences, + segment: { + type: MULTI_SEGMENTED, + segmentName, + }, + }, + })); + return (
@@ -299,12 +314,29 @@ const NucleotideSequences = ({ state, onStateChange }: Props) => { /> ))} + {state.nucleotideSequences.segment.type === MULTI_SEGMENTED && ( + <> + Which segments are you interested in? + + + )}
); }; -const AminoAcidSequences = ({ state, onStateChange }: Props) => { +const AminoAcidSequences = ({ referenceGenomes, state, onStateChange }: Props) => { const changeGene = (gene: string) => { onStateChange({ ...state, @@ -316,13 +348,18 @@ const AminoAcidSequences = ({ state, onStateChange }: Props) => {
Which gene/reading frame are you interested in? - changeGene(e.target.value)} - /> + > + {referenceGenomes.genes.map((gene) => ( + + ))} +
); diff --git a/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts b/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts index 90fb15fc..d11f16e9 100644 --- a/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts +++ b/lapis2-docs/src/components/QueryGenerator/QueryTypeSelectionState.ts @@ -1,5 +1,6 @@ import type { Config, MetadataType } from '../../config.ts'; import type { ResultField, ResultFieldType } from '../../utils/code-generators/types.ts'; +import { isMultiSegmented, type ReferenceGenomes } from '../../reference_genomes.ts'; export type Selection = keyof Omit; @@ -9,6 +10,9 @@ export type DetailsType = 'all' | 'selected'; export const alignmentTypes = ['unaligned', 'aligned'] as const; export type AlignmentType = (typeof alignmentTypes)[number]; +export const SINGLE_SEGMENTED = 'singleSegmented' as const; +export const MULTI_SEGMENTED = 'multiSegmented' as const; + export type QueryTypeSelectionState = { selection: Selection; aggregatedAll: {}; @@ -28,13 +32,21 @@ export type QueryTypeSelectionState = { }; nucleotideSequences: { type: AlignmentType; + segment: + | { + type: typeof SINGLE_SEGMENTED; + } + | { + type: typeof MULTI_SEGMENTED; + segmentName: string; + }; }; aminoAcidSequences: { gene: string; }; }; -export function getInitialQueryState(): QueryTypeSelectionState { +export function getInitialQueryState(referenceGenomes: ReferenceGenomes): QueryTypeSelectionState { return { selection: 'aggregatedAll', aggregatedAll: {}, @@ -54,9 +66,12 @@ export function getInitialQueryState(): QueryTypeSelectionState { }, nucleotideSequences: { type: 'unaligned', + segment: isMultiSegmented(referenceGenomes) + ? { type: MULTI_SEGMENTED, segmentName: referenceGenomes.nucleotideSequences[0].name } + : { type: SINGLE_SEGMENTED }, }, aminoAcidSequences: { - gene: '', + gene: referenceGenomes.genes[0].name, }, }; } @@ -93,9 +108,7 @@ function getFieldsThatAreAlwaysPresent(selection: Selection): ResultField[] { { name: 'count', type: 'integer', nullable: false }, ]; case 'details': - return []; case 'nucleotideSequences': - return []; case 'aminoAcidSequences': return []; } diff --git a/lapis2-docs/src/components/QueryGenerator/Result.tsx b/lapis2-docs/src/components/QueryGenerator/Result.tsx index b76e9aa7..3ac6d842 100644 --- a/lapis2-docs/src/components/QueryGenerator/Result.tsx +++ b/lapis2-docs/src/components/QueryGenerator/Result.tsx @@ -8,7 +8,7 @@ import type { Config } from '../../config'; import { useState } from 'react'; import type { ResultField } from '../../utils/code-generators/types'; import { ContainerWrapper, LabelWrapper } from './styled-components'; -import { getResultFields, type QueryTypeSelectionState } from './QueryTypeSelectionState.ts'; +import { getResultFields, MULTI_SEGMENTED, type QueryTypeSelectionState } from './QueryTypeSelectionState.ts'; import type { OrderByLimitOffset } from './OrderLimitOffsetSelection.tsx'; type Props = { @@ -125,11 +125,11 @@ function constructPostQuery({ queryType, filters, config, lapisUrl, orderByLimit } break; case 'mutations': - endpoint += queryType.mutations.type === 'nucleotide' ? 'nuc-mutations' : 'aa-mutations'; + endpoint += queryType.mutations.type === 'nucleotide' ? 'nucleotideMutations' : 'aminoAcidMutations'; body.minProportion = queryType.mutations.minProportion; break; case 'insertions': - endpoint += queryType.insertions.type === 'nucleotide' ? 'nuc-insertions' : 'aa-insertions'; + endpoint += queryType.insertions.type === 'nucleotide' ? 'nucleotideInsertions' : 'aminoAcidInsertions'; break; case 'details': endpoint += 'details'; @@ -139,11 +139,16 @@ function constructPostQuery({ queryType, filters, config, lapisUrl, orderByLimit } break; case 'nucleotideSequences': - // TODO(#521): multi segment case - endpoint += queryType.nucleotideSequences.type === 'unaligned' ? 'nuc-sequences' : 'nuc-sequences-aligned'; + endpoint += + queryType.nucleotideSequences.type === 'unaligned' + ? 'nucleotideSequences' + : 'alignedNucleotideSequences'; + if (queryType.nucleotideSequences.segment.type === MULTI_SEGMENTED) { + endpoint += `/${queryType.nucleotideSequences.segment.segmentName}`; + } break; case 'aminoAcidSequences': - endpoint += `aa-sequences-aligned/${queryType.aminoAcidSequences.gene}`; + endpoint += `alignedAminoAcidSequences/${queryType.aminoAcidSequences.gene}`; break; } for (let [name, value] of filters) { diff --git a/lapis2-docs/src/content/docs/getting-started/generate-your-request.mdx b/lapis2-docs/src/content/docs/getting-started/generate-your-request.mdx index e3b26fe4..62a74c36 100644 --- a/lapis2-docs/src/content/docs/getting-started/generate-your-request.mdx +++ b/lapis2-docs/src/content/docs/getting-started/generate-your-request.mdx @@ -5,7 +5,7 @@ description: A step-by-step wizard to help you generate a request import { getConfig } from '../../../config.ts'; import { getLapisUrl } from '../../../lapisUrl.js'; - +import { getReferenceGenomes } from "../../../reference_genomes.js"; import { QueryGenerator } from '../../../components/QueryGenerator/QueryGenerator.tsx'; - + diff --git a/lapis2-docs/src/reference_genomes.ts b/lapis2-docs/src/reference_genomes.ts new file mode 100644 index 00000000..a69560f4 --- /dev/null +++ b/lapis2-docs/src/reference_genomes.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; +import { z } from 'zod'; + +const referenceSequenceSchema = z.object({ + name: z.string(), + sequence: z.string(), +}); + +const referenceGenomesSchema = z.object({ + nucleotideSequences: z.array(referenceSequenceSchema), + genes: z.array(referenceSequenceSchema), +}); + +export type ReferenceGenomes = z.infer; + +let _referenceGenomes: ReferenceGenomes | null = null; + +export function getReferenceGenomes(): ReferenceGenomes { + if (_referenceGenomes === null) { + const configFilePath = process.env.REFERENCE_GENOMES_FILE; + if (configFilePath === undefined) { + throw new Error('Please set the environment variable REFERENCE_GENOMES_FILE.'); + } + _referenceGenomes = referenceGenomesSchema.parse(JSON.parse(fs.readFileSync(configFilePath, 'utf8'))); + } + return _referenceGenomes; +} + +export function isMultiSegmented(referenceGenomes: ReferenceGenomes): boolean { + return referenceGenomes.nucleotideSequences.length > 1; +} diff --git a/lapis2-docs/test-docker-compose.yml b/lapis2-docs/test-docker-compose.yml index b7527564..1af307ec 100644 --- a/lapis2-docs/test-docker-compose.yml +++ b/lapis2-docs/test-docker-compose.yml @@ -6,6 +6,7 @@ services: - "3000:3000" volumes: - ../siloLapisTests/testData/testDatabaseConfig.yaml:/config/database_config.yaml + - ../siloLapisTests/testData/reference_genomes.json:/config/reference_genomes.json environment: LAPIS_URL: localhost:8080 BASE_URL: /docs/ diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt index 5f99b59d..b7cfcc6b 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt @@ -20,6 +20,7 @@ import org.genspectrum.lapis.openApi.DataFormat import org.genspectrum.lapis.openApi.DetailsFields import org.genspectrum.lapis.openApi.DetailsOrderByFields import org.genspectrum.lapis.openApi.FieldsToAggregateBy +import org.genspectrum.lapis.openApi.Gene import org.genspectrum.lapis.openApi.INSERTIONS_REQUEST_SCHEMA import org.genspectrum.lapis.openApi.InsertionsOrderByFields import org.genspectrum.lapis.openApi.LapisAggregatedResponse @@ -1269,7 +1270,9 @@ class LapisController( @GetMapping("$ALIGNED_AMINO_ACID_SEQUENCES_ROUTE/{gene}", produces = ["text/x-fasta"]) @LapisAlignedAminoAcidSequenceResponse fun getAlignedAminoAcidSequence( - @PathVariable(name = "gene", required = true) gene: String, + @PathVariable(name = "gene", required = true) + @Gene + gene: String, @PrimitiveFieldFilters @RequestParam sequenceFilters: GetRequestSequenceFilters?, @@ -1314,7 +1317,9 @@ class LapisController( @PostMapping("$ALIGNED_AMINO_ACID_SEQUENCES_ROUTE/{gene}", produces = ["text/x-fasta"]) @LapisAlignedAminoAcidSequenceResponse fun postAlignedAminoAcidSequence( - @PathVariable(name = "gene", required = true) gene: String, + @PathVariable(name = "gene", required = true) + @Gene + gene: String, @Parameter(schema = Schema(ref = "#/components/schemas/$ALIGNED_AMINO_ACID_SEQUENCE_REQUEST_SCHEMA")) @RequestBody request: SequenceFiltersRequest, diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt index e887832e..3fbe7e8e 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt @@ -15,6 +15,7 @@ import org.genspectrum.lapis.openApi.NucleotideMutations import org.genspectrum.lapis.openApi.NucleotideSequencesOrderByFields import org.genspectrum.lapis.openApi.Offset import org.genspectrum.lapis.openApi.PrimitiveFieldFilters +import org.genspectrum.lapis.openApi.Segment import org.genspectrum.lapis.request.AminoAcidInsertion import org.genspectrum.lapis.request.AminoAcidMutation import org.genspectrum.lapis.request.GetRequestSequenceFilters @@ -45,7 +46,9 @@ class MultiSegmentedSequenceController( @GetMapping("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/{segment}", produces = ["text/x-fasta"]) @LapisAlignedMultiSegmentedNucleotideSequenceResponse fun getAlignedNucleotideSequence( - @PathVariable(name = "segment", required = true) segment: String, + @PathVariable(name = "segment", required = true) + @Segment + segment: String, @PrimitiveFieldFilters @RequestParam sequenceFilters: GetRequestSequenceFilters?, @@ -94,7 +97,9 @@ class MultiSegmentedSequenceController( @PostMapping("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/{segment}", produces = ["text/x-fasta"]) @LapisAlignedMultiSegmentedNucleotideSequenceResponse fun postAlignedNucleotideSequence( - @PathVariable(name = "segment", required = true) segment: String, + @PathVariable(name = "segment", required = true) + @Segment + segment: String, @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_SEQUENCE_REQUEST_SCHEMA")) @RequestBody request: SequenceFiltersRequest, diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt index 61a32197..48bb3ea7 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/OpenApiDocs.kt @@ -208,6 +208,11 @@ fun buildOpenApiSchema( NUCLEOTIDE_SEQUENCES_ORDER_BY_FIELDS_SCHEMA, arraySchema(nucleotideSequenceFieldsEnum(referenceGenome, databaseConfig)), ) + .addSchemas( + SEGMENT_SCHEMA, + fieldsEnum(additionalFields = referenceGenome.nucleotideSequences.map { it.name }), + ) + .addSchemas(GENE_SCHEMA, fieldsEnum(additionalFields = referenceGenome.genes.map { it.name })) .addSchemas(LIMIT_SCHEMA, limitSchema()) .addSchemas(OFFSET_SCHEMA, offsetSchema()) .addSchemas(FORMAT_SCHEMA, formatSchema()), @@ -510,7 +515,7 @@ private fun nucleotideSequenceFieldsEnum( ) = fieldsEnum(emptyList(), referenceGenome.nucleotideSequences.map { it.name } + databaseConfig.schema.primaryKey) private fun fieldsEnum( - databaseConfig: List, + databaseConfig: List = emptyList(), additionalFields: List = emptyList(), ) = Schema() .type("string") diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt index cda28746..c546c50c 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/openApi/Schemas.kt @@ -58,6 +58,8 @@ const val OFFSET_SCHEMA = "Offset" const val FORMAT_SCHEMA = "DataFormat" const val FIELDS_TO_AGGREGATE_BY_SCHEMA = "FieldsToAggregateBy" const val DETAILS_FIELDS_SCHEMA = "DetailsFields" +const val GENE_SCHEMA = "Gene" +const val SEGMENT_SCHEMA = "Segment" const val LAPIS_INFO_DESCRIPTION = "Information about LAPIS." const val LAPIS_DATA_VERSION_EXAMPLE = "1702305399" @@ -276,3 +278,17 @@ annotation class FieldsToAggregateBy description = DETAILS_FIELDS_DESCRIPTION, ) annotation class DetailsFields + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Parameter( + schema = Schema(ref = "#/components/schemas/$GENE_SCHEMA"), +) +annotation class Gene + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Parameter( + schema = Schema(ref = "#/components/schemas/$SEGMENT_SCHEMA"), +) +annotation class Segment