I've joined an org that uses sangria and I'm trying to introduce localized caching to the sangria implementation of graphql but I have a few odd edge cases that I can't find examples for in the docs.
Part of the challenge is that it's a batch scenario where for 1 AId I get many BIds and then I can use those BIds in fetching additional values from C services
The TL;DR of my Q is:
Can I utilize the results of one Fetcher inside the call of another Fetcher or is there another more idiomatic way to solve dependencies between calls while utilizing the caching of values to reduce calls to underlying services? In our case they are GRPC services but that is really just an implementation detail.
here is an extremely fake example. The place where I wrote code that genuinely can't work is within allBsForOneAFetcher. This idea of calling aFetcher.defer(aId) and then utilizing it's results to fetch B results is where my brain is stuck. Even tho from what I can tell this can't be done, I can't get my head unstuck to see it a different way.
import com.localorg.common.api.graphql.context.RootContext
import sangria.execution.deferred.{Fetcher, HasId}
import sangria.macros.derive.deriveObjectType
import sangria.schema._
import sangria.macros.derive._
import sangria.schema.{Field, ListType, OptionType}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
type AId = Int
type BId = String
type CId = String
type XId = String
// comes from source A
case class A(id: AId, bId: BId, xId: XId, aName: String)
// comes from source B that has no direct relationship to source A
// It must be joined via the graphql server
case class B(id: BId, bName: String)
// comes from source B that has no direct relationship to source B or A
// It must be joined via the graphql server
case class C(id: CId, bId: BId, cData: Map[String, String])
object Fetchers {
// the aFetcher gets all A objects for a given XId
val aFetcher: Fetcher[
RootContext,
List[A],
List[A],
XId
] = Fetcher.caching((c: RootContext, ids: Seq[XId]) => {
ids.map { id =>
c.repos.aRepo.getAData(id)
}
}(HasId(_.xId))
// get all b objects related to a single A object
val allBsForOneAFetcher: Fetcher[
RootContext,
List[B],
List[B],
(XId, Option[BId]) // maybe we are fetching data for one B, maybe fetching all Bs for one A
] = Fetcher.caching((c: RootContext, ids: Seq[(XId, Option[BId])]) => {
ids.map {
case (xId, Some(bId)) => {
// fetch data specific to one B for one A
// ...
}
case (xId, None) => {
// need to get BIds from A objects, preferably via the aFetcher to rely on caching.
// This is fake code that doesn't work but in another world would work.
// fetch all Bs for one A
// imagine this is Future[List[A]] even tho this isn't how fetcher.defer works
// How do I do this part right here?
val deferredAResponse: Future[List[A]] = aFetcher.defer(xId)
deferredAResponse.map { response =>
val bIds: List[BId] = response.map(_.bId)
c.repos.bRepo.getAllBDataByIds(bIds)
}
}
}
}(HasId (resp => {
if (resp.length == 1) {
(resp.head.xId, Some (resp.head.bId))
} else {
(resp.head.xId, None)
}
})) // match the id for caching to the context of fetching for 1 or many
val allCsForOneBFetcher: Fetcher[
RootContext,
List[C],
List[C],
(BId, Option[CId]) // maybe we are fetching data for one C, maybe fetching all Cs for one B
] = ??? // similar impl as allBsForOneAFetcher but less complicated
}
case class Results(
someA: Option[A],
someBObjects: List[B],
someCObjects: List[C]
// imagine we have other fields we fetch from other sources
)
object Schema{
implicit val resultsOutput = deriveObjectType[RootContext, Results](
ObjectTypeName ("ResultsData"),
ExcludeFields (
"someA",
"someBObjects",
),
AddFields (
Field (
"aName",
fieldType = OptionType (StringType),
resolve = c =>
DeferredValue (
Fetchers.aFetcher.defer (c arg String)
).map (data => data.headOption.map (_.aName))
),
Field (
"bNames",
ListType (StringType),
resolve = c =>
DeferredValue (
Fetchers.allBsForOneAFetcher.defer (c arg String, None)
).map (data => data.map (_.bName))
),
Field(
"CData",
ListType (StringType),
resolve = c =>
DeferredValue (
Fetchers.allCsForOneBFetcher.defer (c arg String, None)
).map (data => data.map (_.cData.values))
)
)
)
}
object Fields {
val getData: Field[RootContext, Unit] = Field(
name = "getData",
fieldType = ListType(Schema.resultsOutput),
description = Some ("get data"),
arguments = Argument[Int] :: Nil, // imagine this is an A id
resolve = c => {
// imagine we need some results that includes some data that is eagerly fetched in addition
// to the deferred Option[A] & List[B] values
//
List(
Results(
None,
List(),
List()
)
)
}
)
}
Tried to fetch a batch of values in a 1-many relationship and then use the many objects in subsequent calls but this seems impossible if I utilize Fetcher.caching. I can do it with eager fetching but that's not a good idea for obvious reasons.