Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrappers + Apollo Tracing #165

Merged
merged 13 commits into from
Jan 17, 2020
9 changes: 6 additions & 3 deletions core/src/main/scala/caliban/CalibanError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ object CalibanError {
/**
* Describes an error that happened while executing a query.
*/
case class ExecutionError(msg: String, fieldName: Option[String] = None, innerThrowable: Option[Throwable] = None)
extends CalibanError {
case class ExecutionError(
msg: String,
path: List[Either[String, Int]] = Nil,
innerThrowable: Option[Throwable] = None
) extends CalibanError {
override def toString: String = {
val field = fieldName.fold("")(f => s" on field '$f'")
val field = path.lastOption.fold("")(f => s" on field '$f'")
val inner = innerThrowable.fold("")(e => s" with ${e.toString}")
s"Execution error$field: $msg$inner"
}
Expand Down
52 changes: 44 additions & 8 deletions core/src/main/scala/caliban/GraphQL.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package caliban

import caliban.Rendering.renderTypes
import caliban.execution.Executor
import caliban.execution.QueryAnalyzer.QueryAnalyzer
import caliban.execution.Executor
import caliban.introspection.Introspector
import caliban.parsing.Parser
import caliban.parsing.adt.Document
import caliban.schema.RootSchema.Operation
import caliban.schema._
import caliban.validation.Validator
import zio.{ IO, URIO }
import caliban.wrappers.Wrapper
import caliban.wrappers.Wrapper._
import zio.{ IO, URIO, ZIO }

/**
* A `GraphQL[-R]` represents a GraphQL API whose execution requires a ZIO environment of type `R`.
Expand All @@ -20,6 +23,7 @@ trait GraphQL[-R] { self =>

protected val schema: RootSchema[R]
protected val queryAnalyzers: List[QueryAnalyzer[R]]
protected val wrappers: List[Wrapper[R]]

private lazy val rootType: RootType =
RootType(schema.query.opType, schema.mutation.map(_.opType), schema.subscription.map(_.opType))
Expand All @@ -33,18 +37,33 @@ trait GraphQL[-R] { self =>
skipValidation: Boolean
): URIO[R, GraphQLResponse[CalibanError]] = {

val (overallWrappers, parsingWrappers, validationWrappers, executionWrappers, fieldWrappers) = decompose(wrappers)
val prepare = for {
document <- Parser.parseQuery(query)
document <- wrap(Parser.parseQuery(query))(parsingWrappers, query)
intro = Introspector.isIntrospection(document)
typeToValidate = if (intro) introspectionRootType else rootType
schemaToExecute = if (intro) introspectionRootSchema else schema
_ <- IO.when(!skipValidation)(Validator.validate(document, typeToValidate))
_ <- ZIO.when(!skipValidation) {
wrap(Validator.validate(document, typeToValidate))(validationWrappers, document)
}
} yield (document, schemaToExecute)

prepare.foldM(
Executor.fail,
req => Executor.executeRequest(req._1, req._2, operationName, variables, queryAnalyzers)
)
val execute = (req: (Document, RootSchema[R])) =>
Executor.executeRequest(req._1, req._2, operationName, variables, queryAnalyzers, fieldWrappers)

ZIO
.environment[R]
.flatMap(
env =>
wrap(
prepare
.foldM(
Executor.fail,
req => ZIO.environment[R].flatMap(env => wrap(execute(req).provide(env))(executionWrappers, req._1))
)
.provide(env)
)(overallWrappers, query)
)
}

/**
Expand Down Expand Up @@ -82,6 +101,20 @@ trait GraphQL[-R] { self =>
new GraphQL[R2] {
override val schema: RootSchema[R2] = self.schema
override val queryAnalyzers: List[QueryAnalyzer[R2]] = queryAnalyzer :: self.queryAnalyzers
override protected val wrappers: List[Wrapper[R2]] = self.wrappers
}

/**
* Attaches a function that will wrap one of the stages of query processing
* (parsing, validation, execution, field execution or overall).
* @param wrapper a wrapping function
* @return a new GraphQL API
*/
final def withWrapper[R2 <: R](wrapper: Wrapper[R2]): GraphQL[R2] =
new GraphQL[R2] {
override val schema: RootSchema[R2] = self.schema
override val queryAnalyzers: List[QueryAnalyzer[R2]] = self.queryAnalyzers
override val wrappers: List[Wrapper[R2]] = wrapper :: self.wrappers
}

/**
Expand All @@ -94,6 +127,7 @@ trait GraphQL[-R] { self =>
new GraphQL[R1] {
override val schema: RootSchema[R1] = self.schema |+| that.schema
override val queryAnalyzers: List[QueryAnalyzer[R1]] = self.queryAnalyzers ++ that.queryAnalyzers
override protected val wrappers: List[Wrapper[R1]] = self.wrappers ++ that.wrappers
}

/**
Expand Down Expand Up @@ -125,6 +159,7 @@ trait GraphQL[-R] { self =>
)
)
override protected val queryAnalyzers: List[QueryAnalyzer[R]] = self.queryAnalyzers
override protected val wrappers: List[Wrapper[R]] = self.wrappers
}
}

Expand All @@ -147,5 +182,6 @@ object GraphQL {
resolver.subscriptionResolver.map(r => Operation(subscriptionSchema.toType(), subscriptionSchema.resolve(r)))
)
val queryAnalyzers: List[QueryAnalyzer[R]] = Nil
val wrappers: List[Wrapper[R]] = Nil
}
}
2 changes: 1 addition & 1 deletion core/src/main/scala/caliban/GraphQLInterpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ trait GraphQLInterpreter[-R, +E] { self =>
* @return a new GraphQL interpreter with error type `E2`
*/
final def mapError[E2](f: E => E2): GraphQLInterpreter[R, E2] =
wrapExecutionWith(_.map(res => GraphQLResponse(res.data, res.errors.map(f))))
wrapExecutionWith(_.map(res => GraphQLResponse(res.data, res.errors.map(f), res.extensions)))

/**
* Eliminates the ZIO environment R requirement of the interpreter.
Expand Down
35 changes: 28 additions & 7 deletions core/src/main/scala/caliban/GraphQLResponse.scala
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
package caliban

import caliban.CalibanError.ExecutionError
import caliban.ResponseValue.ObjectValue
import caliban.interop.circe._

/**
* Represents the result of a GraphQL query, containing a data object and a list of errors.
*/
case class GraphQLResponse[+E](data: ResponseValue, errors: List[E])
case class GraphQLResponse[+E](data: ResponseValue, errors: List[E], extensions: Option[ObjectValue] = None)

object GraphQLResponse {
implicit def circeEncoder[F[_]: IsCirceEncoder, E]: F[GraphQLResponse[E]] =
GraphQLResponceCirce.graphQLResponseEncoder.asInstanceOf[F[GraphQLResponse[E]]]
GraphQLResponseCirce.graphQLResponseEncoder.asInstanceOf[F[GraphQLResponse[E]]]
}

private object GraphQLResponceCirce {
private object GraphQLResponseCirce {
import io.circe._
import io.circe.syntax._
val graphQLResponseEncoder: Encoder[GraphQLResponse[CalibanError]] = Encoder
.instance[GraphQLResponse[CalibanError]] {
case GraphQLResponse(data, Nil) => Json.obj("data" -> data.asJson)
case GraphQLResponse(data, errors) =>
case GraphQLResponse(data, Nil, None) => Json.obj("data" -> data.asJson)
case GraphQLResponse(data, Nil, Some(extensions)) =>
Json.obj("data" -> data.asJson, "extensions" -> extensions.asInstanceOf[ResponseValue].asJson)
case GraphQLResponse(data, errors, None) =>
Json.obj("data" -> data.asJson, "errors" -> Json.fromValues(errors.map(handleError)))
case GraphQLResponse(data, errors, Some(extensions)) =>
Json.obj(
"data" -> data.asJson,
"errors" -> Json.fromValues(errors.map(err => Json.obj("message" -> Json.fromString(err.toString))))
"data" -> data.asJson,
"errors" -> Json.fromValues(errors.map(handleError)),
"extensions" -> extensions.asInstanceOf[ResponseValue].asJson
)
}

private def handleError(err: CalibanError): Json =
err match {
case ExecutionError(_, path, _) if path.nonEmpty =>
Json.obj(
"message" -> Json.fromString(err.toString),
"path" -> Json.fromValues(path.map {
case Left(value) => Json.fromString(value)
case Right(value) => Json.fromInt(value)
})
)
case _ => Json.obj("message" -> Json.fromString(err.toString))
}

}
89 changes: 63 additions & 26 deletions core/src/main/scala/caliban/execution/Executor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import caliban.parsing.adt.OperationType.{ Mutation, Query, Subscription }
import caliban.parsing.adt._
import caliban.schema.Step._
import caliban.schema.{ GenericSchema, ReducedStep, RootSchema, Step }
import caliban.wrappers.Wrapper.FieldWrapper
import caliban.{ CalibanError, GraphQLResponse, InputValue, ResponseValue }
import zio._
import zquery.ZQuery
import zquery.{ Described, ZQuery }

object Executor {

Expand All @@ -28,7 +29,8 @@ object Executor {
schema: RootSchema[R],
operationName: Option[String] = None,
variables: Map[String, InputValue] = Map(),
queryAnalyzers: List[QueryAnalyzer[R]] = Nil
queryAnalyzers: List[QueryAnalyzer[R]] = Nil,
fieldWrappers: List[FieldWrapper[R]] = Nil
): URIO[R, GraphQLResponse[CalibanError]] = {
val fragments = document.definitions.collect {
case fragment: FragmentDefinition => fragment.name -> fragment
Expand Down Expand Up @@ -66,7 +68,14 @@ object Executor {
.foldLeft(queryAnalyzers)(Field(op.selectionSet, fragments, variables, operationType.opType)) {
case (field, analyzer) => analyzer(field)
}
result <- executePlan(operationType.plan, root, op.variableDefinitions, variables, allowParallelism)
result <- executePlan(
operationType.plan,
root,
op.variableDefinitions,
variables,
allowParallelism,
fieldWrappers
)
} yield result).catchAll(fail)
}
}
Expand All @@ -79,13 +88,15 @@ object Executor {
root: Field,
variableDefinitions: List[VariableDefinition],
variableValues: Map[String, InputValue],
allowParallelism: Boolean
allowParallelism: Boolean,
fieldWrappers: List[FieldWrapper[R]]
): URIO[R, GraphQLResponse[CalibanError]] = {

def reduceStep(
step: Step[R],
currentField: Field,
arguments: Map[String, InputValue]
arguments: Map[String, InputValue],
path: List[Either[String, Int]]
): ReducedStep[R] =
step match {
case s @ PureStep(value) =>
Expand All @@ -101,46 +112,66 @@ object Executor {
obj.fold(s)(PureStep(_))
case _ => s
}
case FunctionStep(step) => reduceStep(step(arguments), currentField, Map())
case ListStep(steps) => reduceList(steps.map(reduceStep(_, currentField, arguments)))
case FunctionStep(step) => reduceStep(step(arguments), currentField, Map(), path)
case ListStep(steps) =>
reduceList(steps.zipWithIndex.map {
case (step, i) => reduceStep(step, currentField, arguments, Right(i) :: path)
})
case ObjectStep(objectName, fields) =>
val mergedFields = mergeFields(currentField, objectName)
val items = mergedFields.map {
case Field(name @ "__typename", _, _, alias, _, _, _) =>
alias.getOrElse(name) -> PureStep(StringValue(objectName))
case f @ Field(name @ "__typename", _, _, alias, _, _, _) =>
(alias.getOrElse(name), PureStep(StringValue(objectName)), fieldInfo(f, path))
case f @ Field(name, _, _, alias, _, _, args) =>
val arguments = resolveVariables(args, variableDefinitions, variableValues)
alias.getOrElse(name) ->
(
alias.getOrElse(name),
fields
.get(name)
.fold(NullStep: ReducedStep[R])(reduceStep(_, f, arguments))
.fold(NullStep: ReducedStep[R])(reduceStep(_, f, arguments, Left(alias.getOrElse(name)) :: path)),
fieldInfo(f, path)
)
}
reduceObject(items)
reduceObject(items, fieldWrappers)
case QueryStep(inner) =>
ReducedStep.QueryStep(
inner.bimap(
GenericSchema.effectfulExecutionError(currentField.name, _),
reduceStep(_, currentField, arguments)
)
inner.bimap(GenericSchema.effectfulExecutionError(path, _), reduceStep(_, currentField, arguments, path))
)
case StreamStep(stream) =>
ReducedStep.StreamStep(
stream.bimap(
GenericSchema.effectfulExecutionError(currentField.name, _),
reduceStep(_, currentField, arguments)
)
stream.bimap(GenericSchema.effectfulExecutionError(path, _), reduceStep(_, currentField, arguments, path))
)
}

def makeQuery(step: ReducedStep[R], errors: Ref[List[CalibanError]]): ZQuery[R, Nothing, ResponseValue] = {

def wrap(query: ZQuery[R, Nothing, ResponseValue])(
wrappers: List[FieldWrapper[R]],
fieldInfo: FieldInfo
): ZQuery[R, Nothing, ResponseValue] =
wrappers match {
case Nil => query
case wrapper :: tail =>
ZQuery
.environment[R]
.flatMap(
env =>
wrap(
wrapper
.f(query.provide(Described(env, "Wrapper")), fieldInfo)
ghostdogpr marked this conversation as resolved.
Show resolved Hide resolved
.foldM(error => ZQuery.fromEffect(errors.update(error :: _)).map(_ => NullValue), ZQuery.succeed)
)(tail, fieldInfo)
)
}

def loop(step: ReducedStep[R]): ZQuery[R, Nothing, ResponseValue] =
step match {
case PureStep(value) => ZQuery.succeed(value)
case ReducedStep.ListStep(steps) =>
val queries = steps.map(loop)
(if (allowParallelism) ZQuery.collectAllPar(queries) else ZQuery.collectAll(queries)).map(ListValue)
case ReducedStep.ObjectStep(steps) =>
val queries = steps.map { case (name, field) => loop(field).map(name -> _) }
val queries = steps.map { case (name, step, info) => wrap(loop(step))(fieldWrappers, info).map(name -> _) }
(if (allowParallelism) ZQuery.collectAllPar(queries) else ZQuery.collectAll(queries)).map(ObjectValue)
case ReducedStep.QueryStep(step) =>
step.foldM(
Expand All @@ -157,7 +188,7 @@ object Executor {

for {
errors <- Ref.make(List.empty[CalibanError])
reduced = reduceStep(plan, root, Map())
reduced = reduceStep(plan, root, Map(), Nil)
query = makeQuery(reduced, errors)
result <- query.run
resultErrors <- errors.get
Expand Down Expand Up @@ -199,15 +230,21 @@ object Executor {
.toList
}

private def fieldInfo(field: Field, path: List[Either[String, Int]]): FieldInfo =
FieldInfo(field.alias.getOrElse(field.name), path, field.parentType, field.fieldType)

private def reduceList[R](list: List[ReducedStep[R]]): ReducedStep[R] =
if (list.forall(_.isInstanceOf[PureStep]))
PureStep(ListValue(list.asInstanceOf[List[PureStep]].map(_.value)))
else ReducedStep.ListStep(list)

private def reduceObject[R](items: List[(String, ReducedStep[R])]): ReducedStep[R] =
if (items.map(_._2).forall(_.isInstanceOf[PureStep]))
PureStep(ObjectValue(items.asInstanceOf[List[(String, PureStep)]].map {
case (k, v) => k -> v.value
private def reduceObject[R](
items: List[(String, ReducedStep[R], FieldInfo)],
fieldWrappers: List[FieldWrapper[R]]
): ReducedStep[R] =
if (!fieldWrappers.exists(_.wrapPureValues) && items.map(_._2).forall(_.isInstanceOf[PureStep]))
PureStep(ObjectValue(items.asInstanceOf[List[(String, PureStep, FieldInfo)]].map {
case (k, v, _) => (k, v.value)
}))
else ReducedStep.ObjectStep(items)

Expand Down
Loading