Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8206af0
Remove linkedTargets and linkedDiseases from Drug endpoint
d0choa Jan 20, 2026
a457d45
Remove yearOfFirstApproval from Drug endpoint
d0choa Jan 20, 2026
f5d3e0d
Remove knownDrugs endpoint from Target, Disease, and Drug types
d0choa Jan 20, 2026
ae42e0d
add clinical indication to the api
remo87 Feb 3, 2026
e30eec7
remove indications add list wrapper
remo87 Feb 10, 2026
012929f
format files
remo87 Feb 10, 2026
d63c722
replace drugId and diseaseId
remo87 Feb 10, 2026
d80ca1d
rename disease field
remo87 Feb 10, 2026
6892ef3
split clinical indication graphql definition
remo87 Feb 11, 2026
64d8cab
add clinical reports query
remo87 Feb 13, 2026
c9192ff
resolve clinical report in clin indications
remo87 Feb 16, 2026
eadaf5a
add clinical target
remo87 Feb 17, 2026
1c83f0d
feat: rename chembl fields for clinical_precedence evidence
d0choa Feb 17, 2026
0b55a0e
update clinical schemas
remo87 Feb 18, 2026
5bbe84d
feat: replace maximumClinicalTrialPhase with maximumClinicalStage in …
d0choa Feb 20, 2026
b1ae678
handle empty/null id disease and drug and escape
remo87 Feb 25, 2026
003dcda
fix config issue after rebase
remo87 Mar 5, 2026
0c4b19f
update schemas to match changes in data
remo87 Mar 5, 2026
b78dde7
add method to handle escaping when reading from ch as json
remo87 Mar 5, 2026
88b878f
fix isMeasurement ref
jdhayhurst Mar 6, 2026
e68548a
target schema change
jdhayhurst Mar 6, 2026
897bea9
fix one to many sort
jdhayhurst Mar 10, 2026
b8c9ebb
add null validation
remo87 Mar 13, 2026
f150ddf
fix default weights
jdhayhurst Mar 17, 2026
0236067
go schema change
jdhayhurst Mar 17, 2026
5b0383d
fix doc
jdhayhurst Mar 17, 2026
3ab5713
pattern match for none
jdhayhurst Mar 18, 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
124 changes: 66 additions & 58 deletions app/models/Backend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -402,53 +402,6 @@ class Backend @Inject() (implicit
dbRetriever.executeQuery[TargetPrioritisation, Query](query.query)
}

def getKnownDrugs(
queryString: String,
kv: Map[String, String],
sizeLimit: Option[Int],
cursor: Option[String]
): Future[Option[KnownDrugs]] = {

val pag = Pagination(0, sizeLimit.getOrElse(Pagination.sizeDefault))
val sortByField = sort.FieldSort(field = "phase").desc()
val cbIndex = getIndexOrDefault("known_drugs")

val mappedValues =
Seq(keyValue("index", cbIndex)) ++ kv.map(pair => keyValue(pair._1, pair._2)).toSeq

logger.debug(s"querying known drugs", mappedValues*)

val aggs = Seq(
cardinalityAgg("uniqueTargets", "targetId.raw"),
cardinalityAgg("uniqueDiseases", "diseaseId.raw"),
cardinalityAgg("uniqueDrugs", "drugId.raw"),
valueCountAgg("rowsCount", "drugId.raw")
)

esRetriever
.getByFreeQuery(
cbIndex,
queryString,
kv,
pag,
fromJsValue[KnownDrug],
aggs,
Some(sortByField),
Seq("ancestors", "descendants"),
cursor
)
.map {
case (Seq(), _, _) => Some(KnownDrugs(0, 0, 0, 0, cursor, Seq()))
case (seq, agg, nextCursor) =>
logger.trace(Json.prettyPrint(agg))
val drugs = (agg \ "uniqueDrugs" \ "value").as[Long]
val diseases = (agg \ "uniqueDiseases" \ "value").as[Long]
val targets = (agg \ "uniqueTargets" \ "value").as[Long]
val rowsCount = (agg \ "rowsCount" \ "value").as[Long]
Some(KnownDrugs(drugs, diseases, targets, rowsCount, nextCursor, seq))
}
}

def getEvidencesByVariantId(
datasourceIds: Option[Seq[String]],
variantId: String,
Expand Down Expand Up @@ -552,6 +505,71 @@ class Backend @Inject() (implicit
dbRetriever.executeQuery[MousePhenotypes, Query](query.query)
}

def getClinicalTargetsByTarget(id: String): Future[ClinicalTargets] =
val tableName = getTableWithPrefixOrDefault(defaultOTSettings.clickhouse.clinicalTarget.name)

val clinicalTargetQuery = AllByIdInColumnQuery(id, tableName, 0, Pagination.sizeMax, "targetId")

logger.info(s"getting clinical target with id $id", keyValue("table", tableName))

dbRetriever
.executeQuery[ClinicalTarget, Query](clinicalTargetQuery.query)
.map {
case Seq() =>
logger.info(s"no clinical target found for $id", keyValue("table", tableName))
ClinicalTargets(0, IndexedSeq())
case ct =>
logger.info(s"clinical target found for $id ${ct.length}", keyValue("table", tableName))
ClinicalTargets(ct.length, ct)
}

def getClinicalIndicationsByDrug(id: String): Future[ClinicalIndications] =
val tableName = getTableWithPrefixOrDefault(
defaultOTSettings.clickhouse.clinicalIndication.drugTable.name
)

logger.info(s"getting clinical indications by the drug $id", keyValue("table", tableName))
getClinicalIndications(id, tableName, "drugId")

def getClinicalIndicationsByDisease(id: String): Future[ClinicalIndications] =
val tableName = getTableWithPrefixOrDefault(
defaultOTSettings.clickhouse.clinicalIndication.diseaseTable.name
)

logger.info(s"getting clinical indications by the disease $id", keyValue("table", tableName))
getClinicalIndications(id, tableName, "diseaseId")

def getClinicalReports(ids: Seq[String]): Future[IndexedSeq[ClinicalReport]] = {
val tableName = getTableWithPrefixOrDefault(defaultOTSettings.clickhouse.clinicalReport.name)

val clinicalReportQuery = ClinicalReportQuery(ids, tableName, 0, Pagination.sizeMax)

logger.info(s"getting clinical reports with ids $ids", keyValue("table", tableName))

dbRetriever
.executeQuery[ClinicalReport, Query](clinicalReportQuery.query)
}

private def getClinicalIndications(id: String,
tableName: String,
columnName: String
): Future[ClinicalIndications] = {

val clinicalIndicationsQuery =
AllByIdInColumnQuery(id, tableName, 0, Pagination.sizeMax, columnName)

dbRetriever
.executeQuery[ClinicalIndication, Query](clinicalIndicationsQuery.query)
.map {
case Seq() =>
logger.info(s"no clinical indication found for $id in table $tableName")
ClinicalIndications(0, IndexedSeq())
case cis =>
logger.info(s"clinical indications found for $id in table $tableName: ${cis.length}")
ClinicalIndications(cis.length, cis)
}
}

def getPharmacogenomicsByDrug(ids: Seq[String]): Future[IndexedSeq[PharmacogenomicsByDrug]] = {
val tableName = getTableWithPrefixOrDefault(
defaultOTSettings.clickhouse.pharmacogenomics.drug.name
Expand Down Expand Up @@ -676,16 +694,6 @@ class Backend @Inject() (implicit
dbRetriever.executeQuery[MechanismsOfAction, Query](query.query)
}

def getIndications(ids: Seq[String]): Future[IndexedSeq[Indications]] = {
val index = getIndexOrDefault("drugIndications")
logger.debug(s"querying indications", keyValue("ids", ids), keyValue("index", index))
val queryTerm = Map("id.keyword" -> ids)

esRetriever
.getByIndexedQueryShould(index, queryTerm, Pagination.mkDefault, fromJsValue[Indications])
.map(_.mappedHits)
}

def getDrugWarnings(ids: Seq[String]): Future[IndexedSeq[DrugWarnings]] = {
val tableName = getTableWithPrefixOrDefault(defaultOTSettings.clickhouse.drugWarnings.name)
logger.debug(s"querying drug warnings", keyValue("ids", ids), keyValue("table", tableName))
Expand Down Expand Up @@ -927,7 +935,7 @@ class Backend @Inject() (implicit
} else Set.empty[String]

val columnFilters = if (includeMeasurements == false) {
Seq(("is_measurement", false))
Seq(("isMeasurement", false))
} else {
Seq.empty
}
Expand Down
2 changes: 1 addition & 1 deletion app/models/ClickhouseRetriever.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class ClickhouseRetriever(config: OTSettings)(implicit
case Failure(ex) =>
lazy val qStr = qq.statements.mkString("\n")
logger.error(s"executeQuery an exception was thrown ${ex.getMessage} with Query $qStr")
Vector.empty
Vector.empty // TODO: maybe we should return the error instead of an empty vector, to inform the user
}
}
}
15 changes: 15 additions & 0 deletions app/models/GQLSchema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,21 @@ object GQLSchema {
)
ctx.ctx.getCredibleSets(credSetQueryArgs, ctx.arg(pageArg))
}
),
Field(
"clinicalReport",
OptionType(clinicalReportImp),
description = Some(""),
arguments = clinicalReportId :: Nil,
resolve = ctx => clinicalReportFetcher.deferOpt(ctx.arg(clinicalReportId))
),
Field(
"clinicalReports",
ListType(clinicalReportImp),
description = Some(""),
arguments = clinicalReportIds :: Nil,
complexity = Some(complexityCalculator(clinicalReportIds)),
resolve = ctx => clinicalReportFetcher.deferSeqOpt(ctx.arg(clinicalReportIds))
)
)
)
Expand Down
29 changes: 29 additions & 0 deletions app/models/db/AllByIdInColumnQuery.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package models.db

import esecuele.Column.{column, literal}
import esecuele.{Column, Format, From, Functions, Limit, OrderBy, PreWhere, Query, Select, Settings}
import utils.OTLogging

case class AllByIdInColumnQuery(id: String,
tableName: String,
offset: Int,
size: Int,
columnName: String
) extends Queryable
with OTLogging {

override val query: Query =
Query(
Select(
Column.star :: Nil
),
From(column(tableName)),
PreWhere(
Functions.in(column(columnName), literal(id))
),
OrderBy(column("id") :: Nil),
Limit(offset, size),
Format("JSONEachRow"),
Settings(Map("output_format_json_escape_forward_slashes" -> "0"))
)
}
39 changes: 39 additions & 0 deletions app/models/db/ClinicalReportQuery.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package models.db

import esecuele.Column.{column, literal}
import esecuele.{
Column,
Format,
From,
Functions,
Limit,
OrderBy,
PreWhere,
Query,
Select,
Settings,
Where
}
import utils.OTLogging

case class ClinicalReportQuery(ids: Seq[String], tableName: String, offset: Int, size: Int)
extends Queryable
with OTLogging {

private val conditional = Where(
Functions.in(column("id"), Functions.set(ids.map(literal).toSeq))
)

override val query: Query =
Query(
Select(
Column.star :: Nil
),
From(column(tableName)),
conditional,
OrderBy(column("id") :: Nil),
Limit(offset, size),
Format("JSONEachRow"),
Settings(Map("output_format_json_escape_forward_slashes" -> "0"))
)
}
6 changes: 3 additions & 3 deletions app/models/db/OneToManyQuery.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import models.gql.{StudyTypeEnum, InteractionSourceEnum}
enum sortDirection:
case ASC, DESC

case class OrderBy(field: String, direction: sortDirection)
case class OrderBy(lambda: String, direction: sortDirection)

case class OneToMany(ids: Seq[String],
idField: String,
Expand All @@ -31,7 +31,7 @@ case class OneToMany(ids: Seq[String],
case Some(order) =>
order.direction match {
case sortDirection.ASC => Functions.arraySort(filteredArray)
case sortDirection.DESC => Functions.reverse(Functions.arraySort(filteredArray))
case sortDirection.DESC => Functions.arrayReverseSort(Some(order.lambda), filteredArray)
}
case None =>
filteredArray
Expand Down Expand Up @@ -127,7 +127,7 @@ object OneToMany {
offset,
size,
filter,
sortBy = Some(OrderBy("i.scoring", sortDirection.DESC))
sortBy = Some(OrderBy("i -> i.scoring", sortDirection.DESC))
)

def l2gQuery(studyLocusIds: Seq[String], tableName: String, offset: Int, size: Int): OneToMany =
Expand Down
3 changes: 2 additions & 1 deletion app/models/entities/AdverseEvent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models.entities
import play.api.libs.json._
import slick.jdbc.GetResult
import models.gql.TypeWithId
import utils.db.DbJsonParser.fromPositionedResult

case class AdverseEvent(
name: String,
Expand All @@ -19,7 +20,7 @@ case class AdverseEvents(count: Long,

object AdverseEvent {
implicit val getRowFromDB: GetResult[AdverseEvents] =
GetResult(r => Json.parse(r.<<[String]).as[AdverseEvents])
GetResult(fromPositionedResult[AdverseEvents])
implicit val adverseEventF: OFormat[AdverseEvent] = Json.format[AdverseEvent]
implicit val adverseEventsF: OFormat[AdverseEvents] = Json.format[AdverseEvents]
}
3 changes: 2 additions & 1 deletion app/models/entities/Biosample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models.entities
import play.api.libs.json.*
import utils.OTLogging
import slick.jdbc.GetResult
import utils.db.DbJsonParser.fromPositionedResult

case class Biosample(
biosampleId: String,
Expand All @@ -18,7 +19,7 @@ case class Biosample(

object Biosample extends OTLogging {
implicit val getResultBiosample: GetResult[Biosample] =
GetResult(r => Json.parse(r.<<[String]).as[Biosample])
GetResult(fromPositionedResult[Biosample])

implicit val biosampleF: OFormat[Biosample] = Json.format[Biosample]
}
3 changes: 3 additions & 0 deletions app/models/entities/ClinicalDiseaseListItem.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package models.entities

case class ClinicalDiseaseListItem(diseaseFromSource: Option[String], diseaseId: Option[String])
22 changes: 22 additions & 0 deletions app/models/entities/ClinicalIndication.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package models.entities

import play.api.libs.json.{Json, OFormat}
import slick.jdbc.GetResult
import utils.OTLogging
import utils.db.DbJsonParser.fromPositionedResult

case class ClinicalIndication(
id: String,
drugId: Option[String],
diseaseId: Option[String],
maxClinicalStage: String,
clinicalReportIds: Seq[String]
)

object ClinicalIndication extends OTLogging {

implicit val getClinicalIndicationsFromDB: GetResult[ClinicalIndication] =
GetResult(fromPositionedResult[ClinicalIndication])

implicit val clinicalIndicationsF: OFormat[ClinicalIndication] = Json.format[ClinicalIndication]
}
48 changes: 48 additions & 0 deletions app/models/entities/ClinicalIndications.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package models.entities

import models.Backend
import models.gql.Objects.{clinicalIndicationFromDiseaseImp, clinicalIndicationFromDrugImp}
import sangria.schema.{Field, ListType, LongType, ObjectType, fields}

case class ClinicalIndications(
count: Long,
rows: IndexedSeq[ClinicalIndication]
)

object ClinicalIndications {
def empty: ClinicalIndications = ClinicalIndications(0, IndexedSeq.empty)
val clinicalIndicationsFromDrugImp: ObjectType[Backend, ClinicalIndications] = ObjectType(
"clinicalIndicationsFromDrugImp",
"",
fields[Backend, ClinicalIndications](
Field("count",
LongType,
description = Some("Total number of indications results matching the query filters"),
resolve = _.value.count
),
Field(
"rows",
ListType(clinicalIndicationFromDrugImp),
description = Some("List of clinical indications results between drug-disease pairs"),
resolve = _.value.rows
)
)
)
val clinicalIndicationsFromDiseaseImp: ObjectType[Backend, ClinicalIndications] = ObjectType(
"clinicalIndicationsFromDiseaseImp",
"",
fields[Backend, ClinicalIndications](
Field("count",
LongType,
description = Some("Total number of indications results matching the query filters"),
resolve = _.value.count
),
Field(
"rows",
ListType(clinicalIndicationFromDiseaseImp),
description = Some("List of clinical indications results between drug-disease pairs"),
resolve = _.value.rows
)
)
)
}
Loading
Loading