All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sangria.relay.Connection.scala Maven / Gradle / Ivy

The newest version!
package sangria.relay

import sangria.execution.UserFacingError
import sangria.relay.util.Base64
import sangria.schema._

import scala.annotation.{implicitNotFound, tailrec}
import scala.concurrent.{ExecutionContext, Future}
import scala.language.higherKinds
import scala.reflect.ClassTag
import scala.util.Try

trait Connection[+T] {
  def pageInfo: PageInfo
  def edges: Seq[Edge[T]]
}

object Connection {
  object Args {
    val Before = Argument("before", OptionInputType(StringType))
    val After = Argument("after", OptionInputType(StringType))
    val First = Argument("first", OptionInputType(IntType))
    val Last = Argument("last", OptionInputType(IntType))

    val All = Before :: After :: First :: Last :: Nil
  }

  @tailrec
  def isValidNodeType[Val](nodeType: OutputType[Val]): Boolean = nodeType match {
    case _: ScalarType[_] | _: EnumType[_] | _: CompositeType[_] => true
    case OptionType(ofType) => isValidNodeType(ofType)
    case _ => false
  }

  def definition[Ctx, Conn[_], Val](
      name: String,
      nodeType: OutputType[Val],
      edgeFields: => List[Field[Ctx, Edge[Val]]] = Nil,
      connectionFields: => List[Field[Ctx, Conn[Val]]] = Nil
  )(implicit
      connEv: ConnectionLike[Conn, PageInfo, Val, Edge[Val]],
      classEv: ClassTag[Conn[Val]]): ConnectionDefinition[Ctx, Conn[Val], Val, Edge[Val]] =
    definitionWithEdge[Ctx, DefaultPageInfo, Conn, Val, Edge[Val]](
      name,
      nodeType,
      edgeFields,
      connectionFields)

  def definitionWithEdge[Ctx, P <: PageInfo, Conn[_], Val, E <: Edge[Val]](
      name: String,
      nodeType: OutputType[Val],
      edgeFields: => List[Field[Ctx, E]] = Nil,
      connectionFields: => List[Field[Ctx, Conn[Val]]] = Nil,
      pageInfoType: Option[OutputType[P]] = None
  )(implicit
      connEv: ConnectionLike[Conn, P, Val, E],
      classEv: ClassTag[Conn[Val]],
      classE: ClassTag[E],
      classP: ClassTag[P]) = {
    if (!isValidNodeType(nodeType))
      throw new IllegalArgumentException(
        "Node type is invalid. It must be either a Scalar, Enum, Object, Interface, Union, " +
          "or a Non‐Null wrapper around one of those types. Notably, this field cannot return a list.")

    val edgeType = ObjectType[Ctx, E](
      name + "Edge",
      "An edge in a connection.",
      () =>
        List[Field[Ctx, E]](
          Field("node", nodeType, Some("The item at the end of the edge."), resolve = _.value.node),
          Field(
            "cursor",
            StringType,
            Some("A cursor for use in pagination."),
            resolve = _.value.cursor)
        ) ++ edgeFields
    )

    val connectionType = ObjectType[Ctx, Conn[Val]](
      name + "Connection",
      "A connection to a list of items.",
      () =>
        List[Field[Ctx, Conn[Val]]](
          Field(
            "pageInfo",
            pageInfoType.getOrElse(DefaultPageInfo.pageInfoType[Ctx, P]),
            Some("Information to aid in pagination."),
            resolve = ctx => connEv.pageInfo(ctx.value)
          ),
          Field(
            "edges",
            OptionType(ListType(OptionType(edgeType))),
            Some("A list of edges."),
            resolve = ctx => connEv.edges(ctx.value).map(Some(_)))
        ) ++ connectionFields
    )

    ConnectionDefinition[Ctx, Conn[Val], Val, E](edgeType, connectionType)
  }

  val CursorPrefix = "arrayconnection:"

  def empty[T] = DefaultConnection(PageInfo.empty, Vector.empty[Edge[T]])

  def connectionFromFutureSeq[T](seq: Future[Seq[T]], args: ConnectionArgs)(implicit
      ec: ExecutionContext): Future[Connection[T]] =
    seq.map(connectionFromSeq(_, args))

  def connectionFromSeq[T](seq: Seq[T], args: ConnectionArgs): Connection[T] =
    connectionFromSeq(seq, args, SliceInfo(0, seq.size))

  def connectionFromFutureSeq[T](seq: Future[Seq[T]], args: ConnectionArgs, sliceInfo: SliceInfo)(
      implicit ec: ExecutionContext): Future[Connection[T]] =
    seq.map(connectionFromSeq(_, args, sliceInfo))

  def connectionFromSeq[T](
      arraySlice: Seq[T],
      args: ConnectionArgs,
      sliceInfo: SliceInfo): Connection[T] = {
    val ConnectionArgs(before, after, first, last) = args

    first.foreach(f =>
      if (f < 0)
        throw ConnectionArgumentValidationError("Argument 'first' must be a non-negative integer"))
    last.foreach(l =>
      if (l < 0)
        throw ConnectionArgumentValidationError("Argument 'last' must be a non-negative integer"))

    val SliceInfo(sliceStart, arrayLength) = sliceInfo

    val sliceEnd = sliceStart + arraySlice.size

    val startOffset = math.max(sliceStart, 0)
    val endOffset = math.min(sliceEnd, arrayLength)

    val afterOffset = getOffset(after, -1)
    val (firstEdgeOffset, actualStartOffset) = if (afterOffset >= 0 && afterOffset < arrayLength) {
      val actualStartOffset = math.max(startOffset, afterOffset + 1)
      val fEO = afterOffset + 1
      fEO -> actualStartOffset
    } else 0 -> startOffset

    val beforeOffset = getOffset(before, arrayLength)
    val (lastEdgeOffset, actualEndOffset) = if (0 <= beforeOffset && beforeOffset < arrayLength) {
      val lEO = beforeOffset - 1
      val actualEndOffset = math.min(endOffset, beforeOffset)
      lEO -> actualEndOffset
    } else (arrayLength - 1) -> endOffset

    val numberEdgesAfterCursor = lastEdgeOffset - firstEdgeOffset + 1

    val finalEndOffset =
      first.fold(actualEndOffset)(f => math.min(actualEndOffset, f + actualStartOffset))
    val finalStartOffset =
      last.fold(actualStartOffset)(l => math.max(actualStartOffset, actualEndOffset - l))

    // If supplied slice is too large, trim it down before mapping over it.
    val slice = arraySlice.slice(
      math.max(finalStartOffset - sliceStart, 0),
      arraySlice.size - (sliceEnd - finalEndOffset))

    val edges = slice.zipWithIndex.map { case (value, index) =>
      Edge(value, offsetToCursor(finalStartOffset + index))
    }

    val firstEdge = edges.headOption
    val lastEdge = edges.lastOption

    val hasPreviousPage = (last, after) match {
      case (Some(l), _) => numberEdgesAfterCursor > l
      case (None, Some(_)) => afterOffset >= 0
      case (_, _) => false
    }

    val hasNextPage = (first, before) match {
      case (Some(f), _) => numberEdgesAfterCursor > f
      case (_, Some(_)) => beforeOffset < arrayLength
      case (_, _) => false
    }

    DefaultConnection(
      PageInfo(
        hasNextPage,
        hasPreviousPage,
        startCursor = firstEdge.map(_.cursor),
        endCursor = lastEdge.map(_.cursor)
      ),
      edges
    )
  }

  def cursorForObjectInConnection[T, E](coll: Seq[T], obj: E): Option[String] = {
    val idx = coll.indexOf(obj)

    if (idx >= 0) Some(offsetToCursor(idx)) else None
  }

  private def getOffset(cursor: Option[String], defaultOffset: Int): Int =
    cursor.flatMap(cursorToOffset).getOrElse(defaultOffset)

  def offsetToCursor(offset: Int): String = Base64.encode(CursorPrefix + offset)

  def cursorToOffset(cursor: String): Option[Int] =
    GlobalId.fromGlobalId(cursor).flatMap(id => Try(id.id.toInt).toOption)
}

case class SliceInfo(sliceStart: Int, size: Int)

case class ConnectionDefinition[Ctx, Conn, Val, E <: Edge[Val]](
    edgeType: ObjectType[Ctx, E],
    connectionType: ObjectType[Ctx, Conn])

case class DefaultConnection[T](pageInfo: PageInfo, edges: Seq[Edge[T]]) extends Connection[T]

trait Edge[+T] {
  def node: T
  def cursor: String
}

object Edge {
  def apply[T](node: T, cursor: String) = DefaultEdge(node, cursor)
}

case class DefaultEdge[T](node: T, cursor: String) extends Edge[T]

trait PageInfo {
  def hasNextPage: Boolean
  def hasPreviousPage: Boolean
  def startCursor: Option[String]
  def endCursor: Option[String]
}

case class DefaultPageInfo(
    hasNextPage: Boolean = false,
    hasPreviousPage: Boolean = false,
    startCursor: Option[String] = None,
    endCursor: Option[String] = None)
    extends PageInfo

object DefaultPageInfo {
  def pageInfoType[Ctx, P <: PageInfo: ClassTag]: ObjectType[Ctx, P] =
    ObjectType[Ctx, P](
      "PageInfo",
      "Information about pagination in a connection.",
      () =>
        List[Field[Ctx, P]](
          Field(
            "hasNextPage",
            BooleanType,
            Some("When paginating forwards, are there more items?"),
            resolve = _.value.hasNextPage),
          Field(
            "hasPreviousPage",
            BooleanType,
            Some("When paginating backwards, are there more items?"),
            resolve = _.value.hasPreviousPage),
          Field(
            "startCursor",
            OptionType(StringType),
            Some("When paginating backwards, the cursor to continue."),
            resolve = _.value.startCursor),
          Field(
            "endCursor",
            OptionType(StringType),
            Some("When paginating forwards, the cursor to continue."),
            resolve = _.value.endCursor)
        )
    )
}

object PageInfo {
  def empty: DefaultPageInfo = DefaultPageInfo()

  def apply(
      hasNextPage: Boolean = false,
      hasPreviousPage: Boolean = false,
      startCursor: Option[String] = None,
      endCursor: Option[String] = None
  ): PageInfo = DefaultPageInfo(hasNextPage, hasPreviousPage, startCursor, endCursor)

}

@implicitNotFound(
  "Type ${T} can't be used as a Connection. Please consider defining implicit instance of sangria.relay.ConnectionLike for type ${T} or extending sangria.relay.Connection trait.")
trait ConnectionLike[T[_], P <: PageInfo, Val, E <: Edge[Val]] {
  def pageInfo(conn: T[Val]): P
  def edges(conn: T[Val]): Seq[E]
}

object ConnectionLike {
  private object ConnectionIsConnectionLike
      extends ConnectionLike[Connection, PageInfo, Any, Edge[Any]] {
    override def pageInfo(conn: Connection[Any]) = conn.pageInfo
    override def edges(conn: Connection[Any]) = conn.edges
  }

  implicit def connectionIsConnectionLike[E <: Edge[Val], P <: PageInfo, Val, T[_]]
      : ConnectionLike[T, P, Val, E] =
    ConnectionIsConnectionLike.asInstanceOf[ConnectionLike[T, P, Val, E]]
}

case class ConnectionArgs(
    before: Option[String] = None,
    after: Option[String] = None,
    first: Option[Int] = None,
    last: Option[Int] = None)

object ConnectionArgs {
  def apply(args: WithArguments): ConnectionArgs =
    ConnectionArgs(
      args.arg(Connection.Args.Before),
      args.arg(Connection.Args.After),
      args.arg(Connection.Args.First),
      args.arg(Connection.Args.Last))

  val empty = ConnectionArgs()
}

case class ConnectionArgumentValidationError(message: String)
    extends Exception(message)
    with UserFacingError




© 2015 - 2025 Weber Informatics LLC | Privacy Policy