Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7038c6f
Add tables
fhennig Mar 11, 2026
f1bca94
add a 'create collection' endpoint
fhennig Mar 11, 2026
1fc3d40
Add 'GET' for collections; add tests
fhennig Mar 11, 2026
70b1194
Add setup to spin up local postgres instance for testing
fhennig Mar 11, 2026
c205631
Add 'GET' for collections by ID
fhennig Mar 11, 2026
2fb9f00
foo
fhennig Mar 11, 2026
b9049d3
change mutation list defintion
fhennig Mar 11, 2026
649d910
Add delete implementation
fhennig Mar 12, 2026
1894e8b
put pt1
fhennig Mar 12, 2026
0d3f253
dedicated Variant.kt api file
fhennig Mar 12, 2026
28743cd
Change collection & variant IDs from UUID to Long
fhennig Mar 16, 2026
fbc5f44
review
fhennig Mar 16, 2026
f2ec21a
Consolidate DB setup: use root docker-compose for local dev
fhennig Mar 17, 2026
ccef31c
Move validateIsValidOrganism into DashboardsConfig class
fhennig Mar 17, 2026
fc75a8d
Fix variant deletion in putCollection and clean up imports
fhennig Mar 17, 2026
47a7c19
Extract duplicated lineage filter validation into a single method
fhennig Mar 17, 2026
1dccf7f
optimize collection loading
fhennig Mar 17, 2026
96d1228
better MutationListDefinition
fhennig Mar 17, 2026
81ad926
Update backend/src/test/kotlin/org/genspectrum/dashboardsbackend/cont…
fhennig Mar 17, 2026
836d747
some progress, but failing tests
fhennig Mar 17, 2026
9629cdd
simplify Mutationlist
fhennig Mar 18, 2026
40c7702
Use import for MutationListDefinition in VariantTable
fhennig Mar 18, 2026
99cd270
Add doc comment to validateLineageFilters
fhennig Mar 18, 2026
4a8185c
Reduce duplication by routing createCollection through createVariantE…
fhennig Mar 18, 2026
fb205e1
Use when expression to capture variantId in putCollection
fhennig Mar 18, 2026
51152bb
Split CollectionsControllerTest by HTTP verb
fhennig Mar 18, 2026
13880e3
Rename test to follow GIVEN/WHEN/THEN convention
fhennig Mar 18, 2026
a2eea38
Use imports instead of fully-qualified names in CollectionsPostTest
fhennig Mar 18, 2026
d86c272
Add test asserting type field is present on variants in GET response
fhennig Mar 18, 2026
513ad8c
Add type field to VariantRequest subclasses for consistency with Variant
fhennig Mar 18, 2026
d550451
Revert "Add type field to VariantRequest subclasses for consistency w…
fhennig Mar 18, 2026
29061e3
format
fhennig Mar 18, 2026
aae53d6
docs: make the correct type of `type` appear in OpenAPI (#1088)
fengelniederhammer Mar 19, 2026
65512bf
Add test
fhennig Mar 19, 2026
1d19b67
Remove unused function
fhennig Mar 19, 2026
3f03d59
remove explicit spring JDBC import
fhennig Mar 19, 2026
2cba7bb
remove unused exception handler
fhennig Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,40 @@ You have to provide config information to the backend:
* Dashboards configuration, e.g. the LAPIS instances of the organisms.
We have profiles available that only need to be activated via `spring.profiles.active`.
* Database connection configuration: values need to be passed via [external properties](https://docs.spring.io/spring-boot/reference/features/external-config.html).
For local development, we have a `local-db` profile available.
For local development, we have a `local-db` profile available.
You can also check that for required properties.

### Start local database

Start the local PostgreSQL database using Docker Compose (from the repo root):

```bash
docker compose up -d database
```

Stop the database:

```bash
docker compose down database
```

Stop and remove data volumes:

```bash
docker compose down -v database
```

### Run the backend

To run the backend locally, you can use the following command:
```bash
./gradlew bootRun --args='--spring.profiles.active=local-db,dashboards-prod'
```

The backend will be available at:
- Base URL: `http://localhost:8080`
- Swagger UI: `http://localhost:8080/swagger-ui/index.html`

Run tests:

```bash
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.genspectrum.dashboardsbackend.api

import io.swagger.v3.oas.annotations.media.Schema

@Schema(
description = "A collection of variants",
example = """
{
"id": 1,
"name": "My Collection",
"ownedBy": "user123",
"organism": "covid",
"description": "A collection of interesting variants",
"variants": []
}
""",
)
data class Collection(
val id: Long,
val name: String,
val ownedBy: String,
val organism: String,
val description: String?,
val variants: List<Variant>,
)

@Schema(
description = "Request to create a collection",
example = """
{
"name": "My Collection",
"organism": "covid",
"description": "A collection of interesting variants",
"variants": [
{
"type": "query",
"name": "BA.2 in USA",
"description": "BA.2 lineage cases in USA",
"countQuery": "country='USA' & lineage='BA.2'",
"coverageQuery": "country='USA'"
}
]
}
""",
)
data class CollectionRequest(
val name: String,
val organism: String,
val description: String? = null,
val variants: List<VariantRequest>,
)

@Schema(
description = "Request to update a collection",
example = """
{
"name": "Updated Collection Name",
"description": "Updated description",
"variants": [
{
"type": "query",
"id": 1,
"name": "BA.2 in USA",
"description": "BA.2 lineage cases in USA",
"countQuery": "country='USA' & lineage='BA.2'",
"coverageQuery": "country='USA'"
},
{
"type": "query",
"name": "New Variant Without ID",
"countQuery": "country='Germany'"
}
]
}
""",
)
data class CollectionUpdate(
val name: String? = null,
val description: String? = null,
val variants: List<VariantUpdate>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.genspectrum.dashboardsbackend.api

import com.fasterxml.jackson.annotation.JsonInclude

/**
* A JSON object with mutation lists (keys: aaMutations, nucMutations, ...)
* as well as lineage filtering under the "filters" key
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
data class MutationListDefinition(
val aaMutations: List<String>? = null,
val nucMutations: List<String>? = null,
val aaInsertions: List<String>? = null,
val nucInsertions: List<String>? = null,
val filters: Map<String, String>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package org.genspectrum.dashboardsbackend.api

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import io.swagger.v3.oas.annotations.media.Schema
import org.genspectrum.dashboardsbackend.api.Variant.MutationListVariant
import org.genspectrum.dashboardsbackend.api.Variant.QueryVariant

enum class QueryVariantType {
@JsonProperty("query")
QUERY,
}

enum class MutationListVariantType {
@JsonProperty("mutationList")
MUTATION_LIST,
}

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
)
@JsonSubTypes(
JsonSubTypes.Type(value = QueryVariant::class, name = "query"),
JsonSubTypes.Type(value = MutationListVariant::class, name = "mutationList"),
)
@Schema(
description = "Base interface for different variant types",
)
sealed interface Variant {
Comment on lines +21 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type doesn't tell which value is allowed here. Can we copy the magic that we did in LAPIS over here?

Image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I just saw that it looks good for the GET request, but not in the POST.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find it anymore how we did it in LAPIS, can you link it? IMO it's a shortcoming of the generator that this is missing and I personally don't mind it much, I'd rather just solve this with a doc string and move on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I made it work: #1088

val id: Long
val collectionId: Long

@Schema(
description = "A variant defined by LAPIS queries",
example = """
{
"type": "query",
"id": 1,
"collectionId": 2,
"name": "BA.2 in USA",
"description": "BA.2 lineage cases in USA",
"countQuery": "country='USA' & lineage='BA.2'",
"coverageQuery": "country='USA'"
}
""",
)
data class QueryVariant @JsonCreator constructor(
override val id: Long,
override val collectionId: Long,
val name: String,
val description: String?,
val countQuery: String,
val coverageQuery: String? = null,
) : Variant {
val type: QueryVariantType = QueryVariantType.QUERY
}

@Schema(
description = "A variant defined by a list of mutations",
example = """
{
"type": "mutationList",
"id": 1,
"collectionId": 2,
"name": "Omicron mutations",
"description": "Key mutations for Omicron",
"mutationList": {
"aaMutations": ["S:N501Y", "S:E484K", "S:K417N"]
}
}
""",
)
data class MutationListVariant @JsonCreator constructor(
override val id: Long,
override val collectionId: Long,
val name: String,
val description: String?,
val mutationList: MutationListDefinition,
) : Variant {
val type: MutationListVariantType = MutationListVariantType.MUTATION_LIST
}
}

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
)
@JsonSubTypes(
JsonSubTypes.Type(value = VariantRequest.QueryVariantRequest::class, name = "query"),
JsonSubTypes.Type(value = VariantRequest.MutationListVariantRequest::class, name = "mutationList"),
)
@Schema(
description = "Request to create a variant",
)
sealed interface VariantRequest {
@Schema(
description = "Request to create a query variant",
example = """
{
"type": "query",
"name": "BA.2 in USA",
"description": "BA.2 lineage cases in USA",
"countQuery": "country='USA' & lineage='BA.2'",
"coverageQuery": "country='USA'"
}
""",
)
data class QueryVariantRequest(
val name: String,
val description: String? = null,
val countQuery: String,
val coverageQuery: String? = null,
) : VariantRequest {
val type: QueryVariantType = QueryVariantType.QUERY
}

@Schema(
description = "Request to create a mutation list variant",
example = """
{
"type": "mutationList",
"name": "Omicron mutations",
"description": "Key mutations for Omicron",
"mutationList": {
"aaMutations": ["S:N501Y", "S:E484K", "S:K417N"]
}
}
""",
)
data class MutationListVariantRequest(
val name: String,
val description: String? = null,
val mutationList: MutationListDefinition,
) : VariantRequest {
val type: MutationListVariantType = MutationListVariantType.MUTATION_LIST
}
}

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
)
@JsonSubTypes(
JsonSubTypes.Type(value = VariantUpdate.QueryVariantUpdate::class, name = "query"),
JsonSubTypes.Type(value = VariantUpdate.MutationListVariantUpdate::class, name = "mutationList"),
)
@Schema(
description = "Request to update or create a variant",
)
sealed interface VariantUpdate {
val id: Long?

@Schema(
description = "Request to update or create a query variant",
example = """
{
"type": "query",
"id": 1,
"name": "BA.2 in USA",
"description": "BA.2 lineage cases in USA",
"countQuery": "country='USA' & lineage='BA.2'",
"coverageQuery": "country='USA'"
}
""",
)
data class QueryVariantUpdate(
override val id: Long? = null,
val name: String,
val description: String? = null,
val countQuery: String,
val coverageQuery: String? = null,
) : VariantUpdate {
val type: QueryVariantType = QueryVariantType.QUERY
}

@Schema(
description = "Request to update or create a mutation list variant",
example = """
{
"type": "mutationList",
"id": 1,
"name": "Omicron mutations",
"description": "Key mutations for Omicron",
"mutationList": {
"aaMutations": ["S:N501Y", "S:E484K", "S:K417N"]
}
}
""",
)
data class MutationListVariantUpdate(
override val id: Long? = null,
val name: String,
val description: String? = null,
val mutationList: MutationListDefinition,
) : VariantUpdate {
val type: MutationListVariantType = MutationListVariantType.MUTATION_LIST
}

fun toVariantRequest(): VariantRequest {
require(id == null) { "Cannot convert a VariantUpdate with an existing id to a VariantRequest: $id" }
return when (this) {
is QueryVariantUpdate -> VariantRequest.QueryVariantRequest(
name = name,
description = description,
countQuery = countQuery,
coverageQuery = coverageQuery,
)

is MutationListVariantUpdate -> VariantRequest.MutationListVariantRequest(
name = name,
description = description,
mutationList = mutationList,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.flywaydb.core.Flyway
import org.genspectrum.dashboardsbackend.logging.REQUEST_ID_HEADER
import org.genspectrum.dashboardsbackend.logging.REQUEST_ID_HEADER_DESCRIPTION
import org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration
import org.jetbrains.exposed.sql.Database
import org.springdoc.core.customizers.OperationCustomizer
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
Expand All @@ -31,6 +32,10 @@ class BackendSpringConfig {
.validateMigrationNaming(true)
val flyway = Flyway(configuration)
flyway.migrate()

// Set up exposed database connection after migration is done
Database.connect(dataSource)

return flyway
}

Expand Down
Loading