Skip to content

Commit 31c7b85

Browse files
authored
Merge pull request #1133 from GenSpectrum/main
chore: update prod from main
2 parents 0ec9267 + 376b393 commit 31c7b85

41 files changed

Lines changed: 884 additions & 262 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/backend.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ jobs:
2727
- name: Execute Tests
2828
run: ./gradlew test
2929

30+
- name: Upload test report
31+
uses: actions/upload-artifact@v4
32+
if: failure()
33+
with:
34+
name: backend-test-report
35+
path: backend/build/reports/tests/test/
36+
3037
- name: Check Format And Lint
3138
run: ./gradlew ktlintCheck
3239

.github/workflows/e2e.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ jobs:
3737
run: echo "branchTag=${TAG##*:}" >> $GITHUB_OUTPUT
3838

3939
- name: Wait for website Docker image
40-
uses: lewagon/wait-on-check-action@v1.6.0
40+
uses: lewagon/wait-on-check-action@v1.6.1
4141
with:
4242
ref: ${{ github.sha }}
4343
check-name: Build Website Docker Image
4444
repo-token: ${{ secrets.GITHUB_TOKEN }}
4545
wait-interval: 10
4646
- name: Wait for backend Docker image
47-
uses: lewagon/wait-on-check-action@v1.6.0
47+
uses: lewagon/wait-on-check-action@v1.6.1
4848
with:
4949
ref: ${{ github.sha }}
5050
check-name: Build Backend Docker Image

AGENTS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# AGENTS.md — Coding Agent Guidelines
2+
3+
This monorepo contains two packages:
4+
- **`website/`** — Astro + React frontend (TypeScript), see `website/AGENTS.md`
5+
- **`backend/`** — Kotlin + Spring Boot backend, see `backend/AGENTS.md`
6+
7+
---
8+
9+
## Repository Structure
10+
11+
```
12+
/
13+
├── website/ # Astro/React frontend
14+
├── backend/ # Kotlin/Spring Boot backend
15+
├── docs/ # Architecture documentation (arc42)
16+
└── docker-compose.yml
17+
```
18+
19+
---
20+
21+
## Commit Messages
22+
23+
Follow [Conventional Commits](https://www.conventionalcommits.org):
24+
```
25+
feat: add new organism dashboard
26+
fix: correct date range calculation
27+
chore: update dependencies
28+
```
29+
- PR titles become the squash-merge commit message — they must also follow conventional commits.
30+
- Messages should explain **why** a change was made, not just what.
31+
32+
---
33+
34+
## Architecture Notes
35+
36+
- **State in URL**: View state (filters, selected variants) is stored as query parameters so pages are bookmarkable and shareable.
37+
- **LAPIS**: All genomic data queries go directly from the browser to LAPIS instances — they do not go through the backend.
38+
- **Backend**: Only handles subscriptions and collections features; authentication is handled in the frontend (Astro middleware) and proxied to the backend.
39+
- **Organisms**: Adding a new organism requires entries in `website/src/types/Organism.ts`, `website/src/views/`, and corresponding Astro pages.

backend/AGENTS.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# AGENTS.md — Backend
2+
3+
Kotlin + Spring Boot backend. All commands run from `backend/`. Docker must be running (tests use Testcontainers).
4+
5+
---
6+
7+
## Build & Dev Commands
8+
9+
```bash
10+
./gradlew build # Build
11+
./gradlew test # Run all tests
12+
./gradlew bootRun --args='--spring.profiles.active=local-db,dashboards-prod' # Run locally
13+
```
14+
15+
The backend will be available at `http://localhost:8080`. Swagger UI at `http://localhost:8080/swagger-ui/index.html`.
16+
The database from the top-level Docker Compose file needs to be running.
17+
18+
## Running a Single Test
19+
20+
```bash
21+
# Run a single test class:
22+
./gradlew test --tests "org.genspectrum.dashboardsbackend.controller.SubscriptionsControllerTest"
23+
24+
# Run a single test method (use the full backtick-quoted name):
25+
./gradlew test --tests "org.genspectrum.dashboardsbackend.controller.SubscriptionsControllerTest.GIVEN I created a subscription WHEN getting subscriptions THEN contains created subscription"
26+
```
27+
28+
## Lint
29+
30+
```bash
31+
./gradlew ktlintCheck
32+
./gradlew ktlintFormat
33+
```
34+
35+
---
36+
37+
## Code Style
38+
39+
### Style
40+
- Enforced by **ktlint** (version 1.4.1). Run `./gradlew ktlintFormat` to auto-fix.
41+
- Follow standard Kotlin idioms: data classes, sealed interfaces, extension functions.
42+
43+
### Naming
44+
- Classes/interfaces: `PascalCase`
45+
- Functions/variables: `camelCase`
46+
- Constants: `UPPER_CASE`
47+
- Test method names use backtick syntax describing behavior:
48+
```kotlin
49+
@Test
50+
fun `GIVEN some state WHEN action THEN expected result`() { ... }
51+
```
52+
53+
### Logging
54+
Use `kotlin-logging` (`io.github.microutils:kotlin-logging-jvm`):
55+
```kotlin
56+
private val log = KotlinLogging.logger {}
57+
log.info { "message" }
58+
```
59+
60+
(There should be one global logger instance that can be reused everywhere)
61+
62+
### Error Handling
63+
- Throw typed exceptions: `BadRequestException`, `NotFoundException`, `ForbiddenException`.
64+
- The global `ExceptionHandler` maps these to appropriate HTTP status codes with `ProblemDetail` bodies.
65+
- Log unexpected exceptions at `error` level; expected/handled exceptions at `info` or `warn`.
66+
67+
### Testing
68+
- Use `@SpringBootTest` + `@AutoConfigureMockMvc` for integration tests.
69+
- Since the API is a relatively thin layer around the database, most tests should focus on interacting with the code via the endpoints.
70+
Only add dedicated unit tests for isolated logic that is more complex.
71+
- Use **MockK** (via `springmockk`) instead of Mockito.
72+
- Use **Testcontainers** for database tests (Docker required).
73+
- Tests are organized by controller/feature, not by test type.

backend/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ Run tests:
5252
./gradlew test
5353
```
5454

55+
## Authentication
56+
While the backend uses user IDs internally, for example to track owners
57+
of collections or subscriptions, it doesn't handle authentication of users.
58+
This is done in the frontend. All requests are proxied through the frontend
59+
server, where authentication is handled, and then the authenticated requests
60+
are sent to the backend.
61+
5562
## Logging
5663
Logs to rotating files are stored in `./logs` and written to stdout.
5764
In the Docker container, log files are stored in `/workspace/logs`

backend/build.gradle.kts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
2+
import java.io.ByteArrayOutputStream
23

34
plugins {
45
kotlin("jvm") version "2.3.20"
56
kotlin("plugin.spring") version "2.3.20"
6-
id("org.springframework.boot") version "3.4.0"
7+
id("org.springframework.boot") version "3.5.13"
78
id("io.spring.dependency-management") version "1.1.7"
89
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
910
id("org.springdoc.openapi-gradle-plugin") version "1.9.0"
@@ -27,11 +28,13 @@ dependencies {
2728
implementation("org.jetbrains.kotlin:kotlin-reflect")
2829
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
2930
implementation("org.jetbrains.kotlin:kotlin-stdlib")
30-
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
31+
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
3132
implementation("org.flywaydb:flyway-database-postgresql:11.0.0")
3233
implementation("org.postgresql:postgresql:42.7.10")
33-
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.56.0")
34-
implementation("org.jetbrains.exposed:exposed-json:0.56.0")
34+
implementation("org.jetbrains.exposed:exposed-spring-boot-starter:1.2.0")
35+
implementation("org.jetbrains.exposed:exposed-json:1.2.0")
36+
implementation("org.jetbrains.exposed:exposed-jdbc:1.2.0")
37+
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.2.0")
3538
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat")
3639
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
3740

@@ -58,7 +61,20 @@ kotlin {
5861
}
5962
}
6063

64+
val checkDockerAvailable by tasks.registering(Exec::class) {
65+
commandLine("docker", "info")
66+
standardOutput = ByteArrayOutputStream()
67+
errorOutput = ByteArrayOutputStream()
68+
isIgnoreExitValue = true
69+
doLast {
70+
if (executionResult.get().exitValue != 0) {
71+
throw GradleException("Docker is not available. Please start Docker before running tests.")
72+
}
73+
}
74+
}
75+
6176
tasks.withType<Test> {
77+
dependsOn(checkDockerAvailable)
6278
useJUnitPlatform()
6379
testLogging {
6480
events("FAILED")

backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Collection.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.genspectrum.dashboardsbackend.api
22

33
import io.swagger.v3.oas.annotations.media.Schema
4+
import kotlin.time.Instant
45

56
@Schema(
67
description = "A collection of variants",
@@ -11,7 +12,9 @@ import io.swagger.v3.oas.annotations.media.Schema
1112
"ownedBy": "user123",
1213
"organism": "covid",
1314
"description": "A collection of interesting variants",
14-
"variants": []
15+
"variants": [],
16+
"createdAt": "2026-01-01T00:00:00Z",
17+
"updatedAt": "2026-01-02T00:00:00Z"
1518
}
1619
""",
1720
)
@@ -22,6 +25,8 @@ data class Collection(
2225
val organism: String,
2326
val description: String?,
2427
val variants: List<Variant>,
28+
val createdAt: Instant,
29+
val updatedAt: Instant,
2530
)
2631

2732
@Schema(

backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Variant.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
77
import io.swagger.v3.oas.annotations.media.Schema
88
import org.genspectrum.dashboardsbackend.api.Variant.FilterObjectVariant
99
import org.genspectrum.dashboardsbackend.api.Variant.QueryVariant
10+
import kotlin.time.Instant
1011

1112
enum class QueryVariantType {
1213
@JsonProperty("query")
@@ -44,7 +45,9 @@ sealed interface Variant {
4445
"name": "BA.2 in USA",
4546
"description": "BA.2 lineage cases in USA",
4647
"countQuery": "country='USA' & lineage='BA.2'",
47-
"coverageQuery": "country='USA'"
48+
"coverageQuery": "country='USA'",
49+
"createdAt": "2026-01-01T00:00:00Z",
50+
"updatedAt": "2026-01-02T00:00:00Z"
4851
}
4952
""",
5053
)
@@ -55,6 +58,8 @@ sealed interface Variant {
5558
val description: String?,
5659
val countQuery: String,
5760
val coverageQuery: String? = null,
61+
val createdAt: Instant,
62+
val updatedAt: Instant,
5863
) : Variant {
5964
val type: QueryVariantType = QueryVariantType.QUERY
6065
}
@@ -70,7 +75,9 @@ sealed interface Variant {
7075
"description": "Key mutations for Omicron",
7176
"filterObject": {
7277
"aminoAcidMutations": ["S:N501Y", "S:E484K", "S:K417N"]
73-
}
78+
},
79+
"createdAt": "2026-01-01T00:00:00Z",
80+
"updatedAt": "2026-01-02T00:00:00Z"
7481
}
7582
""",
7683
)
@@ -80,6 +87,8 @@ sealed interface Variant {
8087
val name: String,
8188
val description: String?,
8289
val filterObject: FilterObject,
90+
val createdAt: Instant,
91+
val updatedAt: Instant,
8392
) : Variant {
8493
val type: FilterObjectVariantType = FilterObjectVariantType.FILTER_OBJECT
8594
}

backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/BackendSpringConfig.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import io.swagger.v3.oas.models.parameters.HeaderParameter
66
import org.flywaydb.core.Flyway
77
import org.genspectrum.dashboardsbackend.logging.REQUEST_ID_HEADER
88
import org.genspectrum.dashboardsbackend.logging.REQUEST_ID_HEADER_DESCRIPTION
9-
import org.jetbrains.exposed.spring.autoconfigure.ExposedAutoConfiguration
10-
import org.jetbrains.exposed.sql.Database
9+
import org.jetbrains.exposed.v1.jdbc.Database
10+
import org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedAutoConfiguration
1111
import org.springdoc.core.customizers.OperationCustomizer
1212
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
1313
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration

backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/DashboardsConfig.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,19 @@ data class DashboardsConfig(val organisms: Map<String, OrganismConfig>) {
1313
throw BadRequestException("Organism '$organism' is not supported")
1414
}
1515
}
16+
17+
fun validateCollectionsEnabled(organism: String) {
18+
if (!getOrganismConfig(organism).hasCollections) {
19+
throw BadRequestException("Collections are not supported for organism '$organism'")
20+
}
21+
}
1622
}
1723

18-
data class OrganismConfig(val lapis: LapisConfig, val externalNavigationLinks: List<ExternalNavigationLink>?)
24+
data class OrganismConfig(
25+
val lapis: LapisConfig,
26+
val externalNavigationLinks: List<ExternalNavigationLink>?,
27+
val hasCollections: Boolean = true,
28+
)
1929

2030
data class LapisConfig(
2131
val url: String,

0 commit comments

Comments
 (0)