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

caliban.wrappers.IncrementalDelivery.scala Maven / Gradle / Ivy

The newest version!
package caliban.wrappers

import caliban.execution.{ ExecutionRequest, Feature }
import caliban.introspection.adt.{ __Directive, __Type }
import caliban.parsing.adt.Definition.ExecutableDefinition.FragmentDefinition
import caliban.parsing.adt.Selection.{ Field, FragmentSpread, InlineFragment }
import caliban.parsing.adt._
import caliban.validation.ValidationOps.validateAllDiscard
import caliban.validation.Validator.{ failValidation, QueryValidation }
import caliban.wrappers.Wrapper.ValidationWrapper
import caliban._
import zio.ZIO

import scala.annotation.tailrec
import scala.collection.mutable

object IncrementalDelivery {

  lazy val defer: GraphQLAspect[Nothing, Any]  = aspect(Feature.Defer)
  lazy val stream: GraphQLAspect[Nothing, Any] = aspect(Feature.Stream)
  lazy val all: GraphQLAspect[Nothing, Any]    = aspect(Feature.Defer, Feature.Stream)

  def aspect(feature: Feature, others: Feature*): GraphQLAspect[Nothing, Any] = new GraphQLAspect[Nothing, Any] {
    private val featureSet    = Set(feature) ++ others
    private val directiveList = {
      val directives = List.newBuilder[__Directive]
      for { f <- featureSet } directives ++= f.directives
      directives.result()
    }
    private val flags         = featureSet.foldLeft(0)(_ | _.mask)

    override def apply[R](gql: GraphQL[R]): GraphQL[R] =
      gql
        .enableAll(featureSet)
        .withAdditionalDirectives(directiveList)
        .withWrapper(withValidations(flags))
  }

  private val onlyTopLevelQuery: Feature.Flags => QueryValidation = flags =>
    context => {
      def matches(name: String): Boolean =
        (Feature.isStreamEnabled(flags) && name == Directives.Stream) ||
          (Feature.isDeferEnabled(flags) && name == Directives.Defer)

      @tailrec
      def hasStreamOrDirective(selection: List[Selection]): Boolean =
        selection match {
          case Selection.Field(_, _, _, directives, _, _) :: rest   =>
            directives.exists(d => matches(d.name)) || hasStreamOrDirective(rest)
          case Selection.InlineFragment(_, _, selectionSet) :: rest =>
            hasStreamOrDirective(rest ++ selectionSet)
          case Selection.FragmentSpread(name, _) :: rest            =>
            hasStreamOrDirective(rest ++ context.fragments(name).selectionSet)
          case Nil                                                  => false
        }

      validateAllDiscard(context.operations) { op =>
        if (op.operationType != OperationType.Query && hasStreamOrDirective(op.selectionSet)) {
          Left(
            CalibanError.ValidationError(
              "Stream or defer directive was used on a root field in a mutation or subscription",
              "Defer and stream may not be used on root fields of mutations or subscriptions"
            )
          )
        } else {
          Right(())
        }
      }
    }

  private val appearsOnlyOnLists: QueryValidation = context => {
    val checked: mutable.Set[(String, Option[String])] = mutable.Set.empty

    def validateFields(selectionSet: List[Selection], currentType: __Type): Either[CalibanError.ValidationError, Unit] =
      validateAllDiscard(selectionSet) {
        case f: Field                                          =>
          validateField(f, currentType)
        case FragmentSpread(name, directives)                  =>
          if (directives.exists(_.name == Directives.Stream))
            Left(CalibanError.ValidationError("Stream directive was used on a fragment spread", ""))
          else {
            context.fragments.getOrElse(name, null) match {
              case null                                              => Left(CalibanError.ValidationError(s"Fragment $name not found", ""))
              case fragment if checked.add((name, currentType.name)) =>
                validateSpread(fragment, currentType)
              case _                                                 => Right(())
            }
          }
        case InlineFragment(typeCondition, dirs, selectionSet) =>
          if (dirs.exists(_.name == Directives.Stream))
            Left(CalibanError.ValidationError("Stream directive was used on an inline fragment", ""))
          else {
            if (typeCondition.exists(_.name.contains(currentType.typeNameRepr)))
              validateFields(selectionSet, currentType)
            else Right(())
          }

      }

    def validateSpread(fragment: FragmentDefinition, currentType: __Type): Either[CalibanError.ValidationError, Unit] =
      validateFields(fragment.selectionSet, currentType)

    def validateField(field: Field, currentType: __Type): Either[CalibanError.ValidationError, Unit] = {
      val selected = currentType.allFields.find(_.name == field.name)
      Either.cond(
        selected.isDefined && !selected.get._type.isList && field.directives.forall(_.name != Directives.Stream),
        (),
        CalibanError.ValidationError("Stream directive was used on a non-list field", "")
      )
    }

    validateAllDiscard(context.operations) { op =>
      op.operationType match {
        case OperationType.Query        =>
          validateFields(op.selectionSet, context.rootType.queryType)
        case OperationType.Mutation     =>
          context.rootType.mutationType.fold[Either[CalibanError.ValidationError, Unit]](
            failValidation("Mutation operations are not supported on this schema.", "")
          )(validateFields(op.selectionSet, _))
        case OperationType.Subscription =>
          context.rootType.subscriptionType.fold[Either[CalibanError.ValidationError, Unit]](
            failValidation("Subscription operations are not supported on this schema.", "")
          )(validateFields(op.selectionSet, _))
      }
    }
  }

  private val uniqueLabels: Feature.Flags => QueryValidation = flags =>
    context => {
      val labels = mutable.Set[String]()

      def extractLabel(directives: List[Directive], directiveName: String): Option[InputValue] =
        directives.collectFirst {
          case d if d.name == directiveName =>
            d.arguments.get("label")
        }.flatten

      def isLabelUnique(label: Option[InputValue]): Boolean = label.forall {
        case Value.StringValue(value) => labels.add(value)
        case _                        => true
      }

      @tailrec
      def allLabelsUnique(selections: List[Selection]): Boolean = selections match {
        case Selection.Field(_, _, _, directives, children, _) :: rest if Feature.isStreamEnabled(flags)    =>
          val label = extractLabel(directives, Directives.Stream)

          isLabelUnique(label) && allLabelsUnique(rest ++ children)
        case Selection.InlineFragment(_, directives, selectionSet) :: rest if Feature.isDeferEnabled(flags) =>
          val label = extractLabel(directives, Directives.Defer)

          isLabelUnique(label) && allLabelsUnique(rest ++ selectionSet)
        case Selection.FragmentSpread(name, directives) :: rest if Feature.isDeferEnabled(flags)            =>
          val label = extractLabel(directives, Directives.Defer)

          isLabelUnique(label) && allLabelsUnique(rest ++ context.fragments(name).selectionSet)
        case _                                                                                              => true
      }

      if (!allLabelsUnique(context.operations.flatMap(_.selectionSet))) {
        Left(CalibanError.ValidationError("Stream and defer directive labels must be unique", ""))
      } else {
        Right(())
      }
    }

  private def additionalValidations(features: Feature.Flags) = {
    val streamValidations =
      if (Feature.isStreamEnabled(features)) List(appearsOnlyOnLists) else Nil

    List(onlyTopLevelQuery(features), uniqueLabels(features)) ++ streamValidations
  }

  private def withValidations(features: Feature.Flags): Wrapper[Any] = new ValidationWrapper[Any] {
    private val validations = additionalValidations(features)

    override def wrap[R1 <: Any](
      f: Document => ZIO[R1, CalibanError.ValidationError, ExecutionRequest]
    ): Document => ZIO[R1, CalibanError.ValidationError, ExecutionRequest] = { doc =>
      Configurator.ref
        .locallyWith(config => config.copy(validations = config.validations ++ validations))(f(doc))
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy