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

com.iterable.graphql.FromGraphQLJava.scala Maven / Gradle / Ivy

The newest version!
package com.iterable.graphql

import java.util.concurrent.CompletableFuture

import com.iterable.graphql.compiler.Schema
import graphql.ExecutionInput
import graphql.Scalars
import graphql.execution.ExecutionContext
import graphql.execution.ExecutionStepInfo
import graphql.execution.FetchedValue
import graphql.execution.MergedField
import graphql.execution.ValuesResolver
import graphql.execution.nextgen.ExecutionStrategy
import graphql.execution.nextgen.FetchedValueAnalysis
import graphql.execution.nextgen.FieldSubSelection
import graphql.execution.nextgen.result.ExecutionResultNode
import graphql.execution.nextgen.result.LeafExecutionResultNode
import graphql.execution.nextgen.result.RootExecutionResultNode
import graphql.introspection.Introspection
import graphql.language.{Field => JField}
import graphql.schema.DataFetchingFieldSelectionSet
import graphql.schema.DataFetchingFieldSelectionSetImpl
import graphql.schema.GraphQLTypeUtil
import graphql.schema.idl.EchoingWiringFactory
import graphql.schema.idl.SchemaGenerator
import graphql.schema.GraphQLSchema
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaParser
import io.circe.Json
import io.circe.JsonNumber
import io.circe.JsonObject
import play.api.libs.json.{JsArray, JsBoolean, JsNull, JsNumber, JsObject, JsString, JsValue}

import scala.concurrent.Future
import scala.collection.JavaConverters.iterableAsScalaIterableConverter
import scala.collection.JavaConverters.mapAsScalaMapConverter
import scala.collection.JavaConverters.mapAsJavaMapConverter
import scala.util.Try

/**
  * Document and schema parsing using the GraphQL Java library.
  */
object FromGraphQLJava {

  /** Parse and validate documents (queries and mutations) using the GraphQL Java library.
    * This requires a GraphQL Java GraphQLSchema. Returns our Query type.
    *
    * Overall flow looks as follows:
    * - Build a GraphQL Java GraphQLSchema whether from SDL or programmatically.
    * - Use GraphQL Java to parse the document (i.e. query) into Java AST
    * - Use GraphQL Java to resolve variables etc.
    * - Convert to Scala AST by traversing Java using selection-set peeking. This is what the code below does.
    * - Compile Scala AST into DBIO
    * - Run DBIO
    *
    * This method uses "field selection sets" from GraphQL-Java to extract a Field tree for the query:
    * https://www.graphql-java.com/documentation/v12/fieldselection/
    */
  def parseAndValidateQuery(graphQLSchema: GraphQLSchema, query: String, variables: Json): Try[Query[Field.Fixed]] = {
    val extractExecutionStrategy = new ContextExtractingExecutionStrategy
    Try {
      val result = graphql.nextgen.GraphQL.newGraphQL(graphQLSchema)
        .executionStrategy(extractExecutionStrategy)
        .build()
        .execute(ExecutionInput.newExecutionInput(query)
          .variables(toJavaValues(variables).asInstanceOf[Map[String, AnyRef]].asJava)
          .build())
      val errors = result.getErrors
      if (!errors.isEmpty) {
        // TODO: consider using an Either
        throw new IllegalArgumentException(errors.toString)
      }
      val (context: ExecutionContext, fieldSubSelection: FieldSubSelection) =
        result
          .getData[java.util.LinkedHashMap[String, Any]]
          .get(null) // the map will have a value with key = null

      val valuesResolver = new ValuesResolver
      val topLevelFields: Seq[Field.Fixed] =
        fieldSubSelection.getMergedSelectionSet.getSubFieldsList.asScala.map { mergedField =>
          // See DataFetchingSelectionSetImpl.traverseFields
          val fieldDef = Introspection.getFieldDef(graphQLSchema, graphQLSchema.getQueryType, mergedField.getName)
          val unwrappedType = GraphQLTypeUtil.unwrapAll(fieldDef.getType)
          // See ValueFetcher.fetchValue() or other callers of DataFetchingFieldSelectionSetImpl.newCollector()
          val argumentValues = valuesResolver.getArgumentValues(fieldDef.getArguments, mergedField.getArguments, context.getVariables)
          val selectionSet = DataFetchingFieldSelectionSetImpl.newCollector(context, unwrappedType, mergedField)
          mkField(mergedField.getName, argumentValues, selectionSet)
        }.toSeq
      Query[Field.Fixed](topLevelFields)
    }
  }

  private def toJavaValues(value: Json): AnyRef = {
    value.foldWith(new Json.Folder[AnyRef] {
      override def onNull = null
      override def onBoolean(value: Boolean) = value: java.lang.Boolean
      override def onNumber(value: JsonNumber) = value.toBigDecimal
      override def onString(value: String) = value
      override def onArray(value: Vector[Json]) = value.map(toJavaValues)
      override def onObject(value: JsonObject) = value.toMap.mapValues(toJavaValues).toMap
    })
  }

  import higherkindness.droste.syntax.fix._
  private def mkField(fieldName: String, arguments: java.util.Map[String, AnyRef], fss: DataFetchingFieldSelectionSet): Field.Fixed = {
    // getFields contains flattened fields from all children, but we only want the immediate children
    val childFields = fss.getFields.asScala.filterNot(_.getQualifiedName.contains("/"))
    val args = JsObject(arguments.asScala.mapValues(fromJavaValue).toMap)
    Field[Field.Fixed](
      fieldName,
      args,
      childFields.map { selectedField =>
        val subfss = selectedField.getSelectionSet
        mkField(selectedField.getName, selectedField.getArguments, subfss)
      }.toSeq,
    ).fix
  }

  private def fromJavaValue(value: Any): JsValue = {
    value match {
      case x if x == null => JsNull
      case f: Float => JsNumber(BigDecimal.decimal(f))
      case d: Double => JsNumber(BigDecimal.decimal(d))
      case s: String => JsString(s)
      case n: Int => JsNumber(n)
      case n: Long => JsNumber(n)
      case f: Boolean => JsBoolean(f)
      case rg: Array[_] => JsArray(rg.map(fromJavaValue))
      case v => throw new IllegalArgumentException(s"Java value $v has unexpected type ${v.getClass}")
    }
  }

  /**
    * Create a GraphQL Java GraphQLSchema from a GraphQL SDL String.
    */
  def parseSchema(schemaStr: String): GraphQLSchema = {
    val schemaParser = new SchemaParser
    val schemaGenerator = new SchemaGenerator

    val typeRegistry = schemaParser.parse(schemaStr)
    val wiring = RuntimeWiring.newRuntimeWiring().wiringFactory(new EchoingWiringFactory).build()
    val graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, wiring)
    graphQLSchema
  }

  def toSchemaFunction(schema: GraphQLSchema): Schema = new Schema {
    override def getUnwrappedTypeNameOf(parentTypeName: Option[String], fieldName: String) = {
      // TODO: there should be a better way to handle this. Maybe using TraversalContext.getFieldDef
      if (fieldName == Introspection.TypeNameMetaFieldDef.getName) {
        // This doesn't really matter today since we don't currently do anything with Scalars
        // it's defined as nonNull(GraphQLString)
        GraphQLTypeUtil.unwrapAll(Introspection.TypeNameMetaFieldDef.getType).getName
      } else {
        // TODO: obv we need to handle nulls and return options etc.
        val fieldType =
          parentTypeName.map { parentEntityName =>
            val parentType = Option(schema.getObjectType(parentEntityName)).getOrElse(throw new NoSuchElementException(parentEntityName))
            val fieldDefn = Option(parentType.getFieldDefinition(fieldName)).getOrElse(throw new NoSuchElementException(parentEntityName + " " + fieldName))
            fieldDefn.getType
          }.getOrElse {
            schema.getQueryType.getFieldDefinition(fieldName).getType
          }
        GraphQLTypeUtil.unwrapAll(fieldType).getName
      }
    }
  }
}

private[graphql] class ContextExtractingExecutionStrategy extends ExecutionStrategy {
  override def execute(context: ExecutionContext, fieldSubSelection: FieldSubSelection): CompletableFuture[RootExecutionResultNode] = {
    // This is a hack to extract the FieldSubSelection for the query without
    // truly executing it.
    // In the future we can add a method alongside GraphQL.execute() that just returns
    // the ExecutionData directly
    // See call hierarchy of DataFetchingFieldSelectionSetImpl.newCollector()
    import scala.compat.java8.FutureConverters.FutureOps
    val result =
      new LeafExecutionResultNode(
        FetchedValueAnalysis.newFetchedValueAnalysis()
          .fetchedValue(FetchedValue.newFetchedValue()
            .fetchedValue((context, fieldSubSelection))
            .build())
          .completedValue((context, fieldSubSelection))
          .valueType(FetchedValueAnalysis.FetchedValueType.SCALAR)
          .executionStepInfo(
            ExecutionStepInfo.newExecutionStepInfo().`type`(Scalars.GraphQLString)
              .field(MergedField.newMergedField(JField.newField().build()).build())
              .build())
          .errors(new java.util.ArrayList)
          .build(),
        null
      )
    val list = new java.util.ArrayList[ExecutionResultNode]()
    list.add(result)
    Future.successful[RootExecutionResultNode](new RootExecutionResultNode(list))
      .toJava.toCompletableFuture
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy