Skip to content

Commit

Permalink
Fixes #99 #133 #157
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostdogpr committed Jan 12, 2020
1 parent 15306a5 commit 6fa249b
Show file tree
Hide file tree
Showing 13 changed files with 480 additions and 57 deletions.
8 changes: 6 additions & 2 deletions core/src/main/scala/caliban/CalibanError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ 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,
fieldName: Option[String] = None,
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 inner = innerThrowable.fold("")(e => s" with ${e.toString}")
Expand Down
58 changes: 50 additions & 8 deletions core/src/main/scala/caliban/GraphQL.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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.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 +22,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 +36,40 @@ 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)
)
ZIO
.environment[R]
.flatMap(
env =>
wrap(
prepare
.foldM(
Executor.fail,
req =>
ZIO
.environment[R]
.flatMap(
env =>
wrap(
Executor
.executeRequest(req._1, req._2, operationName, variables, queryAnalyzers, fieldWrappers)
.provide(env)
)(executionWrappers, req._1)
)
)
.provide(env)
)(overallWrappers, query)
)
}

/**
Expand Down Expand Up @@ -82,6 +107,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).
* @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 +133,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 +165,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 +188,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, ParsingError, ValidationError }
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 _: ParsingError | _: ValidationError => Json.obj("message" -> Json.fromString(err.toString))
case ExecutionError(_, _, path, _) =>
Json.obj(
"message" -> Json.fromString(err.toString),
"path" -> Json.fromValues(path.map {
case Left(value) => Json.fromString(value)
case Right(value) => Json.fromInt(value)
})
)
}

}
88 changes: 68 additions & 20 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,72 @@ 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)
GenericSchema.effectfulExecutionError(currentField.name, path, _),
reduceStep(_, currentField, arguments, path)
)
)
case StreamStep(stream) =>
ReducedStep.StreamStep(
stream.bimap(
GenericSchema.effectfulExecutionError(currentField.name, _),
reduceStep(_, currentField, arguments)
GenericSchema.effectfulExecutionError(currentField.name, 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)
.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 +194,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,13 +236,24 @@ object Executor {
.toList
}

private def fieldInfo(field: Field, path: List[Either[String, Int]]): FieldInfo =
FieldInfo(
field.alias.getOrElse(field.name),
path,
field.parentType.flatMap(_.name).getOrElse(""), // TODO should go up to catch [] and !
field.fieldType.name.getOrElse("") // TODO should go up to catch [] and !
)

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]))
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)]].map {
case (k, v) => k -> v.value
}))
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/caliban/execution/FieldInfo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package caliban.execution

case class FieldInfo(fieldName: String, path: List[Either[String, Int]], parentType: String, returnType: String)
9 changes: 5 additions & 4 deletions core/src/main/scala/caliban/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,9 @@ trait DerivationSchema[R] {

object GenericSchema {

def effectfulExecutionError(fieldName: String, e: Throwable): ExecutionError = e match {
case e: ExecutionError => e
case other => ExecutionError("Effect failure", Some(fieldName), Some(other))
}
def effectfulExecutionError(fieldName: String, path: List[Either[String, Int]], e: Throwable): ExecutionError =
e match {
case e: ExecutionError => e
case other => ExecutionError("Effect failure", Some(fieldName), path, Some(other))
}
}
Loading

0 comments on commit 6fa249b

Please sign in to comment.