Search code examples
scalagraphqlsangria

What's the best way to pass field arguments (e.g. paging parameters) to a deferred `Fetcher`?


Below is an example of how I'm currently handling deferred field arguments.

A Parent class contains a deferred Page of Child objects. The paging parameters for the children are defined on the children field. I need to be able to pass the paging parameters, along with the parent's id, to the deferred Fetcher, so I bundle them up in a temporary DeferredChildInput object, along with the parent's id, and pass them to the Fetcher. A corresponding DeferredChildResult returns the result of the query (the Page[Child]) and the DeferredChildInput (use in the HasId).

Question is, is there a better way to pass field arguments and parent id to the deferred Fetcher?

case class Page[T](
  data: Seq[T],
  pageNumber: Int,
  pageSize: Int,
  totalRecords: Long
)

case class Parent(
  id: Long,
  children: Page[Children] // this field is deferred
)

case class Child(
  id: Long
  parentId: Long
)

// the field's query parameters
case class DeferredChildInput(
  parentId: Long,
  pageNumber: Int,
  pageSize: Int
)

// used to temporarily hold the result of the deferred resolution
case class DeferredChildResult(
  input: DeferredChildInput // this is used to resolve the HasId check
  page: Page[Child] // this is what we really want
)

trait ChildService {
  def getChildrenByParentId(
    parentId: Long,
    pageNumber: Int,
    pageSize: Int
  ): Page[Child]
}

val childFetcher: Fetcher[MyContext, DeferredChildResult, DeferredChildResult, DeferredChildInput] = Fetcher {
    (ctx: MyContext, inputs: Seq[DeferredChildInput]) =>
      val futures = inputs.map { input =>
        ctx.childService.getChildrenByParentId(
          input.parentId,
          input.pageNumber,
          input.pageSize
        ).map { childPage =>
          DeferredChildResult(input, childPage)
        }
      }

    Future.sequence {
      futures
    }
  }(HasId(_.input))
}

val ChildObjectType = derivedObjectType[Unit, Child]()

val ParentObjectType = deriveObjectType[Unit, Parent](
  ReplaceField(
    fieldName = "children",
    field = Field(
      name = "children",
      fieldType = PageType(childObjectType),
      arguments = List(
        Argument(
          name = "pageNumber",
          argumentType = IntType
        ), Argument(
          name = "pageSize",
          argumentType = IntType,
        )
      ),
      resolve = ctx => {
        // bundle the field/query parameters into a single input object
        val input = DeferredChildInput(
          parentId = ctx.value.id,
          pageNumber = ctx.args[Int]("pageNumber"),
          pageSize = ctx.args[Int]("pageSize")
        )

        DeferredValue(childFetcher.defer(input)).map { results =>
          results.page
        }
      }
    )
  )
)

Solution

  • In this scenario, you don't really benefit from using fetchers, it just increases the complexity of data fetching mechanism. For example, it is quite unlikely that results would be cached and reused. You are also resolving each DeferredChildInput separately which defeats the main purpose of fetchers (which is to batch the data fetching, where data for all inputs is fetched in a single request/DB interaction).

    I would recommend avoiding using fetchers altogether in this scenario and fetch the page data directly from resolve function. Fetchers don't really support pagination. In some scenarios, it might be viable to fetch the IDs of entities in a resolve function and then use fetcher to fetch the entity data based on the already known list of IDs. But as far as I can tell, it is not the case in your scenario.