diff --git a/app/models/Backend.scala b/app/models/Backend.scala index 1770f610..550f0e45 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -4,8 +4,6 @@ import clickhouse.ClickHouseProfile import net.logstash.logback.argument.StructuredArguments.keyValue import com.sksamuel.elastic4s.* import com.sksamuel.elastic4s.http.JavaClient -import com.sksamuel.elastic4s.requests.searches.* -import com.sksamuel.elastic4s.requests.searches.aggs.* import esecuele.* import javax.inject.Inject @@ -26,6 +24,7 @@ import models.entities.Interactions.* import models.entities.Loci.* import models.entities.MechanismsOfAction.* import models.entities.MousePhenotypes.* +import models.entities.NoveltyResults.* import models.entities.Pharmacogenomics.* import models.entities.SearchFacetsResults.* import models.entities.Studies.* @@ -726,6 +725,24 @@ class Backend @Inject() (implicit dbRetriever.executeQuery[MechanismsOfAction, Query](query.query) } + def getNovelty(diseaseId: String, + targetId: String, + isDirect: Boolean, + pagination: Option[Pagination] + ): Future[NoveltyResults] = { + val tableName = getTableWithPrefixOrDefault(defaultOTSettings.clickhouse.novelty.name) + val pag = pagination.getOrElse(Pagination.mkDefault).offsetLimit + logger.debug(s"querying novelty", keyValue("table", tableName)) + val query = NoveltyQuery(diseaseId, targetId, isDirect, tableName, pag._1, pag._2) + dbRetriever.executeQuery[Novelty, Query](query.query).map { noveltySeq => + if (noveltySeq.isEmpty) { + NoveltyResults(0, noveltySeq) + } else { + NoveltyResults(noveltySeq.head.meta_total, noveltySeq) + } + } + } + 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)) diff --git a/app/models/GQLSchema.scala b/app/models/GQLSchema.scala index 60c98626..a008179a 100644 --- a/app/models/GQLSchema.scala +++ b/app/models/GQLSchema.scala @@ -5,7 +5,6 @@ import entities.* import sangria.execution.deferred.* import gql.validators.QueryTermsValidator.* -import scala.concurrent.ExecutionContext.Implicits.global import models.gql.Objects.* import models.gql.Arguments.* import models.gql.Fetchers.* diff --git a/app/models/db/CredibleSetQuery.scala b/app/models/db/CredibleSetQuery.scala index b92082f6..d83a3fe0 100644 --- a/app/models/db/CredibleSetQuery.scala +++ b/app/models/db/CredibleSetQuery.scala @@ -4,7 +4,6 @@ import esecuele.Column.column import esecuele.Column.literal import esecuele._ import utils.OTLogging -import models.entities.StudyQueryArgs import models.gql.StudyTypeEnum import models.entities.CredibleSetQueryArgs diff --git a/app/models/db/InteractionSourcesQuery.scala b/app/models/db/InteractionSourcesQuery.scala index 50c28019..33bc92cc 100644 --- a/app/models/db/InteractionSourcesQuery.scala +++ b/app/models/db/InteractionSourcesQuery.scala @@ -1,10 +1,8 @@ package models.db import esecuele.Column.column -import esecuele.Column.literal import esecuele._ import utils.OTLogging -import play.libs.F import models.gql.InteractionSourceEnum case class InteractionSourcesQuery( diff --git a/app/models/db/NoveltyQuery.scala b/app/models/db/NoveltyQuery.scala new file mode 100644 index 00000000..0363ab95 --- /dev/null +++ b/app/models/db/NoveltyQuery.scala @@ -0,0 +1,43 @@ +package models.db + +import esecuele.Column.column +import esecuele.Column.literal +import esecuele.* +import utils.OTLogging + +case class NoveltyQuery(diseaseId: String, + targetId: String, + isDirect: Boolean, + tableName: String, + offset: Int, + size: Int +) extends Queryable + with OTLogging { + + private val positionalQuery = Where( + Functions.and( + Functions.equals(column("diseaseId"), literal(diseaseId)), + Functions.equals(column("targetId"), literal(targetId)), + Functions.equals(column("isDirect"), literal(isDirect)) + ) + ) + + val totals: Query = + Query( + Select(Functions.count(Column.star) :: Nil), + From(column(tableName)), + positionalQuery + ) + + override val query: Query = + Query( + Select( + Column.star :: Functions.countOver("meta_total") :: Nil + ), + From(column(tableName)), + positionalQuery, + OrderBy(column("year").asc :: Nil), + Limit(offset, size), + Format("JSONEachRow") + ) +} diff --git a/app/models/entities/Configuration.scala b/app/models/entities/Configuration.scala index 8772c750..c8781713 100644 --- a/app/models/entities/Configuration.scala +++ b/app/models/entities/Configuration.scala @@ -111,6 +111,7 @@ object Configuration { clinicalTarget: DbTableSettings, mechanismOfAction: DbTableSettings, mousePhenotypes: DbTableSettings, + novelty: DbTableSettings, otarProjects: DbTableSettings, pharmacogenomics: PharmacogenomicsSettings, proteinCodingCoordinates: ProteinCodingCoordinatesSettings, diff --git a/app/models/entities/Novelty.scala b/app/models/entities/Novelty.scala new file mode 100644 index 00000000..103cebe2 --- /dev/null +++ b/app/models/entities/Novelty.scala @@ -0,0 +1,31 @@ +package models.entities + +import play.api.libs.json.{Json, OFormat} +import slick.jdbc.GetResult +import utils.db.DbJsonParser.fromPositionedResult + +case class Novelty( + diseaseId: String, + targetId: String, + aggregationType: String, + aggregationValue: String, + year: Option[Int], + associationScore: Double, + novelty: Option[Double], + yearlyEvidenceCount: Option[Int], + isDirect: Boolean, + meta_total: Long +) + +case class NoveltyResults( + count: Long, + rows: Vector[Novelty] +) + +object NoveltyResults { + val empty: NoveltyResults = NoveltyResults(0, Vector.empty) + implicit val getNoveltyRowFromDB: GetResult[Novelty] = + GetResult(fromPositionedResult[Novelty]) + implicit val NoveltyImp: OFormat[Novelty] = Json.format[Novelty] + implicit val NoveltyResultsImp: OFormat[NoveltyResults] = Json.format[NoveltyResults] +} diff --git a/app/models/entities/Studies.scala b/app/models/entities/Studies.scala index a02d6d5c..8e4c08e4 100644 --- a/app/models/entities/Studies.scala +++ b/app/models/entities/Studies.scala @@ -55,7 +55,6 @@ case class Study( ) object Studies extends OTLogging { - import sangria.macros.derive._ def empty: Studies = Studies(0, IndexedSeq.empty) implicit val studiesFromDB: GetResult[Study] = diff --git a/app/models/gql/Arguments.scala b/app/models/gql/Arguments.scala index 4544bd9a..c54da3f7 100644 --- a/app/models/gql/Arguments.scala +++ b/app/models/gql/Arguments.scala @@ -159,8 +159,6 @@ object Arguments { Argument("studyId", OptionInputType(StringType), description = "Study ID") val studyIds: Argument[Option[Seq[String]]] = Argument("studyIds", OptionInputType(ListInputType(StringType)), description = "Study IDs") - val diseaseId: Argument[Option[String]] = - Argument("diseaseId", OptionInputType(StringType), description = "Disease ID") val diseaseIds: Argument[Option[Seq[String]]] = Argument("diseaseIds", OptionInputType(ListInputType(StringType)), description = "Disease IDs") val studyTypes = @@ -177,6 +175,12 @@ object Arguments { OptionInputType(ListInputType(StringType)), description = "Study-locus IDs" ) + val isDirect: Argument[Boolean] = Argument( + "isDirect", + BooleanType, + description = + "Whether to include only direct associations/evidence (true), or also indirect ones (false)." + ) val enableIndirect: Argument[Option[Boolean]] = Argument( "enableIndirect", OptionInputType(BooleanType), diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 751cba0f..6279af0a 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -11,7 +11,6 @@ import models.entities.ClinicalIndications.{ clinicalIndicationsFromDrugImp } import models.entities.ClinicalTargets.clinicalTargetsImp -import play.api.libs.json.* import sangria.macros.derive.* import sangria.schema.* @@ -444,6 +443,14 @@ object Objects extends OTLogging { description = Some(""), arguments = Nil, resolve = ctx => ctx.ctx.getClinicalTargetsByTarget(ctx.value.id) + ), + Field( + "novelty", + noveltyResultsImp, + description = Some("Novelty"), + arguments = efoId :: isDirect :: pageArg :: Nil, + resolve = ctx => + ctx.ctx.getNovelty(ctx.arg(efoId), ctx.value.id, ctx.arg(isDirect), ctx.arg(pageArg)) ) ) ) @@ -684,6 +691,14 @@ object Objects extends OTLogging { ), arguments = Nil, resolve = ctx => ctx.ctx.getClinicalIndicationsByDisease(ctx.value.id) + ), + Field( + "novelty", + noveltyResultsImp, + description = Some("Novelty"), + arguments = ensemblId :: isDirect :: pageArg :: Nil, + resolve = ctx => + ctx.ctx.getNovelty(ctx.value.id, ctx.arg(ensemblId), ctx.arg(isDirect), ctx.arg(pageArg)) ) ) ) @@ -789,6 +804,60 @@ object Objects extends OTLogging { "List of credible set entries with their associated statistics and fine-mapping information" ) ) + implicit val noveltyResultsImp: ObjectType[Backend, NoveltyResults] = + deriveObjectType[Backend, NoveltyResults]( + ObjectTypeDescription( + "Novelty of the association between a target and a disease. Calculated based on the accumulation of evidence over time, providing insights into how novel or well-established a target-disease association is." + ), + DocumentField( + "count", + "Total number of novelty results matching the query filters" + ), + DocumentField( + "rows", + "List of novelty entries with their associated novelty scores and temporal information" + ) + ) + + implicit val noveltyImp: ObjectType[Backend, Novelty] = deriveObjectType[Backend, Novelty]( + ObjectTypeDescription( + "Novelty of the association between a target and a disease. Calculated based on the accumulation of evidence over time, providing insights into how novel or well-established a target-disease association is." + ), + DocumentField("diseaseId", "EFO ID of the disease"), + DocumentField( + "targetId", + "Ensembl ID of the target gene" + ), + DocumentField( + "aggregationType", + "Type of aggregation used for novelty calculation" + ), + DocumentField( + "aggregationValue", + "Value used for novelty aggregation" + ), + DocumentField( + "year", + "Year of the evidence item used for novelty calculation" + ), + DocumentField( + "associationScore", + "Association score between the target and disease" + ), + DocumentField( + "novelty", + "Novelty score indicating how novel the target-disease association is." + ), + DocumentField( + "yearlyEvidenceCount", + "Yearly count of evidence items" + ), + DocumentField( + "isDirect", + "Flag indicating whether the novelty calculation is based on direct evidence only or includes indirect evidence" + ), + ExcludeFields("meta_total") + ) implicit val tissueImp: ObjectType[Backend, Tissue] = deriveObjectType[Backend, Tissue]( ObjectTypeDescription( diff --git a/conf/application.conf b/conf/application.conf index e7001fab..28a95a1c 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -112,6 +112,10 @@ ot { label = "Mouse phenotypes table" name = "mouse_phenotypes" } + novelty { + label = "Novelty table" + name = "novelty" + } otarProjects { label = "OTAR projects table" name = "otar_projects"