Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
44ea0fb
Move backendApi to src/ top level alongside lapis and covspectrum
fhennig Mar 16, 2026
8946b0a
Add collection types and backend service methods
fhennig Mar 16, 2026
3a58527
Change collection and variant IDs from String to number
fhennig Mar 16, 2026
c0b44d9
format
fhennig Mar 17, 2026
71d5bd4
Extract shared backend proxy, add collections proxy route
fhennig Mar 16, 2026
0c5151f
Drop redundant userId args from collection service methods
fhennig Mar 17, 2026
d2ac8c0
Drop userId from subscription call sites
fhennig Mar 17, 2026
d6eba1f
add a bit of docs
fhennig Mar 17, 2026
9870dc4
Update MutationListDefinition schema to match simplified backend type
fhennig Mar 18, 2026
83164e6
fix(backend): rename MutationListDefinition to FilterObject (#1095)
fhennig Mar 23, 2026
8194462
Adapt Collection types to renamed FilterObject backend API
fhennig Mar 23, 2026
b69b448
Move backendProxy.ts from pages/api/ to backendApi/
fhennig Mar 23, 2026
3bdec17
Split collections catch-all route into explicit per-endpoint files
fhennig Mar 23, 2026
4384ab0
Add collection page stubs
fhennig Mar 16, 2026
36798ec
Add new collection form with variant editor
fhennig Mar 16, 2026
74cdd9c
Add collections overview table
fhennig Mar 16, 2026
331faa5
Add collection detail view
fhennig Mar 16, 2026
6134f0c
Extract shared CollectionForm, add edit form
fhennig Mar 16, 2026
4b3dff7
Add delete button to edit form, restrict edit button to collection owner
fhennig Mar 16, 2026
15224df
Fix linting and formatting issues
fhennig Mar 16, 2026
88470fe
Add Collections dropdown to nav, fix exclusive menu behavior
fhennig Mar 16, 2026
9941db0
Use two-column layout for collection form general fields
fhennig Mar 16, 2026
3c55bf1
Change collection and variant IDs from String to number
fhennig Mar 16, 2026
e0d409a
Fix edit button visibility by coercing GitHub user ID to string
fhennig Mar 16, 2026
3b48a5d
Polish collections overview and detail UI
fhennig Mar 16, 2026
e519ddd
format
fhennig Mar 17, 2026
f3888a4
Redesign collection form layout with 3-column grid and inline variant…
fhennig Mar 18, 2026
81ec7bc
Thread DashboardsConfig through collection form and wrap with GsApp
fhennig Mar 18, 2026
ba61098
Use GsMutationFilter for mutation list variants, fix SSR by switching…
fhennig Mar 18, 2026
b30c3b5
Redesign variant editor with thirds layout and expanding description …
fhennig Mar 18, 2026
fc2a759
Add showLabel prop to GsMutationFilter, hide label in variant editor
fhennig Mar 18, 2026
902ea5e
some more form improvements
fhennig Mar 18, 2026
53d4443
Allow anonymous browsing of collections, hide create actions for unau…
fhennig Mar 23, 2026
909df20
Change menu
fhennig Mar 23, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.genspectrum.dashboardsbackend.api

import com.fasterxml.jackson.annotation.JsonAnyGetter
import com.fasterxml.jackson.annotation.JsonAnySetter
import com.fasterxml.jackson.annotation.JsonInclude

/**
* A JSON object with mutation lists (keys: aminoAcidMutations, nucleotideMutations, ...)
* as well as arbitrary extra properties (e.g. lineage filters) as top-level fields.
* In a validation step, the extra properties are validated to make sure only valid
* fields are used.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
class FilterObject {
var aminoAcidMutations: List<String>? = null
var nucleotideMutations: List<String>? = null
var aminoAcidInsertions: List<String>? = null
var nucleotideInsertions: List<String>? = null

private val filters: MutableMap<String, String> = mutableMapOf()

@JsonAnyGetter
fun getFilters(): Map<String, String> = filters

@JsonAnySetter
fun set(key: String, value: String) {
filters[key] = value
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FilterObject) return false
return aminoAcidMutations == other.aminoAcidMutations &&
nucleotideMutations == other.nucleotideMutations &&
aminoAcidInsertions == other.aminoAcidInsertions &&
nucleotideInsertions == other.nucleotideInsertions &&
filters == other.filters
}

override fun hashCode(): Int = arrayOf(
aminoAcidMutations,
nucleotideMutations,
aminoAcidInsertions,
nucleotideInsertions,
filters,
).contentHashCode()
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ 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.FilterObjectVariant
import org.genspectrum.dashboardsbackend.api.Variant.QueryVariant

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

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

@JsonTypeInfo(
Expand All @@ -25,7 +25,7 @@ enum class MutationListVariantType {
)
@JsonSubTypes(
JsonSubTypes.Type(value = QueryVariant::class, name = "query"),
JsonSubTypes.Type(value = MutationListVariant::class, name = "mutationList"),
JsonSubTypes.Type(value = FilterObjectVariant::class, name = "filterObject"),
)
@Schema(
description = "Base interface for different variant types",
Expand Down Expand Up @@ -63,25 +63,25 @@ sealed interface Variant {
description = "A variant defined by a list of mutations",
example = """
{
"type": "mutationList",
"type": "filterObject",
"id": 1,
"collectionId": 2,
"name": "Omicron mutations",
"description": "Key mutations for Omicron",
"mutationList": {
"aaMutations": ["S:N501Y", "S:E484K", "S:K417N"]
"filterObject": {
"aminoAcidMutations": ["S:N501Y", "S:E484K", "S:K417N"]
}
}
""",
)
data class MutationListVariant @JsonCreator constructor(
data class FilterObjectVariant @JsonCreator constructor(
override val id: Long,
override val collectionId: Long,
val name: String,
val description: String?,
val mutationList: MutationListDefinition,
val filterObject: FilterObject,
) : Variant {
val type: MutationListVariantType = MutationListVariantType.MUTATION_LIST
val type: FilterObjectVariantType = FilterObjectVariantType.FILTER_OBJECT
}
}

Expand All @@ -92,7 +92,7 @@ sealed interface Variant {
)
@JsonSubTypes(
JsonSubTypes.Type(value = VariantRequest.QueryVariantRequest::class, name = "query"),
JsonSubTypes.Type(value = VariantRequest.MutationListVariantRequest::class, name = "mutationList"),
JsonSubTypes.Type(value = VariantRequest.FilterObjectVariantRequest::class, name = "filterObject"),
)
@Schema(
description = "Request to create a variant",
Expand Down Expand Up @@ -123,21 +123,21 @@ sealed interface VariantRequest {
description = "Request to create a mutation list variant",
example = """
{
"type": "mutationList",
"type": "filterObject",
"name": "Omicron mutations",
"description": "Key mutations for Omicron",
"mutationList": {
"aaMutations": ["S:N501Y", "S:E484K", "S:K417N"]
"filterObject": {
"aminoAcidMutations": ["S:N501Y", "S:E484K", "S:K417N"]
}
}
""",
)
data class MutationListVariantRequest(
data class FilterObjectVariantRequest(
val name: String,
val description: String? = null,
val mutationList: MutationListDefinition,
val filterObject: FilterObject,
) : VariantRequest {
val type: MutationListVariantType = MutationListVariantType.MUTATION_LIST
val type: FilterObjectVariantType = FilterObjectVariantType.FILTER_OBJECT
}
}

Expand All @@ -148,7 +148,7 @@ sealed interface VariantRequest {
)
@JsonSubTypes(
JsonSubTypes.Type(value = VariantUpdate.QueryVariantUpdate::class, name = "query"),
JsonSubTypes.Type(value = VariantUpdate.MutationListVariantUpdate::class, name = "mutationList"),
JsonSubTypes.Type(value = VariantUpdate.FilterObjectVariantUpdate::class, name = "filterObject"),
)
@Schema(
description = "Request to update or create a variant",
Expand Down Expand Up @@ -183,23 +183,23 @@ sealed interface VariantUpdate {
description = "Request to update or create a mutation list variant",
example = """
{
"type": "mutationList",
"type": "filterObject",
"id": 1,
"name": "Omicron mutations",
"description": "Key mutations for Omicron",
"mutationList": {
"aaMutations": ["S:N501Y", "S:E484K", "S:K417N"]
"filterObject": {
"aminoAcidMutations": ["S:N501Y", "S:E484K", "S:K417N"]
}
}
""",
)
data class MutationListVariantUpdate(
data class FilterObjectVariantUpdate(
override val id: Long? = null,
val name: String,
val description: String? = null,
val mutationList: MutationListDefinition,
val filterObject: FilterObject,
) : VariantUpdate {
val type: MutationListVariantType = MutationListVariantType.MUTATION_LIST
val type: FilterObjectVariantType = FilterObjectVariantType.FILTER_OBJECT
}

fun toVariantRequest(): VariantRequest {
Expand All @@ -212,10 +212,10 @@ sealed interface VariantUpdate {
coverageQuery = coverageQuery,
)

is MutationListVariantUpdate -> VariantRequest.MutationListVariantRequest(
is FilterObjectVariantUpdate -> VariantRequest.FilterObjectVariantRequest(
name = name,
description = description,
mutationList = mutationList,
filterObject = filterObject,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package org.genspectrum.dashboardsbackend.model.collection
import org.genspectrum.dashboardsbackend.api.Collection
import org.genspectrum.dashboardsbackend.api.CollectionRequest
import org.genspectrum.dashboardsbackend.api.CollectionUpdate
import org.genspectrum.dashboardsbackend.api.MutationListDefinition
import org.genspectrum.dashboardsbackend.api.FilterObject
import org.genspectrum.dashboardsbackend.api.VariantRequest
import org.genspectrum.dashboardsbackend.api.VariantUpdate
import org.genspectrum.dashboardsbackend.config.DashboardsConfig
Expand Down Expand Up @@ -174,18 +174,18 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
this.description = variantRequest.description
this.countQuery = variantRequest.countQuery
this.coverageQuery = variantRequest.coverageQuery
this.mutationList = null
this.filterObject = null
}
}
is VariantRequest.MutationListVariantRequest -> {
validateLineageFilters(collectionEntity.organism, variantRequest.mutationList)
is VariantRequest.FilterObjectVariantRequest -> {
validateLineageFilters(collectionEntity.organism, variantRequest.filterObject)

VariantEntity.new {
this.collectionId = collectionEntity.id
this.variantType = VariantType.MUTATION_LIST
this.variantType = VariantType.FILTER_OBJECT
this.name = variantRequest.name
this.description = variantRequest.description
this.mutationList = variantRequest.mutationList
this.filterObject = variantRequest.filterObject
this.countQuery = null
this.coverageQuery = null
}
Expand All @@ -194,12 +194,12 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) {

/**
* The list of known lineage fields is configured in the organism config.
* This function checks a MutationListDefinition against that list, and raises an error
* This function checks a FilterObject against that list, and raises an error
* if invalid lineage fields are found.
*/
private fun validateLineageFilters(organism: String, mutationList: MutationListDefinition) {
private fun validateLineageFilters(organism: String, filterObject: FilterObject) {
val validLineageFields = dashboardsConfig.getOrganismConfig(organism).lapis.lineageFields ?: emptyList()
val invalidFields = mutationList.filters.orEmpty().keys - validLineageFields.toSet()
val invalidFields = filterObject.getFilters().keys - validLineageFields.toSet()
if (invalidFields.isNotEmpty()) {
val validFieldsStr = if (validLineageFields.isEmpty()) {
"no lineage fields are configured"
Expand Down Expand Up @@ -230,19 +230,19 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
variantEntity.countQuery = variantUpdate.countQuery
variantEntity.coverageQuery = variantUpdate.coverageQuery
}
is VariantUpdate.MutationListVariantUpdate -> {
is VariantUpdate.FilterObjectVariantUpdate -> {
// Verify type matches
if (variantEntity.variantType != VariantType.MUTATION_LIST) {
if (variantEntity.variantType != VariantType.FILTER_OBJECT) {
throw BadRequestException(
"Cannot change variant type from ${variantEntity.variantType} to MUTATION_LIST",
"Cannot change variant type from ${variantEntity.variantType} to FILTER_OBJECT",
)
}

validateLineageFilters(collectionEntity.organism, variantUpdate.mutationList)
validateLineageFilters(collectionEntity.organism, variantUpdate.filterObject)

variantEntity.name = variantUpdate.name
variantEntity.description = variantUpdate.description
variantEntity.mutationList = variantUpdate.mutationList
variantEntity.filterObject = variantUpdate.filterObject
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.genspectrum.dashboardsbackend.model.collection

import org.genspectrum.dashboardsbackend.api.MutationListDefinition
import org.genspectrum.dashboardsbackend.api.FilterObject
import org.genspectrum.dashboardsbackend.api.Variant
import org.genspectrum.dashboardsbackend.model.subscription.jacksonSerializableJsonb
import org.jetbrains.exposed.dao.LongEntity
Expand All @@ -13,18 +13,18 @@ const val VARIANT_TABLE = "variants_table"

enum class VariantType {
QUERY,
MUTATION_LIST,
FILTER_OBJECT,
;

fun toDatabaseValue(): String = when (this) {
QUERY -> "query"
MUTATION_LIST -> "mutationList"
FILTER_OBJECT -> "filterObject"
}

companion object {
fun fromDatabaseValue(value: String): VariantType = when (value) {
"query" -> QUERY
"mutationList" -> MUTATION_LIST
"filterObject" -> FILTER_OBJECT
else -> throw IllegalArgumentException("Unknown variant type: $value")
}
}
Expand All @@ -43,8 +43,8 @@ object VariantTable : LongIdTable(VARIANT_TABLE) {
val countQuery = text("count_query").nullable()
val coverageQuery = text("coverage_query").nullable()

val mutationList = jacksonSerializableJsonb<MutationListDefinition>(
"mutation_list",
val filterObject = jacksonSerializableJsonb<FilterObject>(
"filter_object",
).nullable()
}

Expand All @@ -59,7 +59,7 @@ class VariantEntity(id: EntityID<Long>) : LongEntity(id) {
// Polymorphic property access
var countQuery by VariantTable.countQuery
var coverageQuery by VariantTable.coverageQuery
var mutationList by VariantTable.mutationList
var filterObject by VariantTable.filterObject

// Type-safe variant type accessor
var variantType: VariantType
Expand All @@ -73,12 +73,12 @@ class VariantEntity(id: EntityID<Long>) : LongEntity(id) {
when (variantType) {
VariantType.QUERY -> {
require(countQuery != null) { "Query variant must have count_query" }
require(mutationList == null) { "Query variant must not have mutation_list" }
require(filterObject == null) { "Query variant must not have filter_object" }
}
VariantType.MUTATION_LIST -> {
require(mutationList != null) { "MutationList variant must have mutation_list" }
VariantType.FILTER_OBJECT -> {
require(filterObject != null) { "FilterObject variant must have filter_object" }
require(countQuery == null && coverageQuery == null) {
"MutationList variant must not have query columns"
"FilterObject variant must not have query columns"
}
}
}
Expand All @@ -93,12 +93,12 @@ class VariantEntity(id: EntityID<Long>) : LongEntity(id) {
countQuery = countQuery!!,
coverageQuery = coverageQuery,
)
VariantType.MUTATION_LIST -> Variant.MutationListVariant(
VariantType.FILTER_OBJECT -> Variant.FilterObjectVariant(
id = id.value,
collectionId = collectionId.value,
name = name,
description = description,
mutationList = mutationList!!,
filterObject = filterObject!!,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ create table variants_table (
-- Query variant columns (nullable, only used when variant_type='query')
count_query text,
coverage_query text,
-- MutationList variant column (nullable, only used when variant_type='mutationList')
mutation_list jsonb,
-- FilterObject variant column (nullable, only used when variant_type='filterObject')
filter_object jsonb,
-- Constraints
constraint fk_collection foreign key (collection_id) references collections_table(id) on delete cascade,
constraint chk_variant_type check (variant_type in ('query', 'mutationList')),
constraint chk_variant_type check (variant_type in ('query', 'filterObject')),
-- Ensure correct columns are populated based on type
constraint chk_query_columns check (
(variant_type = 'query' and count_query is not null and mutation_list is null) or
(variant_type = 'mutationList' and mutation_list is not null and count_query is null and coverage_query is null)
(variant_type = 'query' and count_query is not null and filter_object is null) or
(variant_type = 'filterObject' and filter_object is not null and count_query is null and coverage_query is null)
)
);

Expand Down
Loading
Loading