diff --git a/README.md b/README.md index 872c64fe..8ac5ce55 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Higher versions will also work if they are not specified in the table. | LAPIS | SILO | |--------|--------| +| 0.3.14 | 0.5.3 | +| 0.3.13 | 0.5.2 | | 0.3.13 | 0.5.2 | | 0.3.7 | 0.3.0 | | 0.2.10 | 0.2.14 | diff --git a/lapis-e2e/test/info.spec.ts b/lapis-e2e/test/info.spec.ts index 15f97a17..ec9c20d7 100644 --- a/lapis-e2e/test/info.spec.ts +++ b/lapis-e2e/test/info.spec.ts @@ -1,12 +1,10 @@ import { expect } from 'chai'; import { lapisInfoClient } from './common'; -describe('The info endpoind', () => { +describe('The info endpoint', () => { it('should return all infos', async () => { const info = await lapisInfoClient.getInfo(); - console.log(info); - expect(info.dataVersion).to.match(/\d+/); expect(info.lapisVersion).to.be.not.empty; expect(info.siloVersion).to.be.not.empty; diff --git a/lapis-e2e/test/lineageDefinition.spec.ts b/lapis-e2e/test/lineageDefinition.spec.ts new file mode 100644 index 00000000..73f68f98 --- /dev/null +++ b/lapis-e2e/test/lineageDefinition.spec.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import { basePath, lapisInfoClient } from './common'; + +describe('The lineageDefinition endpoint', () => { + it('should return the file as JSON', async () => { + const lineageDefinition = await lapisInfoClient.getLineageDefinition({ + column: 'pango_lineage', + }); + + expect(lineageDefinition['A']).to.deep.equal({}); + expect(lineageDefinition['A.1']).to.deep.equal({ parents: ['A'] }); + expect(lineageDefinition['AT.1']).to.deep.equal({ + aliases: ['B.1.1.370.1'], + parents: ['B.1.1.370'], + }); + }); + + it('should return the file as YAML', async () => { + const result = await fetch(basePath + '/sample/lineageDefinition/pango_lineage'); + const lineageDefinitionYaml = await result.text(); + + const expectedFileStart = `A: {} +A.1: + parents: + - A +A.11: + parents: + - A`; + + expect(lineageDefinitionYaml).to.match(new RegExp(`^${expectedFileStart}`)); + }); +}); diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt index 087ae4d6..3cc33775 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt @@ -21,6 +21,9 @@ const val AMINO_ACID_INSERTIONS_ENDPOINT_DESCRIPTION = considered.""" const val INFO_ENDPOINT_DESCRIPTION = "Returns information about LAPIS." const val DATABASE_CONFIG_ENDPOINT_DESCRIPTION = "Returns the database configuration." +const val LINEAGE_DEFINITION_ENDPOINT_DESCRIPTION = """Download the lineage definition file used for a certain column. +This can be used to reconstruct the lineage tree. +""" const val REFERENCE_GENOME_ENDPOINT_DESCRIPTION = "Returns the reference genome." const val ALIGNED_AMINO_ACID_SEQUENCE_ENDPOINT_DESCRIPTION = """Returns a string of aligned amino acid sequences. Only sequences matching the specified diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/controller/InfoController.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/controller/InfoController.kt index e852345f..e9914fae 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/controller/InfoController.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/controller/InfoController.kt @@ -9,13 +9,16 @@ import org.genspectrum.lapis.logging.RequestIdContext import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.response.LapisInfo import org.genspectrum.lapis.response.LapisInfoFactory +import org.genspectrum.lapis.silo.LineageDefinition import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController const val INFO_ROUTE = "/info" const val DATABASE_CONFIG_ROUTE = "/databaseConfig" +const val LINEAGE_DEFINITION_ROUTE = "/lineageDefinition" const val REFERENCE_GENOME_ROUTE = "/referenceGenome" @RestController @@ -45,6 +48,15 @@ class InfoController( @Operation(description = DATABASE_CONFIG_ENDPOINT_DESCRIPTION) fun getDatabaseConfigAsJson(): DatabaseConfig = databaseConfig + @GetMapping( + "$LINEAGE_DEFINITION_ROUTE/{column}", + produces = [MediaType.APPLICATION_JSON_VALUE, APPLICATION_YAML_VALUE], + ) + @Operation(description = LINEAGE_DEFINITION_ENDPOINT_DESCRIPTION) + fun getLineageDefinition( + @PathVariable("column") column: String, + ): LineageDefinition = siloQueryModel.getLineageDefinition(column) + @GetMapping(REFERENCE_GENOME_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @Operation(description = REFERENCE_GENOME_ENDPOINT_DESCRIPTION) fun getReferenceGenome(): ReferenceGenome = referenceGenome diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt index 08deb8b5..882a5ceb 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt @@ -178,4 +178,6 @@ class SiloQueryModel( ) fun getInfo(): InfoData = siloClient.callInfo() + + fun getLineageDefinition(column: String) = siloClient.getLineageDefinition(column) } diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt index 835c5000..7ef49ac8 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloClient.kt @@ -7,6 +7,7 @@ import org.genspectrum.lapis.controller.LapisHeaders.REQUEST_ID import org.genspectrum.lapis.logging.RequestContext import org.genspectrum.lapis.logging.RequestIdContext import org.genspectrum.lapis.response.InfoData +import org.genspectrum.lapis.util.YamlObjectMapper import org.springframework.cache.annotation.Cacheable import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -52,6 +53,12 @@ class SiloClient( dataVersion.dataVersion = info.dataVersion return info } + + fun getLineageDefinition(column: String): LineageDefinition { + log.info { "Calling SILO lineageDefinition for column '$column'" } + + return cachedSiloClient.getLineageDefinition(column) + } } const val SILO_QUERY_CACHE_NAME = "siloQueryCache" @@ -60,6 +67,7 @@ const val SILO_QUERY_CACHE_NAME = "siloQueryCache" open class CachedSiloClient( private val siloUris: SiloUris, private val objectMapper: ObjectMapper, + private val yamlObjectMapper: YamlObjectMapper, private val requestIdContext: RequestIdContext, private val requestContext: RequestContext, ) { @@ -114,6 +122,16 @@ open class CachedSiloClient( ) } + fun getLineageDefinition(column: String): LineageDefinition { + val response = send( + uri = siloUris.lineageDefinition(column), + bodyHandler = BodyHandlers.ofString(), + tryToReadSiloErrorFromBody = ::tryToReadSiloErrorFromString, + ) { it.GET() } + + return yamlObjectMapper.objectMapper.readValue(response.body()) + } + private fun send( uri: URI, bodyHandler: HttpResponse.BodyHandler, @@ -193,3 +211,10 @@ data class SiloErrorResponse(val error: String, val message: String) data class SiloInfo( val version: String, ) + +typealias LineageDefinition = Map + +data class LineageNode( + val parents: List?, + val aliases: List?, +) diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloUris.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloUris.kt index ddcbe40a..7703a263 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloUris.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloUris.kt @@ -6,8 +6,10 @@ import java.net.URI @Component class SiloUris( - @Value("\${silo.url}") siloUrl: String, + @Value("\${silo.url}") private val siloUrl: String, ) { val query = URI("$siloUrl/query") val info = URI("$siloUrl/info") + + fun lineageDefinition(column: String): URI = URI("$siloUrl/lineageDefinition/").resolve(column) } diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt index 6eecf183..06a7fc58 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt @@ -435,6 +435,48 @@ class SiloClientTest( assertThat(exception.message, containsString(errorMessage)) } + @Test + fun `get lineage definition`() { + val columnName = "test_column" + MockServerClient("localhost", MOCK_SERVER_PORT) + .`when`( + request() + .withMethod("GET") + .withPath("/lineageDefinition/$columnName") + .withHeader("X-Request-Id", REQUEST_ID_VALUE), + ) + .respond( + response() + .withStatusCode(200) + .withBody( + """ + A: {} + A.1: + parents: + - A + B: + aliases: + - A.1.1 + parents: + - A.1 + """.trimIndent(), + ), + ) + + val actual = underTest.getLineageDefinition(columnName) + + assertThat( + actual, + equalTo( + mapOf( + "A" to LineageNode(parents = null, aliases = null), + "A.1" to LineageNode(parents = listOf("A"), aliases = null), + "B" to LineageNode(parents = listOf("A.1"), aliases = listOf("A.1.1")), + ), + ), + ) + } + companion object { @JvmStatic val mutationActions = listOf(