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

sangria.execution.Executor.scala Maven / Gradle / Ivy

The newest version!
package sangria.execution

import sangria.ast
import sangria.ast.SourceMapper
import sangria.marshalling.{InputUnmarshaller, ResultMarshaller}
import sangria.schema._
import sangria.validation.QueryValidator
import InputUnmarshaller.emptyMapVars
import sangria.execution.deferred.DeferredResolver
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}

case class Executor[Ctx, Root](
    schema: Schema[Ctx, Root],
    queryValidator: QueryValidator = QueryValidator.default,
    deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
    exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
    deprecationTracker: Option[DeprecationTracker] = None,
    middleware: List[Middleware[Ctx]] = Nil,
    maxQueryDepth: Option[Int] = None,
    queryReducers: List[QueryReducer[Ctx, _]] = Nil,
    errorsLimit: Option[Int] = Some(10)
)(implicit executionContext: ExecutionContext) {
  def prepare[Input](
      queryAst: ast.Document,
      userContext: Ctx,
      root: Root,
      operationName: Option[String] = None,
      variables: Input = emptyMapVars
  )(implicit um: InputUnmarshaller[Input]): Future[PreparedQuery[Ctx, Root, Input]] = {
    val scalarMiddleware = Middleware.composeFromScalarMiddleware(middleware, userContext)
    val valueCollector = new ValueCollector[Ctx, Input](
      schema,
      variables,
      queryAst.sourceMapper,
      deprecationTracker,
      userContext,
      exceptionHandler,
      scalarMiddleware,
      false)(um)

    val operationCtx = for {
      operation <- Executor.getOperation(exceptionHandler, queryAst, operationName)
      unmarshalledVariables <- valueCollector.getVariableValues(
        operation.variables,
        scalarMiddleware,
        errorsLimit
      )
    } yield (operation, unmarshalledVariables)

    operationCtx match {
      case Failure(error) =>
        // return validation errors without variables first if variables is what failed
        val violations = queryValidator.validateQuery(schema, queryAst, Map.empty, errorsLimit)

        if (violations.nonEmpty)
          Future.failed(ValidationError(violations, exceptionHandler))
        else
          Future.failed(error)
      case Success((operation, unmarshalledVariables)) =>
        val (violations, validationTiming) =
          TimeMeasurement.measure(
            queryValidator.validateQuery(schema, queryAst, unmarshalledVariables, errorsLimit))

        if (violations.nonEmpty)
          Future.failed(ValidationError(violations, exceptionHandler))
        else {
          val executionResult = for {
            tpe <- Executor.getOperationRootType(
              schema,
              exceptionHandler,
              operation,
              queryAst.sourceMapper)
            fieldCollector = new FieldCollector[Ctx, Root](
              schema,
              queryAst,
              unmarshalledVariables,
              queryAst.sourceMapper,
              valueCollector,
              exceptionHandler)
            fields <- fieldCollector.collectFields(ExecutionPath.empty, tpe, Vector(operation))
          } yield {
            val preparedFields = fields.fields.flatMap {
              case CollectedField(_, astField, Success(_)) =>
                val allFields =
                  tpe.getField(schema, astField.name).asInstanceOf[Vector[Field[Ctx, Root]]]
                val field = allFields.head
                val args = valueCollector.getFieldArgumentValues(
                  ExecutionPath.empty.add(astField, tpe),
                  Some(astField),
                  field.arguments,
                  astField.arguments,
                  unmarshalledVariables)

                args.toOption.map(PreparedField(field, _))
              case _ => None
            }

            QueryReducerExecutor
              .reduceQuery(
                schema,
                queryReducers,
                exceptionHandler,
                fieldCollector,
                valueCollector,
                unmarshalledVariables,
                tpe,
                fields,
                userContext)
              .map { case (newCtx, timing) =>
                new PreparedQuery[Ctx, Root, Input](
                  queryAst,
                  operation,
                  tpe,
                  newCtx,
                  root,
                  preparedFields,
                  (c: Ctx, r: Root, m: ResultMarshaller, scheme: ExecutionScheme) =>
                    executeOperation(
                      queryAst,
                      operationName,
                      variables,
                      um,
                      operation,
                      queryAst.sourceMapper,
                      valueCollector,
                      fieldCollector,
                      m,
                      unmarshalledVariables,
                      tpe,
                      fields,
                      c,
                      r,
                      scheme,
                      validationTiming,
                      timing
                    )
                )
              }
          }

          executionResult match {
            case Success(future) => future
            case Failure(error) => Future.failed(error)
          }
        }

    }
  }

  def execute[Input](
      queryAst: ast.Document,
      userContext: Ctx,
      root: Root,
      operationName: Option[String] = None,
      variables: Input = emptyMapVars
  )(implicit
      marshaller: ResultMarshaller,
      um: InputUnmarshaller[Input],
      scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] = {

    val scalarMiddleware = Middleware.composeFromScalarMiddleware(middleware, userContext)
    val valueCollector = new ValueCollector[Ctx, Input](
      schema,
      variables,
      queryAst.sourceMapper,
      deprecationTracker,
      userContext,
      exceptionHandler,
      scalarMiddleware,
      false)(um)

    val operationCtx = for {
      operation <- Executor.getOperation(exceptionHandler, queryAst, operationName)
      unmarshalledVariables <- valueCollector.getVariableValues(
        operation.variables,
        scalarMiddleware,
        errorsLimit
      )
    } yield (operation, unmarshalledVariables)

    operationCtx match {
      case Failure(error) =>
        // return validation errors without variables first if variables is what failed
        val violations = queryValidator.validateQuery(schema, queryAst, Map.empty, errorsLimit)
        if (violations.nonEmpty)
          scheme.failed(ValidationError(violations, exceptionHandler))
        else
          scheme.failed(error)
      case Success((operation, unmarshalledVariables)) =>
        val (violations, validationTiming) =
          TimeMeasurement.measure(
            queryValidator.validateQuery(schema, queryAst, unmarshalledVariables, errorsLimit))

        if (violations.nonEmpty)
          scheme.failed(ValidationError(violations, exceptionHandler))
        else {
          val executionResult = for {
            tpe <- Executor.getOperationRootType(
              schema,
              exceptionHandler,
              operation,
              queryAst.sourceMapper)
            fieldCollector = new FieldCollector[Ctx, Root](
              schema,
              queryAst,
              unmarshalledVariables,
              queryAst.sourceMapper,
              valueCollector,
              exceptionHandler)
            fields <- fieldCollector.collectFields(ExecutionPath.empty, tpe, Vector(operation))
          } yield {
            val reduced = QueryReducerExecutor.reduceQuery(
              schema,
              queryReducers,
              exceptionHandler,
              fieldCollector,
              valueCollector,
              unmarshalledVariables,
              tpe,
              fields,
              userContext)
            scheme.flatMapFuture(reduced) { case (newCtx, timing) =>
              executeOperation(
                queryAst,
                operationName,
                variables,
                um,
                operation,
                queryAst.sourceMapper,
                valueCollector,
                fieldCollector,
                marshaller,
                unmarshalledVariables,
                tpe,
                fields,
                newCtx,
                root,
                scheme,
                validationTiming,
                timing
              )
            }
          }

          executionResult match {
            case Success(result) => result
            case Failure(error) => scheme.failed(error)
          }
        }
    }

  }

  private def executeOperation[Input](
      queryAst: ast.Document,
      operationName: Option[String],
      inputVariables: Input,
      inputUnmarshaller: InputUnmarshaller[Input],
      operation: ast.OperationDefinition,
      sourceMapper: Option[SourceMapper],
      valueCollector: ValueCollector[Ctx, _],
      fieldCollector: FieldCollector[Ctx, Root],
      marshaller: ResultMarshaller,
      variables: Map[String, VariableValue],
      tpe: ObjectType[Ctx, Root],
      fields: CollectedFields,
      ctx: Ctx,
      root: Root,
      scheme: ExecutionScheme,
      validationTiming: TimeMeasurement,
      queryReducerTiming: TimeMeasurement
  ): scheme.Result[Ctx, marshaller.Node] = {
    val middlewareCtx = MiddlewareQueryContext(
      ctx,
      this,
      queryAst,
      operationName,
      inputVariables,
      inputUnmarshaller,
      validationTiming,
      queryReducerTiming)

    try {
      val middlewareVal = middleware.map(m => m.beforeQuery(middlewareCtx) -> m)
      val deferredResolverState = deferredResolver.initialQueryState

      val resolver = scheme.resolverBuilder.build[Ctx](
        marshaller,
        middlewareCtx,
        schema,
        valueCollector,
        variables,
        fieldCollector,
        ctx,
        exceptionHandler,
        deferredResolver,
        sourceMapper,
        deprecationTracker,
        middlewareVal,
        maxQueryDepth,
        deferredResolverState,
        scheme.extended,
        validationTiming,
        queryReducerTiming,
        queryAst
      )

      val result =
        operation.operationType match {
          case ast.OperationType.Query =>
            resolver
              .resolveFieldsPar(tpe, root, fields)(scheme)
              .asInstanceOf[scheme.Result[Ctx, marshaller.Node]]
          case ast.OperationType.Mutation =>
            resolver
              .resolveFieldsSeq(tpe, root, fields)(scheme)
              .asInstanceOf[scheme.Result[Ctx, marshaller.Node]]
          case ast.OperationType.Subscription =>
            tpe.uniqueFields.head.tags.collectFirst { case SubscriptionField(s) => s } match {
              case Some(stream) =>
                // Streaming is supported - resolve as a real subscription
                resolver
                  .resolveFieldsSubs(tpe, root, fields)(scheme)
                  .asInstanceOf[scheme.Result[Ctx, marshaller.Node]]
              case None =>
                // No streaming is supported - resolve as a normal "query" operation
                resolver
                  .resolveFieldsPar(tpe, root, fields)(scheme)
                  .asInstanceOf[scheme.Result[Ctx, marshaller.Node]]
            }

        }

      if (middlewareVal.nonEmpty)
        scheme.onComplete(result)(middlewareVal.foreach { case (v, m) =>
          m.afterQuery(v.asInstanceOf[m.QueryVal], middlewareCtx)
        })
      else result
    } catch {
      case NonFatal(error) =>
        scheme.failed(error)
    }
  }
}

object Executor {
  type ExceptionHandler = sangria.execution.ExceptionHandler

  def execute[Ctx, Root, Input](
      schema: Schema[Ctx, Root],
      queryAst: ast.Document,
      userContext: Ctx = (),
      root: Root = (),
      operationName: Option[String] = None,
      variables: Input = emptyMapVars,
      queryValidator: QueryValidator = QueryValidator.default,
      deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
      exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
      deprecationTracker: Option[DeprecationTracker] = None,
      middleware: List[Middleware[Ctx]] = Nil,
      maxQueryDepth: Option[Int] = None,
      queryReducers: List[QueryReducer[Ctx, _]] = Nil,
      errorsLimit: Option[Int] = None
  )(implicit
      executionContext: ExecutionContext,
      marshaller: ResultMarshaller,
      um: InputUnmarshaller[Input],
      scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] =
    Executor(
      schema,
      queryValidator,
      deferredResolver,
      exceptionHandler,
      deprecationTracker,
      middleware,
      maxQueryDepth,
      queryReducers,
      errorsLimit)
      .execute(queryAst, userContext, root, operationName, variables)

  def prepare[Ctx, Root, Input](
      schema: Schema[Ctx, Root],
      queryAst: ast.Document,
      userContext: Ctx = (),
      root: Root = (),
      operationName: Option[String] = None,
      variables: Input = emptyMapVars,
      queryValidator: QueryValidator = QueryValidator.default,
      deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
      exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
      deprecationTracker: Option[DeprecationTracker] = None,
      middleware: List[Middleware[Ctx]] = Nil,
      maxQueryDepth: Option[Int] = None,
      queryReducers: List[QueryReducer[Ctx, _]] = Nil,
      errorsLimit: Option[Int] = None
  )(implicit
      executionContext: ExecutionContext,
      um: InputUnmarshaller[Input]): Future[PreparedQuery[Ctx, Root, Input]] =
    Executor(
      schema,
      queryValidator,
      deferredResolver,
      exceptionHandler,
      deprecationTracker,
      middleware,
      maxQueryDepth,
      queryReducers,
      errorsLimit)
      .prepare(queryAst, userContext, root, operationName, variables)

  def getOperationRootType[Ctx, Root](
      schema: Schema[Ctx, Root],
      exceptionHandler: ExceptionHandler,
      operation: ast.OperationDefinition,
      sourceMapper: Option[SourceMapper]) = operation.operationType match {
    case ast.OperationType.Query =>
      Success(schema.query)
    case ast.OperationType.Mutation =>
      schema.mutation
        .map(Success(_))
        .getOrElse(
          Failure(
            OperationSelectionError(
              "Schema is not configured for mutations",
              exceptionHandler,
              sourceMapper,
              operation.location.toList)))
    case ast.OperationType.Subscription =>
      schema.subscription
        .map(Success(_))
        .getOrElse(
          Failure(
            OperationSelectionError(
              "Schema is not configured for subscriptions",
              exceptionHandler,
              sourceMapper,
              operation.location.toList)))
  }

  def getOperation(
      exceptionHandler: ExceptionHandler,
      document: ast.Document,
      operationName: Option[String]): Try[ast.OperationDefinition] =
    if (document.operations.size != 1 && operationName.isEmpty)
      Failure(
        OperationSelectionError(
          "Must provide operation name if query contains multiple operations",
          exceptionHandler))
    else {
      val unexpectedDefinition = document.definitions.find(d =>
        !(d.isInstanceOf[ast.OperationDefinition] || d.isInstanceOf[ast.FragmentDefinition]))

      unexpectedDefinition match {
        case Some(unexpected) =>
          Failure(new ExecutionError(
            s"GraphQL cannot execute a request containing a ${unexpected.getClass.getSimpleName}.",
            exceptionHandler))
        case None =>
          operationName match {
            case Some(opName) =>
              document.operations
                .get(Some(opName))
                .map(Success(_))
                .getOrElse(Failure(
                  OperationSelectionError(s"Unknown operation name '$opName'", exceptionHandler)))
            case None =>
              Success(document.operations.values.head)
          }
      }
    }
}

class PreparedQuery[Ctx, Root, Input] private[execution] (
    val queryAst: ast.Document,
    val operation: ast.OperationDefinition,
    val tpe: ObjectType[Ctx, Root],
    val userContext: Ctx,
    val root: Root,
    val fields: Seq[PreparedField[Ctx, Root]],
    execFn: (Ctx, Root, ResultMarshaller, ExecutionScheme) => Any) {
  def execute(userContext: Ctx = userContext, root: Root = root)(implicit
      marshaller: ResultMarshaller,
      scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] =
    execFn(userContext, root, marshaller, scheme).asInstanceOf[scheme.Result[Ctx, marshaller.Node]]
}

case class PreparedField[Ctx, Root](field: Field[Ctx, Root], args: Args)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy