From 239777dbcf837eab3da518f9a9ded6d8b2fedc7f Mon Sep 17 00:00:00 2001 From: David Ochoa Date: Tue, 20 Jan 2026 11:33:13 +0000 Subject: [PATCH 01/16] Remove linkedTargets and linkedDiseases from Drug endpoint --- app/models/entities/Drug.scala | 5 --- app/models/gql/Objects.scala | 56 ---------------------------------- 2 files changed, 61 deletions(-) diff --git a/app/models/entities/Drug.scala b/app/models/entities/Drug.scala index 89a9a137..893cf7fb 100644 --- a/app/models/entities/Drug.scala +++ b/app/models/entities/Drug.scala @@ -37,8 +37,6 @@ case class IndicationRow( references: Option[Seq[IndicationReference]] ) -case class LinkedIds(count: Int, rows: Seq[String]) - case class Indications( id: String, indications: Seq[IndicationRow], @@ -77,14 +75,11 @@ case class Drug( parentId: Option[String], maximumClinicalTrialPhase: Option[Double], hasBeenWithdrawn: Boolean, - linkedDiseases: Option[LinkedIds], - linkedTargets: Option[LinkedIds], blackBoxWarning: Boolean, description: Option[String] ) object Drug { - implicit val linkedIdsImpW: OFormat[LinkedIds] = Json.format[models.entities.LinkedIds] // implicit val drugWarningRefenceImpW = Json.format[models.entities.DrugWarningReference] implicit val drugWarningsReferenceImpR: Reads[models.entities.DrugWarningReference] = ( (JsPath \ "ref_id").read[String] and diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index f745b054..452315af 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1238,39 +1238,6 @@ object Objects extends OTLogging { DocumentField("rows", "List of phenotype annotations for the disease") ) - // howto doc https://sangria-graphql.org/learn/#macro-based-graphql-type-derivation - implicit lazy val linkedDiseasesImp: ObjectType[Backend, LinkedIds] = - deriveObjectType[Backend, LinkedIds]( - ObjectTypeName("LinkedDiseases"), - ObjectTypeDescription("Diseases linked via indications"), - DocumentField("count", "Total number of linked diseases"), - ReplaceField( - "rows", - Field( - "rows", - ListType(diseaseImp), - Some("List of linked disease entities"), - resolve = r => diseasesFetcher.deferSeqOpt(r.value.rows) - ) - ) - ) - - implicit lazy val linkedTargetsImp: ObjectType[Backend, LinkedIds] = - deriveObjectType[Backend, LinkedIds]( - ObjectTypeName("LinkedTargets"), - ObjectTypeDescription("Targets linked via curated mechanisms of action"), - DocumentField("count", "Total number of linked targets"), - ReplaceField( - "rows", - Field( - "rows", - ListType(targetImp), - Some("List of linked target entities"), - resolve = r => targetsFetcher.deferSeqOpt(r.value.rows) - ) - ) - ) - implicit lazy val drugReferenceImp: ObjectType[Backend, Reference] = deriveObjectType[Backend, Reference]( ObjectTypeDescription("Reference information supporting the drug mechanisms of action"), @@ -1720,29 +1687,6 @@ object Objects extends OTLogging { complexity = Some(complexityCalculator(pageArg)), resolve = ctx => ctx.ctx.getPharmacogenomicsByDrug(ctx.value.id) ) - ), - ReplaceField( - "linkedDiseases", - Field( - "linkedDiseases", - OptionType(linkedDiseasesImp), - description = Some("List of molecule potential indications"), - resolve = r => r.value.linkedDiseases - ) - ), - ReplaceField( - "linkedTargets", - Field( - "linkedTargets", - OptionType(linkedTargetsImp), - description = Some("List of molecule targets based on molecule mechanism of action"), - resolve = ctx => { - val moa: Future[MechanismsOfAction] = ctx.ctx.getMechanismsOfAction(ctx.value.id) - val targets: Future[Seq[String]] = - moa.map(m => m.rows.flatMap(r => r.targets.getOrElse(Seq.empty))) - targets.map(t => LinkedIds(t.size, t)) - } - ) ) ) From b3114cccdef021cfad573300808e9b2c93672a04 Mon Sep 17 00:00:00 2001 From: David Ochoa Date: Tue, 20 Jan 2026 11:41:36 +0000 Subject: [PATCH 02/16] Remove yearOfFirstApproval from Drug endpoint --- app/models/entities/Drug.scala | 1 - app/models/gql/Objects.scala | 1 - 2 files changed, 2 deletions(-) diff --git a/app/models/entities/Drug.scala b/app/models/entities/Drug.scala index 893cf7fb..e8705c9c 100644 --- a/app/models/entities/Drug.scala +++ b/app/models/entities/Drug.scala @@ -68,7 +68,6 @@ case class Drug( synonyms: Seq[String], tradeNames: Seq[String], childChemblIds: Option[Seq[String]], // Gone? - yearOfFirstApproval: Option[Int], drugType: String, isApproved: Option[Boolean], crossReferences: Option[Seq[DrugReferences]], diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 452315af..f543bd92 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1541,7 +1541,6 @@ object Objects extends OTLogging { DocumentField("name", "Generic name of the drug molecule"), DocumentField("synonyms", "List of alternative names for the drug"), DocumentField("tradeNames", "List of brand names for the drug"), - DocumentField("yearOfFirstApproval", "Year when the drug received regulatory approval"), DocumentField( "drugType", "Classification of the molecule's therapeutic category or chemical class (e.g. Antibody)" From f8c34d212b8da8413ff4f96d11e6b905901495f4 Mon Sep 17 00:00:00 2001 From: David Ochoa Date: Tue, 20 Jan 2026 11:54:14 +0000 Subject: [PATCH 03/16] Remove knownDrugs endpoint from Target, Disease, and Drug types --- app/models/Backend.scala | 47 ------ app/models/entities/KnownDrug.scala | 75 ---------- app/models/gql/Objects.scala | 134 ------------------ queries.graphql | 59 -------- test/controllers/GqlTest.scala | 14 +- test/inputs/GqlCase.scala | 19 --- .../gqlQueries/KnownDrugs_KnownDrugsQuery.gql | 40 ------ .../KnownDrugs_KnownDrugsSummary.gql | 7 - .../KnownDrugs_KnownDrugsSummaryFragment.gql | 7 - 9 files changed, 1 insertion(+), 401 deletions(-) delete mode 100644 app/models/entities/KnownDrug.scala delete mode 100644 test/resources/gqlQueries/KnownDrugs_KnownDrugsQuery.gql delete mode 100644 test/resources/gqlQueries/KnownDrugs_KnownDrugsSummary.gql delete mode 100644 test/resources/gqlQueries/KnownDrugs_KnownDrugsSummaryFragment.gql diff --git a/app/models/Backend.scala b/app/models/Backend.scala index 3ff537ac..d16a90c5 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -640,53 +640,6 @@ class Backend @Inject() (implicit prioritisationFt.flatMap(identity) } - 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, diff --git a/app/models/entities/KnownDrug.scala b/app/models/entities/KnownDrug.scala deleted file mode 100644 index d6efd2e0..00000000 --- a/app/models/entities/KnownDrug.scala +++ /dev/null @@ -1,75 +0,0 @@ -package models.entities - -import play.api.libs.json.* -import play.api.libs.json.Reads.* -import play.api.libs.functional.syntax.* -import utils.OTLogging - -import scala.util.matching.Regex - -case class URL(url: String, name: String) -case class KnownDrugReference(source: String, ids: Seq[String], urls: Seq[String]) - -case class KnownDrug( - approvedSymbol: String, - approvedName: String, - label: String, - prefName: String, - drugType: String, - targetId: String, - diseaseId: String, - drugId: String, - phase: Double, - mechanismOfAction: String, - status: Option[String], - targetClass: Seq[String], - references: Seq[KnownDrugReference], - ctIds: Seq[String], - urls: Seq[URL] -) - -case class KnownDrugs( - uniqueDrugs: Long, - uniqueDiseases: Long, - uniqueTargets: Long, - count: Long, - cursor: Option[String], - rows: Seq[KnownDrug] -) - -object KnownDrug extends OTLogging { - - val ctPattern: Regex = "NCT(\\d{8})".r - - implicit val KnownDrugReferenceImpJSONF: OFormat[KnownDrugReference] = - Json.format[KnownDrugReference] - implicit val URLImpW: OWrites[URL] = Json.writes[URL] - implicit val URLImpR: Reads[URL] = ( - (__ \ "url").read[String] and - (__ \ "niceName").read[String] - )(URL.apply) - - // approvedSymbol: String, label: String, prefName: String, - implicit val knownDrugImpW: OWrites[KnownDrug] = Json.writes[KnownDrug] - implicit val knownDrugImpR: Reads[KnownDrug] = ( - (__ \ "approvedSymbol").read[String] and - (__ \ "approvedName").read[String] and - (__ \ "label").read[String] and - (__ \ "prefName").read[String] and - (__ \ "drugType").read[String] and - (__ \ "targetId").read[String] and - (__ \ "diseaseId").read[String] and - (__ \ "drugId").read[String] and - (__ \ "phase").read[Double] and - (__ \ "mechanismOfAction").read[String] and - (__ \ "status").readNullable[String] and - (__ \ "targetClass").readWithDefault[Seq[String]](Seq.empty) and - (__ \ "references").readWithDefault[Seq[KnownDrugReference]](Seq.empty) and - (__ \ "urls") - .readWithDefault[Seq[Map[String, String]]](Seq.empty) - .map(s => s.flatMap(m => ctPattern findAllIn m("url"))) and - (__ \ "urls").read[Seq[URL]] - )(KnownDrug.apply) - - implicit val knownDrugsImpF: OFormat[KnownDrugs] = Json.format[KnownDrugs] -} diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index f543bd92..18ba4744 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -312,23 +312,6 @@ object Objects extends OTLogging { case None => Seq.empty } ), - Field( - "knownDrugs", - OptionType(knownDrugsImp), - description = Some( - "Set of clinical precedence for drugs with investigational or approved indications " + - "targeting this gene product according to their curated mechanism of action" - ), - arguments = freeTextQuery :: pageSize :: cursor :: Nil, - complexity = Some(complexityCalculator(pageSize)), - resolve = ctx => - ctx.ctx.getKnownDrugs( - ctx.arg(freeTextQuery).getOrElse(""), - Map("targetId.raw" -> ctx.value.id), - ctx.arg(pageSize), - ctx.arg(cursor) - ) - ), Field( "associatedDiseases", associatedOTFDiseasesImp, @@ -615,25 +598,6 @@ object Objects extends OTLogging { case None => Seq.empty } ), - Field( - "knownDrugs", - OptionType(knownDrugsImp), - description = Some( - "Investigational or approved drugs indicated for this disease with curated mechanisms of action" - ), - arguments = freeTextQuery :: pageSize :: cursor :: Nil, - complexity = Some(complexityCalculator(pageSize)), - resolve = ctx => - ctx.ctx.getKnownDrugs( - ctx.arg(freeTextQuery).getOrElse(""), - Map( - "diseaseId.raw" -> ctx.value.id, - "ancestors.raw" -> ctx.value.id - ), - ctx.arg(pageSize), - ctx.arg(cursor) - ) - ), Field( "associatedTargets", associatedOTFTargetsImp, @@ -1649,23 +1613,6 @@ object Objects extends OTLogging { ), resolve = ctx => DeferredValue(indicationFetcher.deferOpt(ctx.value.id)) ), - Field( - "knownDrugs", - OptionType(knownDrugsImp), - description = Some( - "Curated Clinical trial records and and post-marketing package inserts " + - "with a known mechanism of action" - ), - arguments = freeTextQuery :: pageSize :: cursor :: Nil, - complexity = Some(complexityCalculator(pageSize)), - resolve = ctx => - ctx.ctx.getKnownDrugs( - ctx.arg(freeTextQuery).getOrElse(""), - Map("drugId.raw" -> ctx.value.id), - ctx.arg(pageSize), - ctx.arg(cursor) - ) - ), Field( "adverseEvents", OptionType(adverseEventsImp), @@ -1813,87 +1760,6 @@ object Objects extends OTLogging { DocumentField("id", "Gene ontology term identifier [bioregistry:go]"), DocumentField("name", "Gene ontology term name") ) - implicit val knownDrugReferenceImp: ObjectType[Backend, KnownDrugReference] = - deriveObjectType[Backend, KnownDrugReference]( - ObjectTypeDescription("Reference information for known drug indications"), - DocumentField("source", "Source of the reference (e.g., PubMed, FDA, package inserts)"), - DocumentField("ids", "List of reference identifiers"), - DocumentField("urls", "List of URLs linking to the reference") - ) - - implicit val URLImp: ObjectType[Backend, URL] = deriveObjectType[Backend, URL]( - ObjectTypeDescription("Source URL for clinical trials, FDA and package inserts"), - DocumentField("url", "List of web addresses that support the drug/indication pair"), - DocumentField("name", "List of human readable names for the reference source") - ) - - implicit val knownDrugImp: ObjectType[Backend, KnownDrug] = deriveObjectType[Backend, KnownDrug]( - ObjectTypeDescription( - "For any approved or clinical candidate drug, includes information on the target gene product and indication. It is derived from the ChEMBL target/disease evidence." - ), - DocumentField( - "approvedSymbol", - "Approved gene symbol of the target modulated by the drug" - ), - DocumentField("approvedName", - "Approved full name of the gene or gene product modulated by the drug" - ), - DocumentField("label", "Disease label for the condition being treated"), - DocumentField("prefName", "Commonly used name for the drug"), - DocumentField("drugType", "Classification of the modality of the drug (e.g. Small molecule)"), - DocumentField("targetId", "Open Targets target identifier"), - DocumentField("diseaseId", "Open Targets disease identifier"), - DocumentField("drugId", "Open Targets molecule identifier"), - DocumentField( - "phase", - "Clinical development stage of the drug. [Values: -1: `Unknown`, 0: `Phase 0`, 0.5: `Phase I (Early)`, 1: `Phase I`, 2: `Phase II`, 3: `Phase III`, 4: `Phase IV`]" - ), - DocumentField("mechanismOfAction", "Drug pharmacological action"), - DocumentField("status", "Clinical trial status for the drug/indication pair"), - DocumentField("targetClass", - "Classification category of the drug's biological target (e.g. Enzyme)" - ), - DocumentField("references", "Source urls for FDA or package inserts"), - DocumentField("ctIds", "Clinicaltrials.gov identifiers on entry trials"), - DocumentField("urls", "List of web addresses that support the drug/indication pair"), - AddFields( - Field( - "disease", - OptionType(diseaseImp), - description = Some("Curated disease indication entity"), - resolve = r => diseasesFetcher.deferOpt(r.value.diseaseId) - ), - Field( - "target", - OptionType(targetImp), - description = Some("Drug target entity based on curated mechanism of action"), - resolve = r => targetsFetcher.deferOpt(r.value.targetId) - ), - Field( - "drug", - OptionType(drugImp), - description = Some("Curated drug entity"), - resolve = r => drugsFetcher.deferOpt(r.value.drugId) - ) - ) - ) - - implicit val knownDrugsImp: ObjectType[Backend, KnownDrugs] = - deriveObjectType[Backend, KnownDrugs]( - ObjectTypeDescription( - "Set of clinical precedence for drugs with investigational or " + - "approved indications targeting gene products according to their curated mechanism of action" - ), - DocumentField("uniqueDrugs", "Total unique drug or clinical candidate molecules"), - DocumentField("uniqueDiseases", "Total unique diseases or phenotypes"), - DocumentField( - "uniqueTargets", - "Total unique known mechanism of action targets" - ), - DocumentField("cursor", "Opaque pagination cursor to request the next page of results"), - DocumentField("count", "Total number of entries"), - DocumentField("rows", "Clinical precedence entries with known mechanism of action") - ) lazy val mUnionType: UnionType[Backend] = UnionType( diff --git a/queries.graphql b/queries.graphql index 75b1e097..aad6c6b0 100644 --- a/queries.graphql +++ b/queries.graphql @@ -34,13 +34,6 @@ query test { } } } - knownDrugs { - rows { - drugType - targetId - diseaseId - } - } } search(queryString: "erectile") { total @@ -168,12 +161,6 @@ query test { topCategory } } - knownDrugs { - uniqueDrugs - uniqueDiseases - uniqueTargets - count - } cancerBiomarkers { uniqueDrugs uniqueDiseases @@ -205,16 +192,6 @@ query test { name disease } - knownDrugs { - uniqueDrugs - uniqueDiseases - uniqueTargets - count - rows { - drugType - targetId - } - } relatedDiseases { maxCountAOrB count @@ -225,7 +202,6 @@ query test { name synonyms tradeNames - yearOfFirstApproval drugType maximumClinicalTrialPhase hasBeenWithdrawn @@ -277,40 +253,5 @@ query test { criticalValue } } - knownDrugs { - uniqueDrugs - uniqueDiseases - uniqueTargets - count - rows { - drugType - targetId - diseaseId - phase - mechanismOfAction - status - activity - targetClass - ctIds - } - } } - knwonDrugs: target(ensemblId: "ENSG00000105650") { - approvedSymbol - knownDrugs(sortField:"label", sortOrder:Asc) { - count - rows { - drugType - targetId - diseaseId - approvedSymbol - label - prefName - mechanismOfAction - drug { - synonyms - } - } - } - } } diff --git a/test/controllers/GqlTest.scala b/test/controllers/GqlTest.scala index 36c17655..77851aa3 100644 --- a/test/controllers/GqlTest.scala +++ b/test/controllers/GqlTest.scala @@ -138,7 +138,7 @@ class GqlTest "return valid response for Bibliography summary fragment" taggedAs (IntegrationTestTag, ClickhouseTestTag ) in { - testQueryAgainstGqlEndpoint(DiseaseDrugFragment("Bibliography_BibliographySummaryFragment")) + testQueryAgainstGqlEndpoint(DrugFragment("Bibliography_BibliographySummaryFragment")) } "return valid response for Bibliography similar entities summary fragment" taggedAs ( IntegrationTestTag, @@ -330,9 +330,6 @@ class GqlTest "return a valid response for disease profile header" taggedAs IntegrationTestTag in { testQueryAgainstGqlEndpoint(DiseaseFragment("DiseasePage_ProfileHeader")) } - "return a valid response for known drugs" taggedAs IntegrationTestTag in { - testQueryAgainstGqlEndpoint(DiseaseDrugFragment("KnownDrugs_KnownDrugsSummaryFragment")) - } "return a valid response for ontology summary fragment" taggedAs IntegrationTestTag in { testQueryAgainstGqlEndpoint(DiseaseFragment("Ontology_OntologySummaryFragment")) } @@ -428,12 +425,6 @@ class GqlTest } } - "Known Drugs query" must { - "return a valid response" taggedAs IntegrationTestTag in { - testQueryAgainstGqlEndpoint(KnownDrugs("KnownDrugs_KnownDrugsQuery")) - } - } - "MolecularInteractions" must { "return a valid response for interaction stats" taggedAs IntegrationTestTag in { testQueryAgainstGqlEndpoint(Target("MolecularInteractions_InteractionsStats")) @@ -522,9 +513,6 @@ class GqlTest "return valid gene ontology response" taggedAs IntegrationTestTag in { testQueryAgainstGqlEndpoint(TargetFragment("GeneOntology_GeneOntologySummary")) } - "return valid known drugs fragment response" taggedAs IntegrationTestTag in { - testQueryAgainstGqlEndpoint(TargetFragment("KnownDrugs_KnownDrugsSummary")) - } "return valid response for molecular interactions" taggedAs IntegrationTestTag in { testQueryAgainstGqlEndpoint(TargetFragment("MolecularInteractions_InteractionsSummary")) } diff --git a/test/inputs/GqlCase.scala b/test/inputs/GqlCase.scala index 683450b2..99b7fa40 100644 --- a/test/inputs/GqlCase.scala +++ b/test/inputs/GqlCase.scala @@ -151,12 +151,6 @@ case class DiseaseFragment(file: String) extends AbstractDisease with GqlFragmen .replace("xyz", "$efoId") } -case class DiseaseDrugFragment(file: String) extends AbstractDisease with GqlFragment[String] { - def generateFragmentQuery: String = - s"$fragmentQuery query DiseaseFragment(xyz: String!) { disease(efoId: xyz) { knownDrugs { rows { drug { ...$fragmentName } } } } }" - .replace("xyz", "$efoId") -} - /* This is a fragment on disease which takes a gene as an argument. Used on the FE to create summary information. @@ -190,19 +184,6 @@ case class GeneOntology(file: String) extends GqlCase[List[String]] { """ } -case class KnownDrugs(file: String) extends GqlCase[String] { - val inputGenerator = geneGenerator - - def generateVariables(target: String) = - s""" - "variables": { - "ensgId": "$target", - "cursor": null, - "freeTextQuery": null - } - """ -} - case class Search(file: String) extends GqlCase[String] { override val inputGenerator = searchGenerator diff --git a/test/resources/gqlQueries/KnownDrugs_KnownDrugsQuery.gql b/test/resources/gqlQueries/KnownDrugs_KnownDrugsQuery.gql deleted file mode 100644 index 8621c918..00000000 --- a/test/resources/gqlQueries/KnownDrugs_KnownDrugsQuery.gql +++ /dev/null @@ -1,40 +0,0 @@ -query KnownDrugsQuery( - $ensgId: String! - $cursor: String - $freeTextQuery: String - $size: Int = 10 -) { - target(ensemblId: $ensgId) { - id - knownDrugs(cursor: $cursor, freeTextQuery: $freeTextQuery, size: $size) { - count - cursor - rows { - phase - status - urls { - name - url - } - disease { - id - name - } - drug { - id - name - mechanismsOfAction { - rows { - actionType - targets { - id - } - } - } - } - drugType - mechanismOfAction - } - } - } -} diff --git a/test/resources/gqlQueries/KnownDrugs_KnownDrugsSummary.gql b/test/resources/gqlQueries/KnownDrugs_KnownDrugsSummary.gql deleted file mode 100644 index f78e7e48..00000000 --- a/test/resources/gqlQueries/KnownDrugs_KnownDrugsSummary.gql +++ /dev/null @@ -1,7 +0,0 @@ -fragment TargetKnownDrugsSummaryFragment on Target { - knownDrugs { - count - uniqueDrugs - uniqueDiseases - } -} diff --git a/test/resources/gqlQueries/KnownDrugs_KnownDrugsSummaryFragment.gql b/test/resources/gqlQueries/KnownDrugs_KnownDrugsSummaryFragment.gql deleted file mode 100644 index dba9c0b1..00000000 --- a/test/resources/gqlQueries/KnownDrugs_KnownDrugsSummaryFragment.gql +++ /dev/null @@ -1,7 +0,0 @@ -fragment DrugKnownDrugsSummaryFragment on Drug { - knownDrugs { - count - uniqueTargets - uniqueDiseases - } -} From 10196fe63bb9439a8de76e48235558a3965c078c Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 3 Feb 2026 16:30:25 +0000 Subject: [PATCH 04/16] add clinical indication to the api --- app/models/Backend.scala | 30 +++++++++++++++++++- app/models/db/ClinicalIndicationQuery.scala | 29 +++++++++++++++++++ app/models/entities/ClinicalIndication.scala | 22 ++++++++++++++ app/models/entities/Configuration.scala | 3 +- app/models/gql/Objects.scala | 23 +++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 app/models/db/ClinicalIndicationQuery.scala create mode 100644 app/models/entities/ClinicalIndication.scala diff --git a/app/models/Backend.scala b/app/models/Backend.scala index d16a90c5..7ba78f00 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -11,7 +11,7 @@ import esecuele.* import javax.inject.Inject import models.Helpers.* -import models.db.{IntervalsQuery, QAOTF, QLITAGG, QW2V, TargetsQuery} +import models.db.{ClinicalIndicationQuery, IntervalsQuery, QAOTF, QLITAGG, QW2V, TargetsQuery} import models.entities.Publication.* import models.entities.Associations.* import models.entities.Biosample.* @@ -745,6 +745,34 @@ class Backend @Inject() (implicit .map(_.mappedHits) } + def getClinicalIndicationsByDrug(id: String, + pagination: Option[Pagination] + ): Future[IndexedSeq[ClinicalIndication]] = getClinicalIndications(id, pagination, "drugId") + + def getClinicalIndicationsByDisease(id: String, + pagination: Option[Pagination] + ): Future[IndexedSeq[ClinicalIndication]] = getClinicalIndications(id, pagination, "diseaseId") + + private def getClinicalIndications(id: String, + pagination: Option[Pagination], + columnName: String + ): Future[IndexedSeq[ClinicalIndication]] = { + val tableName = getTableWithPrefixOrDefault( + defaultOTSettings.clickhouse.clinicalIndication.name + ) + + val clinicalIndicationsQuery = pagination match { + case Some(pag) => ClinicalIndicationQuery(id, tableName, pag.offset, pag.size, columnName) + case None => ClinicalIndicationQuery(id, tableName, 0, Pagination.sizeMax, columnName) + } + + logger.debug(s"querying clinical indications", keyValue("id", id), keyValue("table", tableName)) + + dbRetriever + .executeQuery[ClinicalIndication, Query](clinicalIndicationsQuery.query) + .map(clinicalIndications => clinicalIndications) + } + def getPharmacogenomicsByDrug(id: String): Future[IndexedSeq[Pharmacogenomics]] = { val queryTerm: Map[String, String] = Map("drugs.drugId.keyword" -> id) getPharmacogenomics(id, queryTerm) diff --git a/app/models/db/ClinicalIndicationQuery.scala b/app/models/db/ClinicalIndicationQuery.scala new file mode 100644 index 00000000..3f18f135 --- /dev/null +++ b/app/models/db/ClinicalIndicationQuery.scala @@ -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 ClinicalIndicationQuery(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(literal(id), column(columnName)) + ), + OrderBy(column("id") :: Nil), + Limit(offset, size), + Format("JSONEachRow"), + Settings(Map("output_format_json_escape_forward_slashes" -> "0")) + ) +} diff --git a/app/models/entities/ClinicalIndication.scala b/app/models/entities/ClinicalIndication.scala new file mode 100644 index 00000000..e3442df4 --- /dev/null +++ b/app/models/entities/ClinicalIndication.scala @@ -0,0 +1,22 @@ +package models.entities + +import play.api.libs.json.{Json, OFormat} +import slick.jdbc.GetResult +import utils.OTLogging + +case class ClinicalIndication( + id: String, + drugId: Option[String], + diseaseId: Option[String], + maxClinicalStage: String, + clinicalReportIds: Seq[String], + hasExpertReview: Boolean +) + +object ClinicalIndication extends OTLogging { + + implicit val getClinicalIndicationsFromDB: GetResult[ClinicalIndication] = + GetResult(r => Json.parse(r.<<[String]).as[ClinicalIndication]) + + implicit val clinicalIndicationsF: OFormat[ClinicalIndication] = Json.format[ClinicalIndication] +} diff --git a/app/models/entities/Configuration.scala b/app/models/entities/Configuration.scala index 4c3db803..b5aa6126 100644 --- a/app/models/entities/Configuration.scala +++ b/app/models/entities/Configuration.scala @@ -66,7 +66,8 @@ object Configuration { similarities: DbTableSettings, harmonic: HarmonicSettings, literature: DbTableSettings, - literatureIndex: DbTableSettings + literatureIndex: DbTableSettings, + clinicalIndication: DbTableSettings ) /** main Open Targets configuration object. It keeps track of meta, elasticsearch and clickhouse diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 18ba4744..62d7b00b 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -643,6 +643,16 @@ object Objects extends OTLogging { ), arguments = Nil, resolve = ctx => diseasesFetcher.deferSeq(ctx.value.ancestors) + ), + Field( + "clinIndication", // TODO: update + ListType(clinicalIndicationImp), + description = Some( + "Clinical indications for this drug as reported by clinical trial records." + ), + arguments = pageArg :: Nil, + complexity = Some(complexityCalculator(pageArg)), + resolve = ctx => ctx.ctx.getClinicalIndicationsByDisease(ctx.value.id, ctx.arg(pageArg)) ) ) ) @@ -1632,10 +1642,23 @@ object Objects extends OTLogging { arguments = pageArg :: Nil, complexity = Some(complexityCalculator(pageArg)), resolve = ctx => ctx.ctx.getPharmacogenomicsByDrug(ctx.value.id) + ), + Field( + "clinIndications", // TODO: update + ListType(clinicalIndicationImp), + description = Some( + "Clinical indications for this drug as reported by clinical trial records." + ), + arguments = pageArg :: Nil, + complexity = Some(complexityCalculator(pageArg)), + resolve = ctx => ctx.ctx.getClinicalIndicationsByDrug(ctx.value.id, ctx.arg(pageArg)) ) ) ) + implicit val clinicalIndicationImp: ObjectType[Backend, ClinicalIndication] = + deriveObjectType[Backend, ClinicalIndication]() + implicit val datasourceSettingsImp: ObjectType[Backend, DatasourceSettings] = deriveObjectType[Backend, DatasourceSettings]( ObjectTypeDescription( From 3c0309d42c3961cbab82d1626f396fc8138bb3dc Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 10 Feb 2026 10:10:15 +0000 Subject: [PATCH 05/16] remove indications add list wrapper --- app/models/Backend.scala | 51 +++++++++---------- app/models/db/ClinicalIndicationQuery.scala | 2 +- app/models/entities/ClinicalIndications.scala | 30 +++++++++++ app/models/entities/Configuration.scala | 6 ++- app/models/gql/DeferredResolvers.scala | 1 - app/models/gql/Fetchers.scala | 6 --- app/models/gql/Objects.scala | 51 +++++++------------ conf/application.conf | 10 ++++ 8 files changed, 88 insertions(+), 69 deletions(-) create mode 100644 app/models/entities/ClinicalIndications.scala diff --git a/app/models/Backend.scala b/app/models/Backend.scala index 7ba78f00..251a9f05 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -745,32 +745,39 @@ class Backend @Inject() (implicit .map(_.mappedHits) } - def getClinicalIndicationsByDrug(id: String, - pagination: Option[Pagination] - ): Future[IndexedSeq[ClinicalIndication]] = getClinicalIndications(id, pagination, "drugId") + def getClinicalIndicationsByDrug(id: String): Future[ClinicalIndications] = + val tableName = getTableWithPrefixOrDefault( + defaultOTSettings.clickhouse.clinicalIndication.drugTable.name + ) - def getClinicalIndicationsByDisease(id: String, - pagination: Option[Pagination] - ): Future[IndexedSeq[ClinicalIndication]] = getClinicalIndications(id, pagination, "diseaseId") + logger.info(s"getting clinical indications by the drug $id", keyValue("table", tableName)) + getClinicalIndications(id, tableName, "drugId") - private def getClinicalIndications(id: String, - pagination: Option[Pagination], - columnName: String - ): Future[IndexedSeq[ClinicalIndication]] = { + def getClinicalIndicationsByDisease(id: String): Future[ClinicalIndications] = val tableName = getTableWithPrefixOrDefault( - defaultOTSettings.clickhouse.clinicalIndication.name + defaultOTSettings.clickhouse.clinicalIndication.diseaseTable.name ) - val clinicalIndicationsQuery = pagination match { - case Some(pag) => ClinicalIndicationQuery(id, tableName, pag.offset, pag.size, columnName) - case None => ClinicalIndicationQuery(id, tableName, 0, Pagination.sizeMax, columnName) - } + logger.info(s"getting clinical indications by the disease $id", keyValue("table", tableName)) + getClinicalIndications(id, tableName, "diseaseId") + + private def getClinicalIndications(id: String, + tableName: String, + columnName: String + ): Future[ClinicalIndications] = { - logger.debug(s"querying clinical indications", keyValue("id", id), keyValue("table", tableName)) + val clinicalIndicationsQuery = ClinicalIndicationQuery(id, tableName, 0, Pagination.sizeMax, columnName) dbRetriever .executeQuery[ClinicalIndication, Query](clinicalIndicationsQuery.query) - .map(clinicalIndications => clinicalIndications) + .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(id: String): Future[IndexedSeq[Pharmacogenomics]] = { @@ -912,16 +919,6 @@ class Backend @Inject() (implicit mechanismsOfActionRaw.map(i => Drug.mechanismOfActionRaw2MechanismOfAction(i.mappedHits)) } - 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(id: String): Future[IndexedSeq[DrugWarning]] = { val indexName = getIndexOrDefault("drugWarnings") logger.debug(s"querying drug warnings", keyValue("id", id), keyValue("index", indexName)) diff --git a/app/models/db/ClinicalIndicationQuery.scala b/app/models/db/ClinicalIndicationQuery.scala index 3f18f135..30875b5a 100644 --- a/app/models/db/ClinicalIndicationQuery.scala +++ b/app/models/db/ClinicalIndicationQuery.scala @@ -19,7 +19,7 @@ case class ClinicalIndicationQuery(id: String, ), From(column(tableName)), PreWhere( - Functions.in(literal(id), column(columnName)) + Functions.in(column(columnName), literal(id)) ), OrderBy(column("id") :: Nil), Limit(offset, size), diff --git a/app/models/entities/ClinicalIndications.scala b/app/models/entities/ClinicalIndications.scala new file mode 100644 index 00000000..dff4d9b8 --- /dev/null +++ b/app/models/entities/ClinicalIndications.scala @@ -0,0 +1,30 @@ +package models.entities + +import models.Backend +import models.gql.Objects.clinicalIndicationImp +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 clinicalIndicationsImp: ObjectType[Backend, ClinicalIndications] = ObjectType( + "indications", + "", + fields[Backend, ClinicalIndications]( + Field("count", + LongType, + description = Some("Total number of indications results matching the query filters"), + resolve = _.value.count + ), + Field("rows", + ListType(clinicalIndicationImp), + description = Some("List of colocalisation results between study-loci pairs"), + resolve = _.value.rows + ) + ) + ) +} diff --git a/app/models/entities/Configuration.scala b/app/models/entities/Configuration.scala index b5aa6126..bff57775 100644 --- a/app/models/entities/Configuration.scala +++ b/app/models/entities/Configuration.scala @@ -55,6 +55,8 @@ object Configuration { case class HarmonicSettings(pExponent: Int, datasources: Seq[DatasourceSettings]) + case class ClinicalIndicationSettings(drugTable: DbTableSettings, diseaseTable: DbTableSettings) + /** ClickHouse settings stores the configuration for the entities it handles. Target Disease and * Harmonic settings used to compute associations on the fly and LUTs for interaction expansions */ @@ -67,7 +69,7 @@ object Configuration { harmonic: HarmonicSettings, literature: DbTableSettings, literatureIndex: DbTableSettings, - clinicalIndication: DbTableSettings + clinicalIndication: ClinicalIndicationSettings ) /** main Open Targets configuration object. It keeps track of meta, elasticsearch and clickhouse @@ -151,6 +153,8 @@ object Configuration { implicit val harmonicSettingsJSONImp: OFormat[HarmonicSettings] = Json.format[HarmonicSettings] implicit val targetSettingsJSONImp: OFormat[TargetSettings] = Json.format[TargetSettings] implicit val diseaseSettingsJSONImp: OFormat[DiseaseSettings] = Json.format[DiseaseSettings] + implicit val clinicalIndicationSettingsJSONImp: OFormat[ClinicalIndicationSettings] = + Json.format[ClinicalIndicationSettings] implicit val clickhouseSettingsJSONImp: OFormat[ClickhouseSettings] = Json.format[ClickhouseSettings] diff --git a/app/models/gql/DeferredResolvers.scala b/app/models/gql/DeferredResolvers.scala index 6069a238..03681241 100644 --- a/app/models/gql/DeferredResolvers.scala +++ b/app/models/gql/DeferredResolvers.scala @@ -196,7 +196,6 @@ object DeferredResolvers extends OTLogging { Fetchers.expressionFetcher, Fetchers.otarProjectsFetcher, Fetchers.soTermsFetcher, - Fetchers.indicationFetcher, Fetchers.goFetcher, Fetchers.variantFetcher, Fetchers.studyFetcher diff --git a/app/models/gql/Fetchers.scala b/app/models/gql/Fetchers.scala index 854acedb..0948fc65 100644 --- a/app/models/gql/Fetchers.scala +++ b/app/models/gql/Fetchers.scala @@ -106,12 +106,6 @@ object Fetchers extends OTLogging { fetch = (ctx: Backend, ids: Seq[String]) => ctx.getDrugs(ids) ) - implicit val indicationHasId: HasId[Indications, String] = HasId[Indications, String](_.id) - val indicationFetcher: Fetcher[Backend, Indications, Indications, String] = Fetcher( - config = FetcherConfig.maxBatchSize(entities.Configuration.batchSize), - fetch = (ctx: Backend, ids: Seq[String]) => ctx.getIndications(ids) - ) - implicit val goFetcherId: HasId[GeneOntologyTerm, String] = HasId[GeneOntologyTerm, String](_.id) val goFetcherCache = FetcherCache.simple val goFetcher: Fetcher[Backend, GeneOntologyTerm, GeneOntologyTerm, String] = Fetcher( diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 62d7b00b..7620d2f7 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -10,6 +10,7 @@ import models.entities.* import models.gql.Arguments.* import models.gql.Fetchers.* import models.Helpers.ComplexityCalculator.* +import models.entities.ClinicalIndications.clinicalIndicationsImp import play.api.libs.json.* import sangria.macros.derive.{DocumentField, *} import sangria.schema.* @@ -18,11 +19,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.* import models.entities.CredibleSets.credibleSetsImp import models.entities.Study.{LdPopulationStructure, Sample, SumStatQC} -import models.entities.Violations.{ - InputParameterCheckError, - InvalidArgValueError, - invalidArgValueErrorMsg -} +import models.entities.Violations.{InputParameterCheckError, InvalidArgValueError, invalidArgValueErrorMsg} import scala.collection.View.Empty import net.logstash.logback.argument.StructuredArguments.keyValue @@ -645,14 +642,13 @@ object Objects extends OTLogging { resolve = ctx => diseasesFetcher.deferSeq(ctx.value.ancestors) ), Field( - "clinIndication", // TODO: update - ListType(clinicalIndicationImp), + "indications", + clinicalIndicationsImp, description = Some( "Clinical indications for this drug as reported by clinical trial records." ), - arguments = pageArg :: Nil, - complexity = Some(complexityCalculator(pageArg)), - resolve = ctx => ctx.ctx.getClinicalIndicationsByDisease(ctx.value.id, ctx.arg(pageArg)) + arguments = Nil, + resolve = ctx => ctx.ctx.getClinicalIndicationsByDisease(ctx.value.id) ) ) ) @@ -1551,14 +1547,6 @@ object Objects extends OTLogging { ) ), AddFields( - Field( - "approvedIndications", - OptionType(ListType(StringType)), - description = Some("Indications for which there is a phase IV clinical trial"), - resolve = r => - DeferredValue(indicationFetcher.deferOpt(r.value.id)) - .map(_.flatMap(_.approvedIndications)) - ), Field( "drugWarnings", ListType(drugWarningsImp), @@ -1614,15 +1602,6 @@ object Objects extends OTLogging { ), resolve = ctx => ctx.ctx.getMechanismsOfAction(ctx.value.id) ), - Field( - "indications", - OptionType(indicationsImp), - description = Some( - "Investigational and approved indications curated from clinical trial records and " + - "post-marketing package inserts" - ), - resolve = ctx => DeferredValue(indicationFetcher.deferOpt(ctx.value.id)) - ), Field( "adverseEvents", OptionType(adverseEventsImp), @@ -1644,14 +1623,13 @@ object Objects extends OTLogging { resolve = ctx => ctx.ctx.getPharmacogenomicsByDrug(ctx.value.id) ), Field( - "clinIndications", // TODO: update - ListType(clinicalIndicationImp), + "indications", + clinicalIndicationsImp, description = Some( "Clinical indications for this drug as reported by clinical trial records." ), - arguments = pageArg :: Nil, - complexity = Some(complexityCalculator(pageArg)), - resolve = ctx => ctx.ctx.getClinicalIndicationsByDrug(ctx.value.id, ctx.arg(pageArg)) + arguments = Nil, + resolve = ctx => ctx.ctx.getClinicalIndicationsByDrug(ctx.value.id) ) ) ) @@ -1702,6 +1680,12 @@ object Objects extends OTLogging { ObjectTypeDescription("Disease-specific database settings configuration"), DocumentField("associations", "Database table settings for disease associations") ) + implicit val clinicalIndicationSettingsImp: ObjectType[Backend, ClinicalIndicationSettings] = + deriveObjectType[Backend, ClinicalIndicationSettings]( + ObjectTypeDescription("Clinical indication database settings configuration"), + DocumentField("drugTable", "Database table settings for drug indications"), + DocumentField("diseaseTable", "Database table settings for disease indications") + ) implicit val harmonicSettingsImp: ObjectType[Backend, HarmonicSettings] = deriveObjectType[Backend, HarmonicSettings]( ObjectTypeDescription("Harmonic mean scoring settings for association calculations"), @@ -1718,7 +1702,8 @@ object Objects extends OTLogging { DocumentField("similarities", "Database table settings for entity similarities"), DocumentField("harmonic", "Harmonic mean scoring settings"), DocumentField("literature", "Database table settings for literature data"), - DocumentField("literatureIndex", "Database table settings for literature index") + DocumentField("literatureIndex", "Database table settings for literature index"), + DocumentField("clinicalIndication", "Database table settings for clinical indications") ) implicit val evidenceSourceImp: ObjectType[Backend, EvidenceSource] = deriveObjectType[Backend, EvidenceSource]( diff --git a/conf/application.conf b/conf/application.conf index bf37bfa8..a3aba3b7 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -32,6 +32,16 @@ ot { label = "Literature index ocurrences table" name = "literature_index" } + clinicalIndication { + drugTable { + label = "Clinical indication drug table" + name = "clinical_indication_drug" + } + diseaseTable { + label = "Clinical indication disease table" + name = "clinical_indication_disease" + } + } similarities { label = "Similarity table for the Word2Vec model" name = "ml_w2v" From d36eee5c66f335320bd161c6c4aa53851dc51b00 Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 10 Feb 2026 10:10:42 +0000 Subject: [PATCH 06/16] format files --- app/models/Backend.scala | 5 +++-- app/models/entities/ClinicalIndications.scala | 12 ++++++------ app/models/gql/Objects.scala | 6 +++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/models/Backend.scala b/app/models/Backend.scala index 251a9f05..7ba43af0 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -766,7 +766,8 @@ class Backend @Inject() (implicit columnName: String ): Future[ClinicalIndications] = { - val clinicalIndicationsQuery = ClinicalIndicationQuery(id, tableName, 0, Pagination.sizeMax, columnName) + val clinicalIndicationsQuery = + ClinicalIndicationQuery(id, tableName, 0, Pagination.sizeMax, columnName) dbRetriever .executeQuery[ClinicalIndication, Query](clinicalIndicationsQuery.query) @@ -774,7 +775,7 @@ class Backend @Inject() (implicit case Seq() => logger.info(s"no clinical indication found for $id in table $tableName") ClinicalIndications(0, IndexedSeq()) - case cis => + case cis => logger.info(s"clinical indications found for $id in table $tableName: ${cis.length}") ClinicalIndications(cis.length, cis) } diff --git a/app/models/entities/ClinicalIndications.scala b/app/models/entities/ClinicalIndications.scala index dff4d9b8..a7597f99 100644 --- a/app/models/entities/ClinicalIndications.scala +++ b/app/models/entities/ClinicalIndications.scala @@ -16,14 +16,14 @@ object ClinicalIndications { "", fields[Backend, ClinicalIndications]( Field("count", - LongType, - description = Some("Total number of indications results matching the query filters"), - resolve = _.value.count + LongType, + description = Some("Total number of indications results matching the query filters"), + resolve = _.value.count ), Field("rows", - ListType(clinicalIndicationImp), - description = Some("List of colocalisation results between study-loci pairs"), - resolve = _.value.rows + ListType(clinicalIndicationImp), + description = Some("List of colocalisation results between study-loci pairs"), + resolve = _.value.rows ) ) ) diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 7620d2f7..e0f20b7c 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -19,7 +19,11 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.* import models.entities.CredibleSets.credibleSetsImp import models.entities.Study.{LdPopulationStructure, Sample, SumStatQC} -import models.entities.Violations.{InputParameterCheckError, InvalidArgValueError, invalidArgValueErrorMsg} +import models.entities.Violations.{ + InputParameterCheckError, + InvalidArgValueError, + invalidArgValueErrorMsg +} import scala.collection.View.Empty import net.logstash.logback.argument.StructuredArguments.keyValue From 1c13a7f945893d05865bab6e8b2142cfe74d37b6 Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 10 Feb 2026 10:24:56 +0000 Subject: [PATCH 07/16] replace drugId and diseaseId --- app/models/gql/Objects.scala | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index e0f20b7c..cf035eb5 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1639,7 +1639,38 @@ object Objects extends OTLogging { ) implicit val clinicalIndicationImp: ObjectType[Backend, ClinicalIndication] = - deriveObjectType[Backend, ClinicalIndication]() + deriveObjectType[Backend, ClinicalIndication]( + ReplaceField( + "drugId", + Field( + "drug", + OptionType(drugImp), + description = Some( + "" + ), + resolve = ctx => { + val id = ctx.value.drugId + logger.debug(s"finding drug $id") + drugsFetcher.deferOpt(id) + } + ) + ), + ReplaceField( + "diseaseId", + Field( + "disease", + OptionType(diseaseImp), + description = Some(""), + resolve = ctx => + ctx.value.diseaseId match { + case Some(tId) => + logger.debug(s"finding disease $tId") + diseasesFetcher.defer(tId) + case None => None + } + ) + ) + ) implicit val datasourceSettingsImp: ObjectType[Backend, DatasourceSettings] = deriveObjectType[Backend, DatasourceSettings]( From 6541c867851c0adf838e6fafe24d64adba6a6749 Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 10 Feb 2026 15:25:42 +0000 Subject: [PATCH 08/16] rename disease field --- app/models/gql/Objects.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index cf035eb5..0bf57d1e 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -646,10 +646,10 @@ object Objects extends OTLogging { resolve = ctx => diseasesFetcher.deferSeq(ctx.value.ancestors) ), Field( - "indications", + "drugAndClinicalCandidates", clinicalIndicationsImp, description = Some( - "Clinical indications for this drug as reported by clinical trial records." + "Clinical indications for this disease as reported by clinical trial records." ), arguments = Nil, resolve = ctx => ctx.ctx.getClinicalIndicationsByDisease(ctx.value.id) From 1baa05bb6a0ec50c1520b0c516632615df46e096 Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 11 Feb 2026 13:55:15 +0000 Subject: [PATCH 09/16] split clinical indication graphql definition --- app/models/entities/ClinicalIndications.scala | 32 +++++++++++++++---- app/models/gql/Objects.scala | 21 +++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/models/entities/ClinicalIndications.scala b/app/models/entities/ClinicalIndications.scala index a7597f99..105b9868 100644 --- a/app/models/entities/ClinicalIndications.scala +++ b/app/models/entities/ClinicalIndications.scala @@ -1,7 +1,7 @@ package models.entities import models.Backend -import models.gql.Objects.clinicalIndicationImp +import models.gql.Objects.{clinicalIndicationFromDiseaseImp, clinicalIndicationFromDrugImp} import sangria.schema.{Field, ListType, LongType, ObjectType, fields} case class ClinicalIndications( @@ -11,8 +11,8 @@ case class ClinicalIndications( object ClinicalIndications { def empty: ClinicalIndications = ClinicalIndications(0, IndexedSeq.empty) - val clinicalIndicationsImp: ObjectType[Backend, ClinicalIndications] = ObjectType( - "indications", + val clinicalIndicationsFromDrugImp: ObjectType[Backend, ClinicalIndications] = ObjectType( + "clinicalIndicationsFromDrugImp", "", fields[Backend, ClinicalIndications]( Field("count", @@ -20,10 +20,28 @@ object ClinicalIndications { description = Some("Total number of indications results matching the query filters"), resolve = _.value.count ), - Field("rows", - ListType(clinicalIndicationImp), - description = Some("List of colocalisation results between study-loci pairs"), - resolve = _.value.rows + 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 ) ) ) diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 0bf57d1e..71de6592 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -10,7 +10,10 @@ import models.entities.* import models.gql.Arguments.* import models.gql.Fetchers.* import models.Helpers.ComplexityCalculator.* -import models.entities.ClinicalIndications.clinicalIndicationsImp +import models.entities.ClinicalIndications.{ + clinicalIndicationsFromDiseaseImp, + clinicalIndicationsFromDrugImp +} import play.api.libs.json.* import sangria.macros.derive.{DocumentField, *} import sangria.schema.* @@ -647,7 +650,7 @@ object Objects extends OTLogging { ), Field( "drugAndClinicalCandidates", - clinicalIndicationsImp, + clinicalIndicationsFromDiseaseImp, description = Some( "Clinical indications for this disease as reported by clinical trial records." ), @@ -1628,7 +1631,7 @@ object Objects extends OTLogging { ), Field( "indications", - clinicalIndicationsImp, + clinicalIndicationsFromDrugImp, description = Some( "Clinical indications for this drug as reported by clinical trial records." ), @@ -1638,8 +1641,10 @@ object Objects extends OTLogging { ) ) - implicit val clinicalIndicationImp: ObjectType[Backend, ClinicalIndication] = + implicit val clinicalIndicationFromDiseaseImp: ObjectType[Backend, ClinicalIndication] = deriveObjectType[Backend, ClinicalIndication]( + ObjectTypeName("ClinicalIndicationFromDisease"), + ExcludeFields("diseaseId"), ReplaceField( "drugId", Field( @@ -1654,7 +1659,13 @@ object Objects extends OTLogging { drugsFetcher.deferOpt(id) } ) - ), + ) + ) + + implicit val clinicalIndicationFromDrugImp: ObjectType[Backend, ClinicalIndication] = + deriveObjectType[Backend, ClinicalIndication]( + ObjectTypeName("ClinicalIndicationFromDrug"), + ExcludeFields("drugId"), ReplaceField( "diseaseId", Field( From cf469f08dc6b202067ca7dfa58b5ed249389c330 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 13 Feb 2026 15:49:32 +0000 Subject: [PATCH 10/16] add clinical reports query --- app/models/Backend.scala | 25 +++++++++++-- app/models/GQLSchema.scala | 15 ++++++++ app/models/db/ClinicalReportQuery.scala | 39 ++++++++++++++++++++ app/models/entities/ClinicalReport.scala | 46 ++++++++++++++++++++++++ app/models/entities/Configuration.scala | 3 +- app/models/gql/Arguments.scala | 7 ++++ app/models/gql/DeferredResolvers.scala | 3 +- app/models/gql/Fetchers.scala | 19 ++++++++-- app/models/gql/Objects.scala | 38 ++++++++++++++++++++ conf/application.conf | 4 +++ 10 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 app/models/db/ClinicalReportQuery.scala create mode 100644 app/models/entities/ClinicalReport.scala diff --git a/app/models/Backend.scala b/app/models/Backend.scala index 7ba43af0..34fff8b0 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -11,7 +11,15 @@ import esecuele.* import javax.inject.Inject import models.Helpers.* -import models.db.{ClinicalIndicationQuery, IntervalsQuery, QAOTF, QLITAGG, QW2V, TargetsQuery} +import models.db.{ + ClinicalIndicationQuery, + ClinicalReportQuery, + IntervalsQuery, + QAOTF, + QLITAGG, + QW2V, + TargetsQuery +} import models.entities.Publication.* import models.entities.Associations.* import models.entities.Biosample.* @@ -761,6 +769,17 @@ class Backend @Inject() (implicit 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 @@ -897,7 +916,7 @@ class Backend @Inject() (implicit def getDrugs(ids: Seq[String]): Future[IndexedSeq[Drug]] = { val drugIndexName = getIndexOrDefault("drug") - logger.debug(s"querying drugs", keyValue("drug_ids", ids), keyValue("index", drugIndexName)) + logger.debug(s"querying drugs with ids: $ids", keyValue("index", drugIndexName)) val queryTerm = Map("id.keyword" -> ids) esRetriever @@ -947,7 +966,7 @@ class Backend @Inject() (implicit def getDiseases(ids: Seq[String]): Future[IndexedSeq[Disease]] = { val diseaseIndexName = getIndexOrDefault("disease") - logger.debug(s"querying diseases", keyValue("ids", ids), keyValue("index", diseaseIndexName)) + logger.debug(s"querying diseases with ids: $ids", keyValue("index", diseaseIndexName)) esRetriever.getByIds(diseaseIndexName, ids, fromJsValue[Disease]) } diff --git a/app/models/GQLSchema.scala b/app/models/GQLSchema.scala index e27cd8bd..54ec762d 100644 --- a/app/models/GQLSchema.scala +++ b/app/models/GQLSchema.scala @@ -204,6 +204,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)) ) ) ) diff --git a/app/models/db/ClinicalReportQuery.scala b/app/models/db/ClinicalReportQuery.scala new file mode 100644 index 00000000..73342930 --- /dev/null +++ b/app/models/db/ClinicalReportQuery.scala @@ -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")) + ) +} diff --git a/app/models/entities/ClinicalReport.scala b/app/models/entities/ClinicalReport.scala new file mode 100644 index 00000000..b0a79ed6 --- /dev/null +++ b/app/models/entities/ClinicalReport.scala @@ -0,0 +1,46 @@ +package models.entities + +import play.api.libs.json.{Json, OFormat} +import slick.jdbc.GetResult +import utils.OTLogging + +case class ClinRepDiseaseListItem(diseaseFromSource: String, diseaseId: String) + +case class ClinRepDrugListItem(drugFromSource: String, drugId: String) + +case class ClinicalReport( + id: String, + source: String, + clinicalStage: String, + phaseFromSource: Option[String], + `type`: Option[String], + trialStudyType: Option[String], + trialDescription: Option[String], + trialNumberOfArms: Option[Int], + trialStartDate: Option[String], + trialLiterature: Seq[String], + trialOverallStatus: Option[String], + trialWhyStopped: Option[String], + trialPrimaryPurpose: Option[String], + trialPhase: Option[String], + diseases: Seq[ClinRepDiseaseListItem], + drugs: Seq[ClinRepDrugListItem], + hasExpertReview: Boolean, + countries: Seq[String], + year: Option[Int], + sideEffects: Seq[ClinRepDiseaseListItem], + trialOfficialTitle: Option[String], + url: Option[String] +) + +object ClinicalReport extends OTLogging { + implicit val getClinicalReportFromDB: GetResult[ClinicalReport] = + GetResult(r => Json.parse(r.<<[String]).as[ClinicalReport]) + + implicit val diseaseListItemF: OFormat[ClinRepDiseaseListItem] = + Json.format[ClinRepDiseaseListItem] + + implicit val drugListItemF: OFormat[ClinRepDrugListItem] = Json.format[ClinRepDrugListItem] + + implicit val clinicalReportF: OFormat[ClinicalReport] = Json.format[ClinicalReport] +} diff --git a/app/models/entities/Configuration.scala b/app/models/entities/Configuration.scala index bff57775..845a19a7 100644 --- a/app/models/entities/Configuration.scala +++ b/app/models/entities/Configuration.scala @@ -69,7 +69,8 @@ object Configuration { harmonic: HarmonicSettings, literature: DbTableSettings, literatureIndex: DbTableSettings, - clinicalIndication: ClinicalIndicationSettings + clinicalIndication: ClinicalIndicationSettings, + clinicalReport: DbTableSettings ) /** main Open Targets configuration object. It keeps track of meta, elasticsearch and clickhouse diff --git a/app/models/gql/Arguments.scala b/app/models/gql/Arguments.scala index dcbd86ab..8d79165a 100644 --- a/app/models/gql/Arguments.scala +++ b/app/models/gql/Arguments.scala @@ -113,6 +113,13 @@ object Arguments { val ensemblId: Argument[String] = Argument("ensemblId", StringType, description = "Ensembl ID") val ensemblIds: Argument[Seq[String @@ FromInput.CoercedScalaResult]] = Argument("ensemblIds", ListInputType(StringType), description = "List of Ensembl IDs") + val clinicalReportId: Argument[String] = + Argument("clinicalReportId", StringType, description = "Clinical Report ID") + val clinicalReportIds: Argument[Seq[String @@ FromInput.CoercedScalaResult]] = + Argument("clinicalReportsIds", + ListInputType(StringType), + description = "List of Clinical Report IDs" + ) val ensemblIdsOpt: Argument[Option[Seq[String]]] = Argument("ensemblIds", OptionInputType(ListInputType(StringType)), diff --git a/app/models/gql/DeferredResolvers.scala b/app/models/gql/DeferredResolvers.scala index 03681241..9fc450d6 100644 --- a/app/models/gql/DeferredResolvers.scala +++ b/app/models/gql/DeferredResolvers.scala @@ -198,6 +198,7 @@ object DeferredResolvers extends OTLogging { Fetchers.soTermsFetcher, Fetchers.goFetcher, Fetchers.variantFetcher, - Fetchers.studyFetcher + Fetchers.studyFetcher, + Fetchers.clinicalReportFetcher ) } diff --git a/app/models/gql/Fetchers.scala b/app/models/gql/Fetchers.scala index 0948fc65..3f6d344d 100644 --- a/app/models/gql/Fetchers.scala +++ b/app/models/gql/Fetchers.scala @@ -2,6 +2,7 @@ package models.gql import models.entities.{ Biosample, + ClinicalReport, CredibleSet, Disease, Drug, @@ -42,9 +43,10 @@ object Fetchers extends OTLogging { FetcherConfig.maxBatchSize(entities.Configuration.batchSize).caching(targetsFetcherCache), fetch = (ctx: Backend, ids: Seq[String]) => ctx.getTargets(ids) ) - val diseasesFetcherCache = FetcherCache.simple // disease + val diseasesFetcherCache = FetcherCache.simple + implicit val diseaseHasId: HasId[Disease, String] = HasId[Disease, String](_.id) val diseasesFetcher: Fetcher[Backend, Disease, Disease, String] = Fetcher( config = @@ -145,6 +147,18 @@ object Fetchers extends OTLogging { ) } + val clinicalReportFetcherCache = FetcherCache.simple + val clinicalReportFetcher: Fetcher[Backend, ClinicalReport, ClinicalReport, String] = { + implicit val clinicalreportFetcherId: HasId[ClinicalReport, String] = + HasId[ClinicalReport, String](js => js.id) + Fetcher( + config = FetcherConfig + .maxBatchSize(entities.Configuration.batchSize) + .caching(clinicalReportFetcherCache), + fetch = (ctx: Backend, ids: Seq[String]) => ctx.getClinicalReports(ids) + ) + } + def resetCache(): Unit = { logger.info("clearing all GraphQL caches") val fetchers: List[SimpleFetcherCache] = List( @@ -160,7 +174,8 @@ object Fetchers extends OTLogging { reactomeFetcherCache, expressionFetcherCache, otarProjectsFetcherCache, - soTermsFetcherCache + soTermsFetcherCache, + clinicalReportFetcherCache ) fetchers.foreach(_.clear()) } diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 71de6592..5c1a4551 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1641,6 +1641,44 @@ object Objects extends OTLogging { ) ) + implicit val clinRepDiseaseListItemImp: ObjectType[Backend, ClinRepDiseaseListItem] = + deriveObjectType[Backend, ClinRepDiseaseListItem]( + ReplaceField( + "diseaseId", + Field( + "disease", + OptionType(diseaseImp), + description = Some(""), + resolve = ctx => + val tId = ctx.value.diseaseId + logger.debug(s"finding disease $tId") + diseasesFetcher.defer(tId) + ) + ) + ) + + implicit val clinRepDrugListItemImp: ObjectType[Backend, ClinRepDrugListItem] = + deriveObjectType[Backend, ClinRepDrugListItem]( + ReplaceField( + "drugId", + Field( + "drug", + OptionType(drugImp), + description = Some( + "" + ), + resolve = ctx => { + val id = ctx.value.drugId + logger.debug(s"finding drug $id") + drugsFetcher.deferOpt(id) + } + ) + ) + ) + + implicit val clinicalReportImp: ObjectType[Backend, ClinicalReport] = + deriveObjectType[Backend, ClinicalReport]() + implicit val clinicalIndicationFromDiseaseImp: ObjectType[Backend, ClinicalIndication] = deriveObjectType[Backend, ClinicalIndication]( ObjectTypeName("ClinicalIndicationFromDisease"), diff --git a/conf/application.conf b/conf/application.conf index a3aba3b7..9bbe1ee6 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -32,6 +32,10 @@ ot { label = "Literature index ocurrences table" name = "literature_index" } + clinicalReport { + label = "Clinical report table" + name = "clinical_report" + } clinicalIndication { drugTable { label = "Clinical indication drug table" From 1fa8ad572db1876eb8c141f1ea0b5cd0b5211454 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 16 Feb 2026 10:04:47 +0000 Subject: [PATCH 11/16] resolve clinical report in clin indications --- app/models/gql/Objects.scala | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 5c1a4551..63cbc47e 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1697,6 +1697,21 @@ object Objects extends OTLogging { drugsFetcher.deferOpt(id) } ) + ), + ReplaceField( + "clinicalReportIds", + Field( + "clinicalReports", + ListType(clinicalReportImp), + description = Some( + "" + ), + resolve = ctx => { + val ids = ctx.value.clinicalReportIds + logger.debug(s"finding clinical reports for ids ${ids.mkString(",")}") + clinicalReportFetcher.deferSeq(ids) + } + ) ) ) @@ -1718,6 +1733,21 @@ object Objects extends OTLogging { case None => None } ) + ), + ReplaceField( + "clinicalReportIds", + Field( + "clinicalReports", + ListType(clinicalReportImp), + description = Some( + "" + ), + resolve = ctx => { + val ids = ctx.value.clinicalReportIds + logger.debug(s"finding clinical reports for ids ${ids.mkString(",")}") + clinicalReportFetcher.deferSeq(ids) + } + ) ) ) From d2702146a8e23aa7b594498173bfad13dd97d21f Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 17 Feb 2026 11:01:32 +0000 Subject: [PATCH 12/16] add clinical target --- app/models/Backend.scala | 22 ++++++- ...Query.scala => AllByIdInColumnQuery.scala} | 10 ++-- .../entities/ClinicalDiseaseListItem.scala | 3 + app/models/entities/ClinicalReport.scala | 10 ++-- app/models/entities/ClinicalTarget.scala | 25 ++++++++ app/models/entities/ClinicalTargets.scala | 32 ++++++++++ app/models/entities/Configuration.scala | 7 ++- app/models/gql/Objects.scala | 59 +++++++++++++++++-- conf/application.conf | 4 ++ 9 files changed, 154 insertions(+), 18 deletions(-) rename app/models/db/{ClinicalIndicationQuery.scala => AllByIdInColumnQuery.scala} (70%) create mode 100644 app/models/entities/ClinicalDiseaseListItem.scala create mode 100644 app/models/entities/ClinicalTarget.scala create mode 100644 app/models/entities/ClinicalTargets.scala diff --git a/app/models/Backend.scala b/app/models/Backend.scala index 34fff8b0..852e6f71 100644 --- a/app/models/Backend.scala +++ b/app/models/Backend.scala @@ -12,7 +12,7 @@ import esecuele.* import javax.inject.Inject import models.Helpers.* import models.db.{ - ClinicalIndicationQuery, + AllByIdInColumnQuery, ClinicalReportQuery, IntervalsQuery, QAOTF, @@ -753,6 +753,24 @@ class Backend @Inject() (implicit .map(_.mappedHits) } + 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 @@ -786,7 +804,7 @@ class Backend @Inject() (implicit ): Future[ClinicalIndications] = { val clinicalIndicationsQuery = - ClinicalIndicationQuery(id, tableName, 0, Pagination.sizeMax, columnName) + AllByIdInColumnQuery(id, tableName, 0, Pagination.sizeMax, columnName) dbRetriever .executeQuery[ClinicalIndication, Query](clinicalIndicationsQuery.query) diff --git a/app/models/db/ClinicalIndicationQuery.scala b/app/models/db/AllByIdInColumnQuery.scala similarity index 70% rename from app/models/db/ClinicalIndicationQuery.scala rename to app/models/db/AllByIdInColumnQuery.scala index 30875b5a..fc73af00 100644 --- a/app/models/db/ClinicalIndicationQuery.scala +++ b/app/models/db/AllByIdInColumnQuery.scala @@ -4,11 +4,11 @@ import esecuele.Column.{column, literal} import esecuele.{Column, Format, From, Functions, Limit, OrderBy, PreWhere, Query, Select, Settings} import utils.OTLogging -case class ClinicalIndicationQuery(id: String, - tableName: String, - offset: Int, - size: Int, - columnName: String +case class AllByIdInColumnQuery(id: String, + tableName: String, + offset: Int, + size: Int, + columnName: String ) extends Queryable with OTLogging { diff --git a/app/models/entities/ClinicalDiseaseListItem.scala b/app/models/entities/ClinicalDiseaseListItem.scala new file mode 100644 index 00000000..4d658565 --- /dev/null +++ b/app/models/entities/ClinicalDiseaseListItem.scala @@ -0,0 +1,3 @@ +package models.entities + +case class ClinicalDiseaseListItem(diseaseFromSource: String, diseaseId: String) diff --git a/app/models/entities/ClinicalReport.scala b/app/models/entities/ClinicalReport.scala index b0a79ed6..4a0f61cf 100644 --- a/app/models/entities/ClinicalReport.scala +++ b/app/models/entities/ClinicalReport.scala @@ -4,8 +4,6 @@ import play.api.libs.json.{Json, OFormat} import slick.jdbc.GetResult import utils.OTLogging -case class ClinRepDiseaseListItem(diseaseFromSource: String, diseaseId: String) - case class ClinRepDrugListItem(drugFromSource: String, drugId: String) case class ClinicalReport( @@ -23,12 +21,12 @@ case class ClinicalReport( trialWhyStopped: Option[String], trialPrimaryPurpose: Option[String], trialPhase: Option[String], - diseases: Seq[ClinRepDiseaseListItem], + diseases: Seq[ClinicalDiseaseListItem], drugs: Seq[ClinRepDrugListItem], hasExpertReview: Boolean, countries: Seq[String], year: Option[Int], - sideEffects: Seq[ClinRepDiseaseListItem], + sideEffects: Seq[ClinicalDiseaseListItem], trialOfficialTitle: Option[String], url: Option[String] ) @@ -37,8 +35,8 @@ object ClinicalReport extends OTLogging { implicit val getClinicalReportFromDB: GetResult[ClinicalReport] = GetResult(r => Json.parse(r.<<[String]).as[ClinicalReport]) - implicit val diseaseListItemF: OFormat[ClinRepDiseaseListItem] = - Json.format[ClinRepDiseaseListItem] + implicit val diseaseListItemF: OFormat[ClinicalDiseaseListItem] = + Json.format[ClinicalDiseaseListItem] implicit val drugListItemF: OFormat[ClinRepDrugListItem] = Json.format[ClinRepDrugListItem] diff --git a/app/models/entities/ClinicalTarget.scala b/app/models/entities/ClinicalTarget.scala new file mode 100644 index 00000000..f7ef818a --- /dev/null +++ b/app/models/entities/ClinicalTarget.scala @@ -0,0 +1,25 @@ +package models.entities + +import play.api.libs.json.{Json, OFormat} +import slick.jdbc.GetResult +import utils.OTLogging + +case class ClinicalTarget( + id: String, + drugId: Option[String], + targetId: Option[String], + diseases: Seq[ClinicalDiseaseListItem], + maxClinicalStage: String, + clinicalReportIds: Seq[String] +) + +object ClinicalTarget extends OTLogging { + + implicit val getClinicalTargetsFromDB: GetResult[ClinicalTarget] = + GetResult(r => Json.parse(r.<<[String]).as[ClinicalTarget]) + + implicit val clinicalDiseaseListItemF: OFormat[ClinicalDiseaseListItem] = + Json.format[ClinicalDiseaseListItem] + + implicit val clinicalTargetsF: OFormat[ClinicalTarget] = Json.format[ClinicalTarget] +} diff --git a/app/models/entities/ClinicalTargets.scala b/app/models/entities/ClinicalTargets.scala new file mode 100644 index 00000000..fb583607 --- /dev/null +++ b/app/models/entities/ClinicalTargets.scala @@ -0,0 +1,32 @@ +package models.entities + +import models.Backend +import models.gql.Objects.clinicalTargetImp +import sangria.schema.{Field, ListType, LongType, ObjectType, fields} + +case class ClinicalTargets( + count: Long, + rows: IndexedSeq[ClinicalTarget] +) + +object ClinicalTargets { + def empty: ClinicalTargets = ClinicalTargets(0, IndexedSeq.empty) + val clinicalTargetsImp: ObjectType[Backend, ClinicalTargets] = ObjectType( + "clinicalTargets", + "", + fields[Backend, ClinicalTargets]( + Field("count", + LongType, + description = + Some("Total number of clinical targets results matching the query filters"), + resolve = _.value.count + ), + Field( + "rows", + ListType(clinicalTargetImp), + description = Some("List of clinical clinical targets results between drug-target pairs"), + resolve = _.value.rows + ) + ) + ) +} diff --git a/app/models/entities/Configuration.scala b/app/models/entities/Configuration.scala index 845a19a7..d93e069a 100644 --- a/app/models/entities/Configuration.scala +++ b/app/models/entities/Configuration.scala @@ -57,6 +57,8 @@ object Configuration { case class ClinicalIndicationSettings(drugTable: DbTableSettings, diseaseTable: DbTableSettings) + case class ClinicalTargetSettings(drugTable: DbTableSettings, targetTable: DbTableSettings) + /** ClickHouse settings stores the configuration for the entities it handles. Target Disease and * Harmonic settings used to compute associations on the fly and LUTs for interaction expansions */ @@ -70,7 +72,8 @@ object Configuration { literature: DbTableSettings, literatureIndex: DbTableSettings, clinicalIndication: ClinicalIndicationSettings, - clinicalReport: DbTableSettings + clinicalReport: DbTableSettings, + clinicalTarget: DbTableSettings ) /** main Open Targets configuration object. It keeps track of meta, elasticsearch and clickhouse @@ -156,6 +159,8 @@ object Configuration { implicit val diseaseSettingsJSONImp: OFormat[DiseaseSettings] = Json.format[DiseaseSettings] implicit val clinicalIndicationSettingsJSONImp: OFormat[ClinicalIndicationSettings] = Json.format[ClinicalIndicationSettings] + implicit val clinicalTargetSettingsJSONImp: OFormat[ClinicalTargetSettings] = + Json.format[ClinicalTargetSettings] implicit val clickhouseSettingsJSONImp: OFormat[ClickhouseSettings] = Json.format[ClickhouseSettings] diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 63cbc47e..7f4a8fc8 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -14,8 +14,9 @@ import models.entities.ClinicalIndications.{ clinicalIndicationsFromDiseaseImp, clinicalIndicationsFromDrugImp } +import models.entities.ClinicalTargets.clinicalTargetsImp import play.api.libs.json.* -import sangria.macros.derive.{DocumentField, *} +import sangria.macros.derive.* import sangria.schema.* import scala.concurrent.ExecutionContext.Implicits.global @@ -409,6 +410,13 @@ object Objects extends OTLogging { arguments = pageArg :: Nil, complexity = Some(complexityCalculator(pageArg)), resolve = ctx => ctx.ctx.getProteinCodingCoordinatesByTarget(ctx.value.id, ctx.arg(pageArg)) + ), + Field( + "drugAndClinicalCandidates", + clinicalTargetsImp, + description = Some(""), + arguments = Nil, + resolve = ctx => ctx.ctx.getClinicalTargetsByTarget(ctx.value.id) ) ) ) @@ -1641,8 +1649,8 @@ object Objects extends OTLogging { ) ) - implicit val clinRepDiseaseListItemImp: ObjectType[Backend, ClinRepDiseaseListItem] = - deriveObjectType[Backend, ClinRepDiseaseListItem]( + implicit val clinRepDiseaseListItemImp: ObjectType[Backend, ClinicalDiseaseListItem] = + deriveObjectType[Backend, ClinicalDiseaseListItem]( ReplaceField( "diseaseId", Field( @@ -1751,6 +1759,42 @@ object Objects extends OTLogging { ) ) + implicit val clinicalTargetImp: ObjectType[Backend, ClinicalTarget] = + deriveObjectType[Backend, ClinicalTarget]( + ObjectTypeName("ClinicalTargetFromTarget"), + ExcludeFields("targetId"), + ReplaceField( + "drugId", + Field( + "drug", + OptionType(drugImp), + description = Some( + "" + ), + resolve = ctx => { + val id = ctx.value.drugId + logger.debug(s"finding drug $id") + drugsFetcher.deferOpt(id) + } + ) + ), + ReplaceField( + "clinicalReportIds", + Field( + "clinicalReports", + ListType(clinicalReportImp), + description = Some( + "" + ), + resolve = ctx => { + val ids = ctx.value.clinicalReportIds + logger.debug(s"finding clinical reports for ids ${ids.mkString(",")}") + clinicalReportFetcher.deferSeq(ids) + } + ) + ) + ) + implicit val datasourceSettingsImp: ObjectType[Backend, DatasourceSettings] = deriveObjectType[Backend, DatasourceSettings]( ObjectTypeDescription( @@ -1800,6 +1844,12 @@ object Objects extends OTLogging { DocumentField("drugTable", "Database table settings for drug indications"), DocumentField("diseaseTable", "Database table settings for disease indications") ) + implicit val clinicalTargetSettingsImp: ObjectType[Backend, ClinicalTargetSettings] = + deriveObjectType[Backend, ClinicalTargetSettings]( + ObjectTypeDescription("Clinical indication database settings configuration"), + DocumentField("drugTable", "Database table settings for drug indications"), + DocumentField("targetTable", "Database table settings for disease indications") + ) implicit val harmonicSettingsImp: ObjectType[Backend, HarmonicSettings] = deriveObjectType[Backend, HarmonicSettings]( ObjectTypeDescription("Harmonic mean scoring settings for association calculations"), @@ -1817,7 +1867,8 @@ object Objects extends OTLogging { DocumentField("harmonic", "Harmonic mean scoring settings"), DocumentField("literature", "Database table settings for literature data"), DocumentField("literatureIndex", "Database table settings for literature index"), - DocumentField("clinicalIndication", "Database table settings for clinical indications") + DocumentField("clinicalIndication", "Database table settings for clinical indications"), + DocumentField("clinicalTarget", "Database table settings for clinical target") ) implicit val evidenceSourceImp: ObjectType[Backend, EvidenceSource] = deriveObjectType[Backend, EvidenceSource]( diff --git a/conf/application.conf b/conf/application.conf index 9bbe1ee6..44c3c21e 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -46,6 +46,10 @@ ot { name = "clinical_indication_disease" } } + clinicalTarget { + label = "Clinical target table" + name = "clinical_target" + } similarities { label = "Similarity table for the Word2Vec model" name = "ml_w2v" From 171a9e5c20e1d50339e6910705c22c5504d7a2eb Mon Sep 17 00:00:00 2001 From: David Ochoa Date: Tue, 17 Feb 2026 20:44:21 +0000 Subject: [PATCH 13/16] feat: rename chembl fields for clinical_precedence evidence Replace clinicalPhase with clinicalStage (String), add clinicalReportId, rename studyStopReason to trialWhyStopped and studyStopReasonCategories to trialStopReasonCategories. Remove clinicalStatus and urls. Update test GQL queries to use clinical_precedence datasource. --- app/models/entities/Evidence.scala | 8 ++++---- app/models/gql/Objects.scala | 12 ++++++------ test/resources/gqlQueries/Chembl_ChemblQuery.gql | 15 +++++---------- .../gqlQueries/Chembl_ChemblSummaryFragment.gql | 2 +- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app/models/entities/Evidence.scala b/app/models/entities/Evidence.scala index e70316f7..9725cfa6 100644 --- a/app/models/entities/Evidence.scala +++ b/app/models/entities/Evidence.scala @@ -77,7 +77,8 @@ case class Evidence( log2FoldChangePercentileRank: Option[Long], biologicalModelAllelicComposition: Option[String], confidence: Option[String], - clinicalPhase: Option[Double], + clinicalStage: Option[String], + clinicalReportId: Option[String], resourceScore: Option[Double], variantFunctionalConsequenceId: Option[String], variantFunctionalConsequenceFromQtlId: Option[String], @@ -91,7 +92,6 @@ case class Evidence( datasourceId: String, datatypeId: String, oddsRatioConfidenceIntervalUpper: Option[Double], - clinicalStatus: Option[String], log2FoldChangeValue: Option[Double], oddsRatio: Option[Double], cohortDescription: Option[String], @@ -115,8 +115,8 @@ case class Evidence( betaConfidenceIntervalLower: Option[Double], betaConfidenceIntervalUpper: Option[Double], studyStartDate: Option[String], - studyStopReason: Option[String], - studyStopReasonCategories: Option[Seq[String]], + trialWhyStopped: Option[String], + trialStopReasonCategories: Option[Seq[String]], targetFromSource: Option[String], cellLineBackground: Option[String], contrast: Option[String], diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 7f4a8fc8..84c370d3 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -2724,10 +2724,10 @@ object Objects extends OTLogging { DocumentField("biologicalModelAllelicComposition", "Allelic composition of the model organism"), DocumentField("confidence", "Confidence qualifier on the reported evidence"), DocumentField( - "clinicalPhase", - "Phase of the clinical trial. [Values: -1: `Unknown`, 0: `Phase 0`, 0.5: `Phase I (Early)`, 1: `Phase I`, 2: `Phase II`, 3: `Phase III`, 4: `Phase IV`]" + "clinicalStage", + "Clinical stage of the drug-disease pair" ), - DocumentField("clinicalStatus", "Current stage of a clinical study"), + DocumentField("clinicalReportId", "Identifier of the clinical report"), DocumentField("clinicalSignificances", "Standard terms to define clinical significance"), DocumentField("resourceScore", "Score provided by datasource indicating strength of target-disease association" @@ -2767,9 +2767,9 @@ object Objects extends OTLogging { DocumentField("betaConfidenceIntervalLower", "Lower value of the confidence interval"), DocumentField("betaConfidenceIntervalUpper", "Upper value of the confidence interval"), DocumentField("studyStartDate", "Start date of study in a YYYY-MM-DD format"), - DocumentField("studyStopReason", "Reason why a study has been stopped"), - DocumentField("studyStopReasonCategories", - "Predicted reason(s) why the study has been stopped based on studyStopReason" + DocumentField("trialWhyStopped", "Reason why the trial was stopped, as reported"), + DocumentField("trialStopReasonCategories", + "Categorised reason(s) why the trial was stopped" ), DocumentField("cellLineBackground", "Background of the derived cell lines"), DocumentField("contrast", "Experiment contrast"), diff --git a/test/resources/gqlQueries/Chembl_ChemblQuery.gql b/test/resources/gqlQueries/Chembl_ChemblQuery.gql index d9c86369..99be3c46 100644 --- a/test/resources/gqlQueries/Chembl_ChemblQuery.gql +++ b/test/resources/gqlQueries/Chembl_ChemblQuery.gql @@ -4,7 +4,7 @@ query ChemblQuery($ensemblId: String!, $efoId: String!, $size: Int!, $cursor: St chembl: evidences( ensemblIds: [$ensemblId] enableIndirect: true - datasourceIds: ["chembl"] + datasourceIds: ["clinical_precedence"] size: $size cursor: $cursor ) { @@ -36,16 +36,11 @@ query ChemblQuery($ensemblId: String!, $efoId: String!, $size: Int!, $cursor: St variantEffect directionOnTrait targetFromSourceId - clinicalPhase - clinicalStatus + clinicalStage + clinicalReportId studyStartDate - studyStopReason - studyStopReasonCategories - cohortPhenotypes - urls { - niceName - url - } + trialWhyStopped + trialStopReasonCategories } } } diff --git a/test/resources/gqlQueries/Chembl_ChemblSummaryFragment.gql b/test/resources/gqlQueries/Chembl_ChemblSummaryFragment.gql index e0a749db..481f8060 100644 --- a/test/resources/gqlQueries/Chembl_ChemblSummaryFragment.gql +++ b/test/resources/gqlQueries/Chembl_ChemblSummaryFragment.gql @@ -2,7 +2,7 @@ fragment ChemblSummaryFragment on Disease { chembl: evidences( ensemblIds: [$ensgId] enableIndirect: true - datasourceIds: ["chembl"] + datasourceIds: ["clinical_precedence"] size: 0 ) { count From a78f9996c80335614fb52709845187d7fb6d8005 Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 18 Feb 2026 10:32:15 +0000 Subject: [PATCH 14/16] update clinical schemas --- app/models/ClickhouseRetriever.scala | 2 +- app/models/entities/ClinicalIndication.scala | 3 +- app/models/entities/ClinicalReport.scala | 2 + app/models/entities/Drug.scala | 20 --------- app/models/gql/Fetchers.scala | 1 - app/models/gql/Objects.scala | 43 +------------------- 6 files changed, 5 insertions(+), 66 deletions(-) diff --git a/app/models/ClickhouseRetriever.scala b/app/models/ClickhouseRetriever.scala index cdccde18..105d0d09 100644 --- a/app/models/ClickhouseRetriever.scala +++ b/app/models/ClickhouseRetriever.scala @@ -73,7 +73,7 @@ class ClickhouseRetriever(config: OTSettings)(implicit case Failure(ex) => 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 } } } diff --git a/app/models/entities/ClinicalIndication.scala b/app/models/entities/ClinicalIndication.scala index e3442df4..3294b366 100644 --- a/app/models/entities/ClinicalIndication.scala +++ b/app/models/entities/ClinicalIndication.scala @@ -9,8 +9,7 @@ case class ClinicalIndication( drugId: Option[String], diseaseId: Option[String], maxClinicalStage: String, - clinicalReportIds: Seq[String], - hasExpertReview: Boolean + clinicalReportIds: Seq[String] ) object ClinicalIndication extends OTLogging { diff --git a/app/models/entities/ClinicalReport.scala b/app/models/entities/ClinicalReport.scala index 4a0f61cf..d2424a24 100644 --- a/app/models/entities/ClinicalReport.scala +++ b/app/models/entities/ClinicalReport.scala @@ -12,6 +12,7 @@ case class ClinicalReport( clinicalStage: String, phaseFromSource: Option[String], `type`: Option[String], + title: Option[String], trialStudyType: Option[String], trialDescription: Option[String], trialNumberOfArms: Option[Int], @@ -21,6 +22,7 @@ case class ClinicalReport( trialWhyStopped: Option[String], trialPrimaryPurpose: Option[String], trialPhase: Option[String], + trialStopReasonCategories: Seq[String], diseases: Seq[ClinicalDiseaseListItem], drugs: Seq[ClinRepDrugListItem], hasExpertReview: Boolean, diff --git a/app/models/entities/Drug.scala b/app/models/entities/Drug.scala index e8705c9c..ba00dd12 100644 --- a/app/models/entities/Drug.scala +++ b/app/models/entities/Drug.scala @@ -21,8 +21,6 @@ case class DrugWarning( case class Reference(ids: Option[Seq[String]], source: String, urls: Option[Seq[String]]) -case class IndicationReference(ids: Option[Seq[String]], source: String) - case class MechanismOfActionRow( mechanismOfAction: String, actionType: Option[String], @@ -31,19 +29,6 @@ case class MechanismOfActionRow( references: Option[Seq[Reference]] ) -case class IndicationRow( - maxPhaseForIndication: Double, - disease: String, - references: Option[Seq[IndicationReference]] -) - -case class Indications( - id: String, - indications: Seq[IndicationRow], - indicationCount: Long, - approvedIndications: Option[Seq[String]] -) - case class MechanismsOfAction( rows: Seq[MechanismOfActionRow], uniqueActionTypes: Seq[String], @@ -106,11 +91,6 @@ object Drug { Json.format[models.entities.MechanismOfActionRow] implicit val mechanismOfActionImpW: OFormat[MechanismsOfAction] = Json.format[models.entities.MechanismsOfAction] - implicit val indicationReferenceImpW: OFormat[IndicationReference] = - Json.format[models.entities.IndicationReference] - implicit val indicationRowImpW: OFormat[IndicationRow] = - Json.format[models.entities.IndicationRow] - implicit val indicationsImpW: OFormat[Indications] = Json.format[models.entities.Indications] implicit val mechanismOfActionRaw: OFormat[MechanismOfActionRaw] = Json.format[models.entities.MechanismOfActionRaw] diff --git a/app/models/gql/Fetchers.scala b/app/models/gql/Fetchers.scala index 3f6d344d..173555ef 100644 --- a/app/models/gql/Fetchers.scala +++ b/app/models/gql/Fetchers.scala @@ -9,7 +9,6 @@ import models.entities.{ Expressions, GeneOntologyTerm, HPO, - Indications, OtarProjects, Reactome, SequenceOntologyTerm, diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 84c370d3..9fec8a4b 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1352,13 +1352,6 @@ object Objects extends OTLogging { ) ) - implicit lazy val indicationReferenceImp: ObjectType[Backend, IndicationReference] = - deriveObjectType[Backend, IndicationReference]( - ObjectTypeDescription("Reference information for drug indications"), - DocumentField("ids", "List of reference identifiers (e.g., PubMed IDs)"), - DocumentField("source", "Source of the reference") - ) - implicit lazy val mechanismOfActionRowImp: ObjectType[Backend, MechanismOfActionRow] = deriveObjectType[Backend, MechanismOfActionRow]( ObjectTypeDescription("Mechanism of action information for a drug"), @@ -1382,38 +1375,6 @@ object Objects extends OTLogging { ) ) - implicit lazy val indicationRowImp: ObjectType[Backend, IndicationRow] = - deriveObjectType[Backend, IndicationRow]( - ObjectTypeDescription( - "Indication information linking a drug or clinical candidate molecule to a disease" - ), - DocumentField( - "maxPhaseForIndication", - "Maximum clinical trial phase for this drug-disease indication. [Values: -1: `Unknown`, 0: `Phase 0`, 0.5: `Phase I (Early)`, 1: `Phase I`, 2: `Phase II`, 3: `Phase III`, 4: `Phase IV`]" - ), - DocumentField("references", "Reference information supporting the indication"), - ReplaceField( - "disease", - Field( - "disease", - diseaseImp, - description = Some("Potential indication disease entity"), - resolve = r => diseasesFetcher.defer(r.value.disease) - ) - ) - ) - - implicit lazy val indicationsImp: ObjectType[Backend, Indications] = - deriveObjectType[Backend, Indications]( - ObjectTypeDescription("Collection of indications for a drug or clinical candidate molecule"), - ExcludeFields("id"), - RenameField("indications", "rows"), - RenameField("indicationCount", "count"), - DocumentField("indications", "List of potential indication entries"), - DocumentField("indicationCount", "Total number of potential indications"), - DocumentField("approvedIndications", "List of approved indication identifiers") - ) - implicit lazy val resourceScoreImp: ObjectType[Backend, ResourceScore] = deriveObjectType[Backend, ResourceScore]( ObjectTypeDescription("Score from a specific datasource"), @@ -2768,9 +2729,7 @@ object Objects extends OTLogging { DocumentField("betaConfidenceIntervalUpper", "Upper value of the confidence interval"), DocumentField("studyStartDate", "Start date of study in a YYYY-MM-DD format"), DocumentField("trialWhyStopped", "Reason why the trial was stopped, as reported"), - DocumentField("trialStopReasonCategories", - "Categorised reason(s) why the trial was stopped" - ), + DocumentField("trialStopReasonCategories", "Categorised reason(s) why the trial was stopped"), DocumentField("cellLineBackground", "Background of the derived cell lines"), DocumentField("contrast", "Experiment contrast"), DocumentField("crisprScreenLibrary", From a8d33218f5b48c8ed28a4cc82b2ef4b0f7f52423 Mon Sep 17 00:00:00 2001 From: David Ochoa Date: Fri, 20 Feb 2026 21:15:44 +0000 Subject: [PATCH 15/16] feat: replace maximumClinicalTrialPhase with maximumClinicalStage in Drug Remove isApproved, hasBeenWithdrawn, and blackBoxWarning fields from Drug entity and GraphQL type. Rename maximumClinicalTrialPhase (Double) to maximumClinicalStage (String) to match updated PTS drug_molecule output. --- app/models/entities/Drug.scala | 5 +---- app/models/gql/Objects.scala | 9 ++------- queries.graphql | 11 +---------- test/resources/gqlQueries/APIPage_DrugAnnotation.gql | 3 --- test/resources/gqlQueries/DrugPage_ProfileHeader.gql | 5 +---- .../gqlQueries/DrugWarnings_DrugWarningsQuery.gql | 2 -- .../DrugWarnings_DrugWarningsSummaryFragment.gql | 3 +-- .../gqlQueries/SearchPage_SearchPageQuery.gql | 3 +-- 8 files changed, 7 insertions(+), 34 deletions(-) diff --git a/app/models/entities/Drug.scala b/app/models/entities/Drug.scala index ba00dd12..777c51b1 100644 --- a/app/models/entities/Drug.scala +++ b/app/models/entities/Drug.scala @@ -54,12 +54,9 @@ case class Drug( tradeNames: Seq[String], childChemblIds: Option[Seq[String]], // Gone? drugType: String, - isApproved: Option[Boolean], crossReferences: Option[Seq[DrugReferences]], parentId: Option[String], - maximumClinicalTrialPhase: Option[Double], - hasBeenWithdrawn: Boolean, - blackBoxWarning: Boolean, + maximumClinicalStage: String, description: Option[String] ) diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index 9fec8a4b..dd28ed6a 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -1492,14 +1492,9 @@ object Objects extends OTLogging { "Classification of the molecule's therapeutic category or chemical class (e.g. Antibody)" ), DocumentField( - "maximumClinicalTrialPhase", - "Highest clinical trial phase reached by the drug or clinical candidate molecule. [Values: -1: `Unknown`, 0: `Phase 0`, 0.5: `Phase I (Early)`, 1: `Phase I`, 2: `Phase II`, 3: `Phase III`, 4: `Phase IV`]" + "maximumClinicalStage", + "Highest clinical stage reached by the drug or clinical candidate molecule" ), - DocumentField("isApproved", - "Flag indicating whether the drug has received regulatory approval" - ), - DocumentField("hasBeenWithdrawn", "Flag indicating whether the drug was removed from market"), - DocumentField("blackBoxWarning", "Flag indicating whether the drug has safety warnings"), DocumentField("crossReferences", "Cross-reference information for this molecule from external databases" ), diff --git a/queries.graphql b/queries.graphql index aad6c6b0..625f404f 100644 --- a/queries.graphql +++ b/queries.graphql @@ -203,15 +203,7 @@ query test { synonyms tradeNames drugType - maximumClinicalTrialPhase - hasBeenWithdrawn - withdrawnNotice { - classes - countries - reasons - year - } - internalCompound + maximumClinicalStage mechanismsOfAction { uniqueActionTypes uniqueTargetTypes @@ -228,7 +220,6 @@ query test { } } } - blackBoxWarning indications { count rows { diff --git a/test/resources/gqlQueries/APIPage_DrugAnnotation.gql b/test/resources/gqlQueries/APIPage_DrugAnnotation.gql index 38a4c675..385c8249 100644 --- a/test/resources/gqlQueries/APIPage_DrugAnnotation.gql +++ b/test/resources/gqlQueries/APIPage_DrugAnnotation.gql @@ -2,9 +2,6 @@ query drugApprovalWithdrawnWarningData { drug(chemblId: "CHEMBL118") { name id - isApproved - hasBeenWithdrawn - blackBoxWarning drugWarnings { warningType description diff --git a/test/resources/gqlQueries/DrugPage_ProfileHeader.gql b/test/resources/gqlQueries/DrugPage_ProfileHeader.gql index d17bab3c..f5cb3f04 100644 --- a/test/resources/gqlQueries/DrugPage_ProfileHeader.gql +++ b/test/resources/gqlQueries/DrugPage_ProfileHeader.gql @@ -10,10 +10,7 @@ fragment DrugProfileHeaderFragment on Drug { id name } - isApproved - hasBeenWithdrawn - blackBoxWarning - maximumClinicalTrialPhase + maximumClinicalStage tradeNames yearOfFirstApproval } diff --git a/test/resources/gqlQueries/DrugWarnings_DrugWarningsQuery.gql b/test/resources/gqlQueries/DrugWarnings_DrugWarningsQuery.gql index 2f282f0f..ab19b599 100644 --- a/test/resources/gqlQueries/DrugWarnings_DrugWarningsQuery.gql +++ b/test/resources/gqlQueries/DrugWarnings_DrugWarningsQuery.gql @@ -16,7 +16,5 @@ query DrugWarningsQuery($chemblId: String!) { url } } - hasBeenWithdrawn - blackBoxWarning } } diff --git a/test/resources/gqlQueries/DrugWarnings_DrugWarningsSummaryFragment.gql b/test/resources/gqlQueries/DrugWarnings_DrugWarningsSummaryFragment.gql index c66be325..a8ff1f2a 100644 --- a/test/resources/gqlQueries/DrugWarnings_DrugWarningsSummaryFragment.gql +++ b/test/resources/gqlQueries/DrugWarnings_DrugWarningsSummaryFragment.gql @@ -1,4 +1,3 @@ fragment DrugWarningsSummaryFragment on Drug { - hasBeenWithdrawn - blackBoxWarning + id } diff --git a/test/resources/gqlQueries/SearchPage_SearchPageQuery.gql b/test/resources/gqlQueries/SearchPage_SearchPageQuery.gql index 46d40dc8..80d934ec 100644 --- a/test/resources/gqlQueries/SearchPage_SearchPageQuery.gql +++ b/test/resources/gqlQueries/SearchPage_SearchPageQuery.gql @@ -71,8 +71,7 @@ query SearchPageQuery( description name drugType - maximumClinicalTrialPhase - hasBeenWithdrawn + maximumClinicalStage indications { rows { disease { From 1c6db4de3e3139ba473ff864c6e6352c8b3e67f4 Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 25 Feb 2026 13:40:06 +0000 Subject: [PATCH 16/16] handle empty/null id disease and drug and escape --- app/models/entities/ClinicalReport.scala | 20 ++++++++++-- app/models/gql/Objects.scala | 40 +++++++++++++++++------- conf/logback.xml | 12 +++---- production.xml | 14 ++++----- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/app/models/entities/ClinicalReport.scala b/app/models/entities/ClinicalReport.scala index d2424a24..37970877 100644 --- a/app/models/entities/ClinicalReport.scala +++ b/app/models/entities/ClinicalReport.scala @@ -1,6 +1,6 @@ package models.entities -import play.api.libs.json.{Json, OFormat} +import play.api.libs.json.{JsString, JsValue, Json, OFormat, Writes} import slick.jdbc.GetResult import utils.OTLogging @@ -35,12 +35,28 @@ case class ClinicalReport( object ClinicalReport extends OTLogging { implicit val getClinicalReportFromDB: GetResult[ClinicalReport] = - GetResult(r => Json.parse(r.<<[String]).as[ClinicalReport]) + GetResult { r => + val raw = r.<<[String] + val escaped = raw + .replaceAll("""\\([*^<>&\[\]_~])""", "$1") + .replace("\\", "\\\\") + .replace("\n", "\\n") + .replaceAll("""\\(nrt)""", "\\\\$1") + val json = Json.parse(escaped) + json.as[ClinicalReport] + } + + implicit val diseaseListItemW: Writes[ClinicalDiseaseListItem] = + Json.writes[ClinicalDiseaseListItem] implicit val diseaseListItemF: OFormat[ClinicalDiseaseListItem] = Json.format[ClinicalDiseaseListItem] + implicit val drugListItemW: Writes[ClinRepDrugListItem] = Json.writes[ClinRepDrugListItem] + implicit val drugListItemF: OFormat[ClinRepDrugListItem] = Json.format[ClinRepDrugListItem] + implicit val clinicalReportW: Writes[ClinicalReport] = Json.writes[ClinicalReport] + implicit val clinicalReportF: OFormat[ClinicalReport] = Json.format[ClinicalReport] } diff --git a/app/models/gql/Objects.scala b/app/models/gql/Objects.scala index dd28ed6a..6a29f1ac 100644 --- a/app/models/gql/Objects.scala +++ b/app/models/gql/Objects.scala @@ -8,7 +8,7 @@ import models.entities.Publications.publicationsImp import models.entities.Colocalisations.* import models.entities.* import models.gql.Arguments.* -import models.gql.Fetchers.* +import models.gql.Fetchers.{diseasesFetcher, *} import models.Helpers.ComplexityCalculator.* import models.entities.ClinicalIndications.{ clinicalIndicationsFromDiseaseImp, @@ -1614,9 +1614,13 @@ object Objects extends OTLogging { OptionType(diseaseImp), description = Some(""), resolve = ctx => - val tId = ctx.value.diseaseId + val tId: String = ctx.value.diseaseId logger.debug(s"finding disease $tId") - diseasesFetcher.defer(tId) + + tId match { + case "" => None + case _ => diseasesFetcher.deferOpt(tId) + } ) ) ) @@ -1634,7 +1638,11 @@ object Objects extends OTLogging { resolve = ctx => { val id = ctx.value.drugId logger.debug(s"finding drug $id") - drugsFetcher.deferOpt(id) + + id match { + case "" => None + case _ => drugsFetcher.deferOpt(id) + } } ) ) @@ -1656,9 +1664,13 @@ object Objects extends OTLogging { "" ), resolve = ctx => { - val id = ctx.value.drugId + val id: Option[String] = ctx.value.drugId logger.debug(s"finding drug $id") - drugsFetcher.deferOpt(id) + + id match { + case None => None + case Some(dId) => drugsFetcher.deferOpt(dId) + } } ) ), @@ -1673,7 +1685,7 @@ object Objects extends OTLogging { resolve = ctx => { val ids = ctx.value.clinicalReportIds logger.debug(s"finding clinical reports for ids ${ids.mkString(",")}") - clinicalReportFetcher.deferSeq(ids) + clinicalReportFetcher.deferSeqOpt(ids) } ) ) @@ -1693,7 +1705,7 @@ object Objects extends OTLogging { ctx.value.diseaseId match { case Some(tId) => logger.debug(s"finding disease $tId") - diseasesFetcher.defer(tId) + diseasesFetcher.deferOpt(tId) case None => None } ) @@ -1709,7 +1721,7 @@ object Objects extends OTLogging { resolve = ctx => { val ids = ctx.value.clinicalReportIds logger.debug(s"finding clinical reports for ids ${ids.mkString(",")}") - clinicalReportFetcher.deferSeq(ids) + clinicalReportFetcher.deferSeqOpt(ids) } ) ) @@ -1728,9 +1740,13 @@ object Objects extends OTLogging { "" ), resolve = ctx => { - val id = ctx.value.drugId + val id: Option[String] = ctx.value.drugId logger.debug(s"finding drug $id") - drugsFetcher.deferOpt(id) + + id match { + case None => None + case Some(dId) => drugsFetcher.deferOpt(dId) + } } ) ), @@ -1745,7 +1761,7 @@ object Objects extends OTLogging { resolve = ctx => { val ids = ctx.value.clinicalReportIds logger.debug(s"finding clinical reports for ids ${ids.mkString(",")}") - clinicalReportFetcher.deferSeq(ids) + clinicalReportFetcher.deferSeqOpt(ids) } ) ) diff --git a/conf/logback.xml b/conf/logback.xml index 463b948d..154a98f3 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -24,12 +24,12 @@ - - - - - - + + + + + + diff --git a/production.xml b/production.xml index 38a077fd..a805757e 100644 --- a/production.xml +++ b/production.xml @@ -32,13 +32,13 @@ - - - - - - - + + + + + + +