diff --git a/src/main/scala/sangria/gateway/http/GatewayServer.scala b/src/main/scala/sangria/gateway/http/GatewayServer.scala index a96b023..4146dc5 100644 --- a/src/main/scala/sangria/gateway/http/GatewayServer.scala +++ b/src/main/scala/sangria/gateway/http/GatewayServer.scala @@ -26,7 +26,7 @@ class GatewayServer extends Logging { val client = new PlayHttpClient(new StandaloneAhcWSClient(new DefaultAsyncHttpClient)) val directiveProviders = Map( - "http" → new HttpDirectiveProvider(client), + "http" → new HttpDirectiveProvider, "graphql" → new GraphQLDirectiveProvider, "faker" → new FakerDirectiveProvider, "basic" → new BasicDirectiveProvider) @@ -43,7 +43,7 @@ class GatewayServer extends Logging { schemaProvider.schemaInfo // trigger initial schema load at startup - val routing = new GraphQLRouting(config, schemaProvider) + val routing = new GraphQLRouting(config, schemaProvider, new RequestResolver(client)) Http().bindAndHandle(routing.route, config.bindHost, config.port).andThen { case Success(_) ⇒ diff --git a/src/main/scala/sangria/gateway/http/GraphQLRouting.scala b/src/main/scala/sangria/gateway/http/GraphQLRouting.scala index ea0b4c7..4889eed 100644 --- a/src/main/scala/sangria/gateway/http/GraphQLRouting.scala +++ b/src/main/scala/sangria/gateway/http/GraphQLRouting.scala @@ -16,9 +16,11 @@ import io.circe.optics.JsonPath._ import io.circe.parser._ import sangria.ast.Document import sangria.execution._ +import sangria.execution.deferred.DeferredResolver import sangria.gateway.AppConfig import sangria.gateway.schema.SchemaProvider import sangria.gateway.schema.materializer.GatewayContext +import sangria.gateway.schema.materializer.directive.RequestResolver import sangria.gateway.util.Logging import sangria.parser.{QueryParser, SyntaxError} import sangria.marshalling.circe._ @@ -28,7 +30,11 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} import scala.util.control.NonFatal -class GraphQLRouting[Val](config: AppConfig, schemaProvider: SchemaProvider[GatewayContext, Val])(implicit ec: ExecutionContext) extends Logging { +class GraphQLRouting[Val]( + config: AppConfig, + schemaProvider: SchemaProvider[GatewayContext, Val], + deferredResolver: DeferredResolver[GatewayContext] +)(implicit ec: ExecutionContext) extends Logging { val route: Route = path("graphql") { get { @@ -126,7 +132,7 @@ class GraphQLRouting[Val](config: AppConfig, schemaProvider: SchemaProvider[Gate else withSlowLog } - + def executeGraphQL(query: Document, operationName: Option[String], variables: Json) = complete(schemaProvider.schemaInfo.flatMap { case Some(schemaInfo) ⇒ @@ -139,7 +145,7 @@ class GraphQLRouting[Val](config: AppConfig, schemaProvider: SchemaProvider[Gate queryReducers = reducers.asInstanceOf[List[QueryReducer[GatewayContext, _]]], middleware = middleware ++ schemaInfo.middleware, exceptionHandler = exceptionHandler, - deferredResolver = schemaInfo.deferredResolver) + deferredResolver = deferredResolver) .map(res ⇒ TRM(OK → res)) .recover { case error: QueryAnalysisError ⇒ TRM(BadRequest → error.resolveError) diff --git a/src/main/scala/sangria/gateway/schema/materializer/directive/HttpDirectiveProvider.scala b/src/main/scala/sangria/gateway/schema/materializer/directive/HttpDirectiveProvider.scala index ec8fc97..71a8069 100644 --- a/src/main/scala/sangria/gateway/schema/materializer/directive/HttpDirectiveProvider.scala +++ b/src/main/scala/sangria/gateway/schema/materializer/directive/HttpDirectiveProvider.scala @@ -1,6 +1,7 @@ package sangria.gateway.schema.materializer.directive import io.circe.Json +import sangria.execution.deferred.{Deferred, DeferredResolver} import sangria.gateway.http.client.HttpClient import sangria.gateway.json.CirceJsonPath import sangria.gateway.schema.materializer.GatewayContext @@ -8,43 +9,81 @@ import sangria.gateway.schema.materializer.GatewayContext.{convertArgs, namedTyp import sangria.schema.ResolverBasedAstSchemaBuilder.extractValue import sangria.schema._ import sangria.marshalling.circe._ +import sangria.schema.InputObjectType.DefaultInput import scala.concurrent.{ExecutionContext, Future} -class HttpDirectiveProvider(client: HttpClient)(implicit ec: ExecutionContext) extends DirectiveProvider { +class HttpDirectiveProvider extends DirectiveProvider { import HttpDirectiveProvider._ def resolvers(ctx: GatewayContext) = Seq( DirectiveResolver(Dirs.HttpGet, complexity = Some(_ ⇒ (_, _, _) ⇒ 1000.0), - resolve = c ⇒ c.withArgs(Args.Url, Args.Headers, Args.QueryParams, Args.ForAll) { (rawUrl, rawHeaders, rawQueryParams, forAll) ⇒ - val args = Some(convertArgs(c.ctx.args, c.ctx.astFields.head)) + resolve = c ⇒ { + val json = c.ctx.value match { + case j: Json => j + case _ => Json.Null + } + RequestDescription( + c.ctx, + c.arg(Args.Url), + c.arg(Args.Headers), + c.arg(Args.QueryParams), + c.arg(Args.ForAll), + c.arg(Args.BatchUrl), + c.arg(Args.IdFieldInBatchResponse), + json + ) + } + ) + ) +} + +case class RequestDescription( + context: Context[GatewayContext, _], + rawUrl: String, + rawHeaders: Option[Seq[DefaultInput]], + rawQueryParams: Option[Seq[DefaultInput]], + forAll: Option[String], + batchUrl: Option[String], + idFieldInBatchResponse: Option[String], + id: Json +) extends Deferred[Json] + +class RequestResolver(client: HttpClient) extends DeferredResolver[GatewayContext] { + def resolve(deferred: Vector[Deferred[Any]], gwCtx: GatewayContext, queryState: Any)(implicit ec: ExecutionContext): Vector[Future[Any]] = { + deferred.map { + case rd: RequestDescription => + import rd._ + val args = Some(convertArgs(context.args, context.astFields.head)) def extractMap(in: Option[scala.Seq[InputObjectType.DefaultInput]], elem: Json) = - rawHeaders.map(_.map(h ⇒ h("name").asInstanceOf[String] → c.ctx.ctx.fillPlaceholders(c.ctx, h("value").asInstanceOf[String], args, elem))).getOrElse(Nil) + rawHeaders.map(_.map(h ⇒ h("name").asInstanceOf[String] → gwCtx.fillPlaceholders(context, h("value").asInstanceOf[String], args, elem))).getOrElse(Nil) - def makeRequest(tpe: OutputType[_], c: Context[GatewayContext, _], args: Option[Json], elem: Json = Json.Null) = { - val url = c.ctx.fillPlaceholders(c, rawUrl, args, elem) + def makeRequest(tpe: OutputType[_], c: Context[GatewayContext, _], args: Option[Json], elem: Json = Json.Null): Future[Json] = { + val url = gwCtx.fillPlaceholders(c, rawUrl, args, elem) val headers = extractMap(rawHeaders, elem) val query = extractMap(rawQueryParams, elem) client.request(HttpClient.Method.Get, url, query, headers).flatMap(GatewayContext.parseJson) } + val fieldType: OutputType[_] = context.field.fieldType forAll match { - case Some(elem) ⇒ - CirceJsonPath.query(c.ctx.value.asInstanceOf[Json], elem) match { + case Some(jsonPath) ⇒ + CirceJsonPath.query(id, jsonPath) match { case json: Json if json.isArray ⇒ - Future.sequence(json.asArray.get.map(e ⇒ makeRequest(namedType(c.ctx.field.fieldType), c.ctx, args, e))) map { v ⇒ - extractValue(c.ctx.field.fieldType, Some(Json.arr(v.asInstanceOf[Seq[Json]]: _*))) + Future.sequence(json.asArray.get.map(e ⇒ makeRequest(namedType(fieldType), context, args, e))) map { v ⇒ + extractValue(fieldType, Some(Json.arr(v.asInstanceOf[Seq[Json]]: _*))) } case e ⇒ - makeRequest(c.ctx.field.fieldType, c.ctx, args, e) + makeRequest(fieldType, context, args, e) } case None ⇒ - makeRequest(c.ctx.field.fieldType, c.ctx, args) + makeRequest(fieldType, context, args) } - })) + } + } } object HttpDirectiveProvider { @@ -61,11 +100,13 @@ object HttpDirectiveProvider { val Headers = Argument("headers", OptionInputType(ListInputType(HeaderType))) val QueryParams = Argument("query", OptionInputType(ListInputType(QueryParamType))) val ForAll = Argument("forAll", OptionInputType(StringType)) + val BatchUrl = Argument("batchUrl", OptionInputType(StringType)) + val IdFieldInBatchResponse = Argument("idFieldInBatchResponse", OptionInputType(StringType)) } object Dirs { val HttpGet = Directive("httpGet", - arguments = Args.Url :: Args.Headers :: Args.QueryParams :: Args.ForAll :: Nil, + arguments = Args.Url :: Args.Headers :: Args.QueryParams :: Args.ForAll :: Args.BatchUrl :: Args.IdFieldInBatchResponse :: Nil, locations = Set(DirectiveLocation.FieldDefinition)) } }